From 2f2a213da05b4ccb346b647625c184e25420c058 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Tue, 6 Jan 2026 22:52:08 +0500 Subject: [PATCH 001/117] refactor: implement an adapter pattern for schema coercion and internalize utility functions. --- package.json | 4 +- packages/arkenv/package.json | 12 +- packages/arkenv/src/adapters/arktype.ts | 68 +++++++ packages/arkenv/src/adapters/index.ts | 24 +++ packages/arkenv/src/adapters/standard.ts | 32 +++ packages/arkenv/src/create-env.ts | 116 +++++------ packages/arkenv/src/errors.ts | 47 ++--- packages/arkenv/src/index.ts | 7 +- packages/arkenv/src/type.ts | 20 +- packages/arkenv/src/utils/coerce.test.ts | 48 ++--- packages/arkenv/src/utils/coerce.ts | 236 +++++++++------------- packages/arkenv/src/zod.test.ts | 33 +++ packages/internal/types/package.json | 1 + packages/internal/types/src/infer-type.ts | 23 ++- packages/internal/types/src/schema.ts | 8 +- pnpm-lock.yaml | 15 ++ 16 files changed, 412 insertions(+), 282 deletions(-) create mode 100644 packages/arkenv/src/adapters/arktype.ts create mode 100644 packages/arkenv/src/adapters/index.ts create mode 100644 packages/arkenv/src/adapters/standard.ts create mode 100644 packages/arkenv/src/zod.test.ts diff --git a/package.json b/package.json index b165471c8..f3aaad617 100644 --- a/package.json +++ b/package.json @@ -34,13 +34,15 @@ "@changesets/cli": "2.29.8", "@manypkg/cli": "0.25.1", "@playwright/test": "", + "@standard-schema/spec": "^1.1.0", "@vitest/ui": "catalog:", "all-contributors-cli": "^6.26.1", "changesets-changelog-clean": "1.3.0", "rimraf": "catalog:", "turbo": "2.7.2", "typescript": "catalog:", - "vitest": "catalog:" + "vitest": "catalog:", + "zod": "catalog:" }, "packageManager": "pnpm@10.27.0", "pnpm": { diff --git a/packages/arkenv/package.json b/packages/arkenv/package.json index dd6937046..79230f5b8 100644 --- a/packages/arkenv/package.json +++ b/packages/arkenv/package.json @@ -45,24 +45,32 @@ "@repo/types": "workspace:*", "@size-limit/esbuild-why": "catalog:", "@size-limit/preset-small-lib": "catalog:", + "@standard-schema/spec": "^1.1.0", "@types/node": "catalog:", "arktype": "catalog:", "rimraf": "catalog:", "size-limit": "catalog:", "tsdown": "catalog:", "typescript": "catalog:", - "vitest": "catalog:" + "vitest": "catalog:", + "zod": "catalog:" }, "peerDependencies": { "arktype": "^2.1.22" }, + "peerDependenciesMeta": { + "arktype": { + "optional": true + } + }, "size-limit": [ { "path": "dist/index.js", "limit": "2 kB", "import": "*", "ignore": [ - "arktype" + "arktype", + "@repo/scope" ] } ] diff --git a/packages/arkenv/src/adapters/arktype.ts b/packages/arkenv/src/adapters/arktype.ts new file mode 100644 index 000000000..ebe2f62c6 --- /dev/null +++ b/packages/arkenv/src/adapters/arktype.ts @@ -0,0 +1,68 @@ +import { createRequire } from "node:module"; +import type { EnvIssue, SchemaAdapter } from "./index"; +import { coerce } from "../utils/coerce"; + +const require = createRequire(import.meta.url); + +export class ArkTypeAdapter implements SchemaAdapter { + readonly kind = "arktype"; + + constructor( + private schemaDef: unknown, + private config: { + coerce?: boolean; + onUndeclaredKey?: "ignore" | "delete" | "reject"; + arrayFormat?: "comma" | "json" | undefined; + } = {}, + ) {} + + validate(env: Record) { + try { + const { $ } = require("@repo/scope"); + const { type } = require("arktype"); + + const isCompiledType = + typeof this.schemaDef === "function" && + "assert" in (this.schemaDef as any); + + let schema = isCompiledType + ? (this.schemaDef as any) + : $.type(this.schemaDef); + + // Apply the `onUndeclaredKey` option, defaulting to "delete" for arkenv compatibility + schema = schema.onUndeclaredKey(this.config.onUndeclaredKey ?? "delete"); + + // Apply coercion transformation + if (this.config.coerce !== false) { + schema = coerce(type, schema, { + arrayFormat: this.config.arrayFormat as any, + }); + } + + const result = schema(env); + + if (result instanceof type.errors) { + return { + success: false, + issues: Object.entries(result.byPath).map(([path, error]) => ({ + path: path ? path.split(".") : [], + message: (error as any).message, + validator: "arktype" as const, + })), + } as const; + } + + return { + success: true, + value: result, + } as const; + } catch (e: any) { + if (e.code === "MODULE_NOT_FOUND") { + throw new Error( + "ArkType is required for this schema type. Please install 'arktype' or use a Standard Schema validator like Zod.", + ); + } + throw e; + } + } +} diff --git a/packages/arkenv/src/adapters/index.ts b/packages/arkenv/src/adapters/index.ts new file mode 100644 index 000000000..277f46ebf --- /dev/null +++ b/packages/arkenv/src/adapters/index.ts @@ -0,0 +1,24 @@ +/** + * Normalised issue format for ArkEnv + */ +export type EnvIssue = { + path: string[]; + message: string; + validator: "arktype" | "standard"; +}; + +/** + * Universal adapter for validator logic + */ +export interface SchemaAdapter { + readonly kind: "arktype" | "standard"; + validate(env: Record): + | { + success: true; + value: unknown; + } + | { + success: false; + issues: EnvIssue[]; + }; +} diff --git a/packages/arkenv/src/adapters/standard.ts b/packages/arkenv/src/adapters/standard.ts new file mode 100644 index 000000000..836ead29c --- /dev/null +++ b/packages/arkenv/src/adapters/standard.ts @@ -0,0 +1,32 @@ +import type { StandardSchemaV1 } from "@standard-schema/spec"; +import type { EnvIssue, SchemaAdapter } from "./index"; + +export class StandardSchemaAdapter implements SchemaAdapter { + readonly kind = "standard"; + + constructor(private schema: StandardSchemaV1) {} + + validate(env: Record) { + const result = this.schema["~standard"].validate(env); + + if (result instanceof Promise) { + throw new Error("ArkEnv does not support asynchronous validation."); + } + + if (result.issues) { + return { + success: false, + issues: result.issues.map((issue) => ({ + path: (issue.path as string[]) ?? [], + message: issue.message, + validator: "standard" as const, + })), + } as const; + } + + return { + success: true, + value: result.value, + } as const; + } +} diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 8f2259604..dc9c026dc 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -1,15 +1,15 @@ -import { $ } from "@repo/scope"; import type { EnvSchemaWithType, InferType, SchemaShape } from "@repo/types"; import type { type as at, distill } from "arktype"; +import { ArkTypeAdapter } from "./adapters/arktype"; +import { StandardSchemaAdapter } from "./adapters/standard"; import { ArkEnvError } from "./errors"; -import { type } from "./type"; -import { type CoerceOptions, coerce } from "./utils"; +import type { CoerceOptions } from "./utils"; -export type EnvSchema = at.validate; +export type EnvSchema = at.validate; type RuntimeEnvironment = Record; /** - * Configuration options for `createEnv` + * Configuration options for ArkEnv */ export type ArkEnvConfig = { /** @@ -17,89 +17,75 @@ export type ArkEnvConfig = { */ env?: RuntimeEnvironment; /** - * Whether to coerce environment variables to their defined types. Defaults to `true` + * Whether to coerce environment variables to their defined types. + * Only supported for ArkType schemas. + * @default true */ coerce?: boolean; /** - * Control how ArkEnv handles environment variables that are not defined in your schema. - * - * Defaults to `'delete'` to ensure your output object only contains - * keys you've explicitly declared. This differs from ArkType's standard behavior, which - * mirrors TypeScript by defaulting to `'ignore'`. - * - * - `delete` (ArkEnv default): Undeclared keys are allowed on input but stripped from the output. - * - `ignore` (ArkType default): Undeclared keys are allowed and preserved in the output. - * - `reject`: Undeclared keys will cause validation to fail. - * + * Control how ArkEnv handles undeclared keys. + * Only supported for ArkType schemas. * @default "delete" - * @see https://arktype.io/docs/configuration#onundeclaredkey */ onUndeclaredKey?: "ignore" | "delete" | "reject"; - /** - * The format to use for array parsing when coercion is enabled. - * - * - `comma` (default): Strings are split by comma and trimmed. - * - `json`: Strings are parsed as JSON. - * + * The format to use for array parsing in ArkType schemas. * @default "comma" */ arrayFormat?: CoerceOptions["arrayFormat"]; }; /** - * TODO: `SchemaShape` is basically `Record`. - * If possible, find a better type than "const T extends Record", - * and be as close as possible to the type accepted by ArkType's `type`. + * Pure Standard Schema entry point. + * Zero implicit coercion, zero ArkType dependency (unless passed an ArkType schema). */ +export function defineEnv( + schema: T, + config: ArkEnvConfig = {}, +): InferType { + const env = config.env ?? process.env; + const isStandard = !!(schema as any)?.["~standard"]; + const isArkCompiled = + typeof schema === "function" && "assert" in (schema as any); + + const adapter = + isStandard && !isArkCompiled + ? new StandardSchemaAdapter(schema as any) + : new ArkTypeAdapter(schema, config as any); + + const result = adapter.validate(env); + + if (!result.success) { + throw new ArkEnvError(result.issues); + } + + return result.value as InferType; +} /** - * Create an environment variables object from a schema and an environment - * @param def - The environment variable schema (raw object or type definition created with `type()`) - * @param config - Configuration options, see {@link ArkEnvConfig} - * @returns The validated environment variable schema - * @throws An {@link ArkEnvError | error} if the environment variables are invalid. + * Ergonomic entry point. Alias for `createEnv`. + * Supports ArkType raw objects, enabled if ArkType is present. */ -export function createEnv( +export function arkenv( def: EnvSchema, config?: ArkEnvConfig, -): distill.Out>; -export function createEnv( +): distill.Out>; +export function arkenv( def: T, config?: ArkEnvConfig, ): InferType; -export function createEnv( +export function arkenv( def: EnvSchema | EnvSchemaWithType, config?: ArkEnvConfig, -): distill.Out> | InferType; -export function createEnv( +): distill.Out> | InferType; +export function arkenv( def: EnvSchema | EnvSchemaWithType, - { - env = process.env, - coerce: shouldCoerce = true, - onUndeclaredKey = "delete", - arrayFormat = "comma", - }: ArkEnvConfig = {}, -): distill.Out> | InferType { - // If def is a type definition (has assert method), use it directly - // Otherwise, use raw() to convert the schema definition - const isCompiledType = typeof def === "function" && "assert" in def; - let schema = isCompiledType ? def : $.type.raw(def as EnvSchema); - - // Apply the `onUndeclaredKey` option - schema = schema.onUndeclaredKey(onUndeclaredKey); - - // Apply coercion transformation to allow strings to be parsed as numbers/booleans - if (shouldCoerce) { - schema = coerce(schema, { arrayFormat }); - } - - // Validate the environment variables - const validatedEnv = schema(env); - - if (validatedEnv instanceof type.errors) { - throw new ArkEnvError(validatedEnv); - } - - return validatedEnv; + config: ArkEnvConfig = {}, +): distill.Out> | InferType { + return defineEnv(def as any, config) as any; } + +/** + * @deprecated Use `arkenv` or `defineEnv` instead. + */ +export const createEnv = arkenv; diff --git a/packages/arkenv/src/errors.ts b/packages/arkenv/src/errors.ts index e8101b2e0..f6bb22065 100644 --- a/packages/arkenv/src/errors.ts +++ b/packages/arkenv/src/errors.ts @@ -1,39 +1,26 @@ -import type { ArkErrors } from "arktype"; -import { indent, styleText } from "./utils"; - -/** - * Format the errors returned by ArkType to be more readable - * @param errors - The errors returned by ArkType - * @returns A string of the formatted errors - */ -export const formatErrors = (errors: ArkErrors): string => - Object.entries(errors.byPath) - .map(([path, error]) => { - const messageWithoutPath = error.message.startsWith(path) - ? error.message.slice(path.length) - : error.message; - - // Extract the value in parentheses if it exists - const valueMatch = messageWithoutPath.match(/\(was "([^"]+)"\)/); - const formattedMessage = valueMatch - ? messageWithoutPath.replace( - `(was "${valueMatch[1]}")`, - `(was ${styleText("cyan", `"${valueMatch[1]}"`)})`, - ) - : messageWithoutPath; - - return `${styleText("yellow", path)} ${formattedMessage.trimStart()}`; - }) - .join("\n"); +import type { EnvIssue } from "./adapters"; +import { indent } from "./utils/indent"; +import { styleText } from "./utils/style-text"; export class ArkEnvError extends Error { constructor( - errors: ArkErrors, + public issues: EnvIssue[], message = "Errors found while validating environment variables", ) { - super(`${styleText("red", message)}\n${indent(formatErrors(errors))}\n`); + super(`${styleText("red", message)}\n${indent(formatIssues(issues))}\n`); this.name = "ArkEnvError"; } } -Object.defineProperty(ArkEnvError, "name", { value: "ArkEnvError" }); +function formatIssues(issues: EnvIssue[]): string { + return issues + .map((issue) => { + const path = issue.path.length > 0 ? issue.path.join(".") : "root"; + let message = issue.message; + if (message.startsWith(`${path} `)) { + message = message.slice(path.length + 1); + } + return `${styleText("yellow", path)} ${message}`; + }) + .join("\n"); +} diff --git a/packages/arkenv/src/index.ts b/packages/arkenv/src/index.ts index 3fc5871a0..2f9164484 100644 --- a/packages/arkenv/src/index.ts +++ b/packages/arkenv/src/index.ts @@ -1,14 +1,13 @@ -export type { EnvSchema } from "./create-env"; +export type { EnvSchema, ArkEnvConfig } from "./create-env"; -import { createEnv } from "./create-env"; +import { arkenv } from "./create-env"; /** * `arkenv`'s main export, an alias for {@link createEnv} * * {@link https://arkenv.js.org | ArkEnv} is a typesafe environment variables validator from editor to runtime. */ -const arkenv = createEnv; export default arkenv; export { type } from "./type"; -export { createEnv }; +export { arkenv, createEnv, defineEnv } from "./create-env"; export { ArkEnvError } from "./errors"; diff --git a/packages/arkenv/src/type.ts b/packages/arkenv/src/type.ts index 2410781c8..4b13e496b 100644 --- a/packages/arkenv/src/type.ts +++ b/packages/arkenv/src/type.ts @@ -1,3 +1,19 @@ -import { $ } from "@repo/scope"; +import { createRequire } from "node:module"; +const require = createRequire(import.meta.url); -export const type = $.type; +import type { type as at } from "arktype"; + +/** + * `type` is a typesafe environment variable validator, an alias for `arktype`'s `type`. + */ +export const type: typeof at = new Proxy((() => {}) as unknown as typeof at, { + get(_, prop) { + const { $ } = require("@repo/scope"); + return Reflect.get($.type, prop); + }, + apply(_, thisArg, args) { + const { $ } = require("@repo/scope"); + return Reflect.apply($.type, thisArg === type ? $.type : thisArg, args); + }, +}); +export type { type as at } from "arktype"; diff --git a/packages/arkenv/src/utils/coerce.test.ts b/packages/arkenv/src/utils/coerce.test.ts index 0c989698d..565658bc2 100644 --- a/packages/arkenv/src/utils/coerce.test.ts +++ b/packages/arkenv/src/utils/coerce.test.ts @@ -7,7 +7,7 @@ describe("coerce", () => { const schema = type({ PORT: "number", }); - const coercedSchema = coerce(schema); + const coercedSchema = coerce(type, schema); const result = coercedSchema({ PORT: "3000" }); expect(result).toEqual({ PORT: 3000 }); }); @@ -16,7 +16,7 @@ describe("coerce", () => { const schema = type({ AGE: "number >= 18", }); - const coercedSchema = coerce(schema); + const coercedSchema = coerce(type, schema); const result = coercedSchema({ AGE: "21" }); expect(result).toEqual({ AGE: 21 }); @@ -29,7 +29,7 @@ describe("coerce", () => { const schema = type({ EVEN: "number % 2", }); - const coercedSchema = coerce(schema); + const coercedSchema = coerce(type, schema); const result = coercedSchema({ EVEN: "4" }); expect(result).toEqual({ EVEN: 4 }); @@ -42,7 +42,7 @@ describe("coerce", () => { const schema = type({ DEBUG: "boolean", }); - const coercedSchema = coerce(schema); + const coercedSchema = coerce(type, schema); expect(coercedSchema({ DEBUG: "true" })).toEqual({ DEBUG: true }); expect(coercedSchema({ DEBUG: "false" })).toEqual({ DEBUG: false }); @@ -56,14 +56,14 @@ describe("coerce", () => { const schema = type({ "PORT?": "number", }); - const coercedSchema = coerce(schema); + const coercedSchema = coerce(type, schema); expect(coercedSchema({ PORT: "3000" })).toEqual({ PORT: 3000 }); expect(coercedSchema({})).toEqual({}); }); it("should work with root-level primitives", () => { const schema = type("number >= 10"); - const coercedSchema = coerce(schema); + const coercedSchema = coerce(type, schema); expect(coercedSchema("20")).toBe(20); const failure = coercedSchema("5"); expect(failure).toBeInstanceOf(ArkErrors); @@ -72,7 +72,7 @@ describe("coerce", () => { it("should work with strict number literals", () => { const schema = type("1 | 2"); - const coercedSchema = coerce(schema); + const coercedSchema = coerce(type, schema); expect(coercedSchema("1")).toBe(1); expect(coercedSchema("2")).toBe(2); @@ -81,7 +81,7 @@ describe("coerce", () => { it("should coerce numeric values in mixed unions", () => { const schema = type("1 | 'a'"); - const coercedSchema = coerce(schema); + const coercedSchema = coerce(type, schema); expect(coercedSchema("1")).toBe(1); expect(coercedSchema("a")).toBe("a"); @@ -89,7 +89,7 @@ describe("coerce", () => { it("should coerce mixed numeric and boolean unions", () => { const schema = type("number | boolean"); - const coercedSchema = coerce(schema); + const coercedSchema = coerce(type, schema); expect(coercedSchema("123")).toBe(123); expect(coercedSchema("true")).toBe(true); @@ -104,7 +104,7 @@ describe("coerce", () => { const schema = type({ VAL: "number | boolean", }); - const coercedSchema = coerce(schema); + const coercedSchema = coerce(type, schema); expect(coercedSchema({ VAL: "123" })).toEqual({ VAL: 123 }); expect(coercedSchema({ VAL: "true" })).toEqual({ VAL: true }); @@ -119,7 +119,7 @@ describe("coerce", () => { const schema = type({ PORT: "number", }); - const coercedSchema = coerce(schema); + const coercedSchema = coerce(type, schema); const emptyResult = coercedSchema({ PORT: "" }); expect(emptyResult).toBeInstanceOf(ArkErrors); @@ -138,7 +138,7 @@ describe("coerce", () => { const schema = type({ PORT: "number", }); - const coercedSchema = coerce(schema); + const coercedSchema = coerce(type, schema); const result = coercedSchema({ PORT: "abc" }); expect(result).toBeInstanceOf(ArkErrors); @@ -149,7 +149,7 @@ describe("coerce", () => { const schema = type({ DEBUG: "boolean", }); - const coercedSchema = coerce(schema); + const coercedSchema = coerce(type, schema); const result = coercedSchema({ DEBUG: "yes" }); expect(result).toBeInstanceOf(ArkErrors); @@ -163,7 +163,7 @@ describe("coerce", () => { port: "number", }, }); - const coercedSchema = coerce(schema); + const coercedSchema = coerce(type, schema); const result = coercedSchema({ CONFIG: '{"host": "localhost", "port": "3000"}', @@ -185,7 +185,7 @@ describe("coerce", () => { }, }, }); - const coercedSchema = coerce(schema); + const coercedSchema = coerce(type, schema); const result = coercedSchema({ CONFIG: '{"database": {"host": "localhost", "port": "5432"}}', @@ -207,7 +207,7 @@ describe("coerce", () => { debug: "boolean", }, }); - const coercedSchema = coerce(schema); + const coercedSchema = coerce(type, schema); const result = coercedSchema({ SETTINGS: '{"enabled": "true", "debug": "false"}', @@ -226,7 +226,7 @@ describe("coerce", () => { host: "string", }, }); - const coercedSchema = coerce(schema); + const coercedSchema = coerce(type, schema); const result = coercedSchema({ CONFIG: "not valid json" }); expect(result).toBeInstanceOf(ArkErrors); @@ -239,7 +239,7 @@ describe("coerce", () => { host: "string", }, }); - const coercedSchema = coerce(schema); + const coercedSchema = coerce(type, schema); const result = coercedSchema({ PORT: "3000", @@ -260,7 +260,7 @@ describe("coerce", () => { SSL: "boolean", }, }); - const coercedSchema = coerce(schema); + const coercedSchema = coerce(type, schema); const result = coercedSchema({ DB: { @@ -283,7 +283,7 @@ describe("coerce", () => { Number.parseInt(str, 10), ), }); - const coercedSchema = coerce(schema); + const coercedSchema = coerce(type, schema); const result = coercedSchema({ PORT: "3000", VITE_MY_NUMBER_MANUAL: "456", @@ -298,7 +298,7 @@ describe("coerce", () => { const schema = type({ VAL: "number.NaN", }); - const coercedSchema = coerce(schema); + const coercedSchema = coerce(type, schema); const result = coercedSchema({ VAL: "NaN" }); expect(result).not.toBeInstanceOf(ArkErrors); if (result instanceof ArkErrors) return; @@ -310,7 +310,7 @@ describe("coerce", () => { VAL: "number", }); // Coercion happens ("NaN" -> NaN), but "number" checks and rejects NaN - const coercedSchema = coerce(schema); + const coercedSchema = coerce(type, schema); const result = coercedSchema({ VAL: "NaN" }); expect(result).toBeInstanceOf(ArkErrors); expect(result.toString()).toContain("VAL must be a number (was NaN)"); @@ -320,7 +320,7 @@ describe("coerce", () => { // Coerces array elements ["1", "2", "3"] to [1, 2, 3] IDS: "number[]", }); - const coercedSchema = coerce(schema); + const coercedSchema = coerce(type, schema); const result = coercedSchema({ IDS: ["1", "2", "3"] }); expect(result).toEqual({ IDS: [1, 2, 3] }); @@ -330,7 +330,7 @@ describe("coerce", () => { const schema = type({ VAL: "number", }); - const coercedSchema = coerce(schema); + const coercedSchema = coerce(type, schema); // Logic coerces ["1", "2"] -> [1, 2] in place // Then validation sees [1, 2] against "number" and fails diff --git a/packages/arkenv/src/utils/coerce.ts b/packages/arkenv/src/utils/coerce.ts index 214f8299c..2bba6cf01 100644 --- a/packages/arkenv/src/utils/coerce.ts +++ b/packages/arkenv/src/utils/coerce.ts @@ -1,5 +1,38 @@ -import { maybeBoolean, maybeJson, maybeNumber } from "@repo/keywords"; -import { type BaseType, type JsonSchema, type } from "arktype"; +/** + * A loose numeric morph. + */ +const maybeNumber = (s: unknown) => { + if (typeof s === "number") return s; + if (typeof s !== "string") return s; + const trimmed = s.trim(); + if (trimmed === "") return s; + if (trimmed === "NaN") return Number.NaN; + const n = Number(trimmed); + return Number.isNaN(n) ? s : n; +}; + +/** + * A loose boolean morph. + */ +const maybeBoolean = (s: unknown) => { + if (s === "true") return true; + if (s === "false") return false; + return s; +}; + +/** + * A loose JSON morph. + */ +const maybeJson = (s: unknown) => { + if (typeof s !== "string") return s; + const trimmed = s.trim(); + if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return s; + try { + return JSON.parse(trimmed); + } catch { + return s; + } +}; /** * A marker used in the coercion path to indicate that the target @@ -16,30 +49,15 @@ type CoercionTarget = { type: "primitive" | "array" | "object"; }; -/** - * Options for coercion behavior. - */ -export type CoerceOptions = { - /** - * format to use for array parsing - * @default "comma" - */ - arrayFormat?: "comma" | "json"; -}; - /** * Recursively find all paths in a JSON Schema that require coercion. - * We prioritize "number", "integer", "boolean", "array", and "object" types. */ const findCoercionPaths = ( - node: JsonSchema, + node: any, // JsonSchema path: string[] = [], ): CoercionTarget[] => { const results: CoercionTarget[] = []; - - if (typeof node === "boolean") { - return results; - } + if (typeof node === "boolean") return results; if ("const" in node) { if (typeof node.const === "number" || typeof node.const === "boolean") { @@ -49,7 +67,9 @@ const findCoercionPaths = ( if ("enum" in node && node.enum) { if ( - node.enum.some((v) => typeof v === "number" || typeof v === "boolean") + node.enum.some( + (v: any) => typeof v === "number" || typeof v === "boolean", + ) ) { results.push({ path: [...path], type: "primitive" }); } @@ -61,43 +81,28 @@ const findCoercionPaths = ( } else if (node.type === "boolean") { results.push({ path: [...path], type: "primitive" }); } else if (node.type === "object") { - // Check if this object has properties defined - // If it does, we want to coerce the whole object from a JSON string - // But we also want to recursively check nested properties const hasProperties = "properties" in node && node.properties && Object.keys(node.properties).length > 0; - - if (hasProperties) { - // Mark this path as needing object coercion (JSON parsing) - results.push({ path: [...path], type: "object" }); - } - - // Also recursively check nested properties for their own coercions + if (hasProperties) results.push({ path: [...path], type: "object" }); if ("properties" in node && node.properties) { for (const [key, prop] of Object.entries(node.properties)) { - results.push( - ...findCoercionPaths(prop as JsonSchema, [...path, key]), - ); + results.push(...findCoercionPaths(prop as any, [...path, key])); } } } else if (node.type === "array") { - // Mark the array itself as a target for splitting strings results.push({ path: [...path], type: "array" }); - if ("items" in node && node.items) { if (Array.isArray(node.items)) { - // Tuple traversal - node.items.forEach((item, index) => { + node.items.forEach((item: any, index: number) => { results.push( - ...findCoercionPaths(item as JsonSchema, [...path, `${index}`]), + ...findCoercionPaths(item as any, [...path, `${index}`]), ); }); } else { - // List traversal results.push( - ...findCoercionPaths(node.items as JsonSchema, [ + ...findCoercionPaths(node.items as any, [ ...path, ARRAY_ITEM_MARKER, ]), @@ -108,24 +113,18 @@ const findCoercionPaths = ( } if ("anyOf" in node && node.anyOf) { - for (const branch of node.anyOf) { - results.push(...findCoercionPaths(branch as JsonSchema, path)); - } + for (const branch of node.anyOf) + results.push(...findCoercionPaths(branch as any, path)); } - if ("allOf" in node && node.allOf) { - for (const branch of node.allOf) { - results.push(...findCoercionPaths(branch as JsonSchema, path)); - } + for (const branch of node.allOf) + results.push(...findCoercionPaths(branch as any, path)); } - if ("oneOf" in node && node.oneOf) { - for (const branch of node.oneOf) { - results.push(...findCoercionPaths(branch as JsonSchema, path)); - } + for (const branch of node.oneOf) + results.push(...findCoercionPaths(branch as any, path)); } - // Deduplicate by path and type combination const seen = new Set(); return results.filter((t) => { const key = JSON.stringify(t.path) + t.type; @@ -135,6 +134,17 @@ const findCoercionPaths = ( }); }; +/** + * Options for coercion behavior. + */ +export type CoerceOptions = { + /** + * format to use for array parsing + * @default "comma" + */ + arrayFormat?: "comma" | "json"; +}; + /** * Apply coercion to a data object based on identified paths. */ @@ -144,8 +154,6 @@ const applyCoercion = ( options: CoerceOptions = {}, ) => { const { arrayFormat = "comma" } = options; - - // Helper to split string to array const splitString = (val: string) => { if (arrayFormat === "json") { try { @@ -154,34 +162,24 @@ const applyCoercion = ( return val; } } - if (!val.trim()) return []; return val.split(",").map((s) => s.trim()); }; if (typeof data !== "object" || data === null) { - // If root data needs coercion if (targets.some((t) => t.path.length === 0)) { const rootTarget = targets.find((t) => t.path.length === 0); - - if (rootTarget?.type === "object" && typeof data === "string") { + if (rootTarget?.type === "object" && typeof data === "string") return maybeJson(data); - } - - if (rootTarget?.type === "array" && typeof data === "string") { + if (rootTarget?.type === "array" && typeof data === "string") return splitString(data); - } - const asNumber = maybeNumber(data); - if (typeof asNumber === "number") { - return asNumber; - } + if (typeof asNumber === "number") return asNumber; return maybeBoolean(data); } return data; } - // Sort targets by path length to ensure parent objects/arrays are coerced before their children const sortedTargets = [...targets].sort( (a, b) => a.path.length - b.path.length, ); @@ -192,26 +190,20 @@ const applyCoercion = ( type: "primitive" | "array" | "object", ) => { if (!current || typeof current !== "object") return; + if (targetPath.length === 0) return; - if (targetPath.length === 0) { - return; - } - - // If we've reached the last key, apply coercion if (targetPath.length === 1) { const lastKey = targetPath[0]; - if (lastKey === ARRAY_ITEM_MARKER) { if (Array.isArray(current)) { for (let i = 0; i < current.length; i++) { const original = current[i]; if (type === "primitive") { const asNumber = maybeNumber(original); - if (typeof asNumber === "number") { - current[i] = asNumber; - } else { - current[i] = maybeBoolean(original); - } + current[i] = + typeof asNumber === "number" + ? asNumber + : maybeBoolean(original); } else if (type === "object") { current[i] = maybeJson(original); } @@ -221,99 +213,59 @@ const applyCoercion = ( } const record = current as Record; - // biome-ignore lint/suspicious/noPrototypeBuiltins: ES2020 compatibility + // biome-ignore lint/suspicious/noPrototypeBuiltins: preserve existing logic if (Object.prototype.hasOwnProperty.call(record, lastKey)) { const original = record[lastKey]; - if (type === "array" && typeof original === "string") { record[lastKey] = splitString(original); - return; - } - - if (type === "object" && typeof original === "string") { + } else if (type === "object" && typeof original === "string") { record[lastKey] = maybeJson(original); - return; - } - - if (Array.isArray(original)) { + } else if (Array.isArray(original)) { if (type === "primitive") { for (let i = 0; i < original.length; i++) { - const item = original[i]; - const asNumber = maybeNumber(item); - if (typeof asNumber === "number") { - original[i] = asNumber; - } else { - original[i] = maybeBoolean(item); - } - } - } - } else { - if (type === "primitive") { - const asNumber = maybeNumber(original); - // If numeric parsing didn't produce a number, try boolean coercion - if (typeof asNumber === "number") { - record[lastKey] = asNumber; - } else { - record[lastKey] = maybeBoolean(original); + const asNumber = maybeNumber(original[i]); + original[i] = + typeof asNumber === "number" + ? asNumber + : maybeBoolean(original[i]); } } + } else if (type === "primitive") { + const asNumber = maybeNumber(original); + record[lastKey] = + typeof asNumber === "number" ? asNumber : maybeBoolean(original); } } return; } - // Recurse down const [nextKey, ...rest] = targetPath; - if (nextKey === ARRAY_ITEM_MARKER) { if (Array.isArray(current)) { - for (const item of current) { - walk(item, rest, type); - } + for (const item of current) walk(item, rest, type); } - return; + } else { + walk((current as Record)[nextKey], rest, type); } - - const record = current as Record; - walk(record[nextKey], rest, type); }; - for (const target of sortedTargets) { - walk(data, target.path, target.type); - } - + for (const target of sortedTargets) walk(data, target.path, target.type); return data; }; /** * Create a coercing wrapper around an ArkType schema using JSON Schema introspection. - * Pre-process input data to coerce string values to numbers/booleans at identified paths - * before validation. */ -export function coerce( - schema: BaseType, +export function coerce( + type: any, // Use any for type utility to avoid top-level import + schema: any, // Use any for BaseType to avoid top-level import options?: CoerceOptions, -): BaseType { - // Use a fallback to handle unjsonifiable parts of the schema (like predicates) - // by preserving the base schema. This ensures that even if part of the schema - // cannot be fully represented in JSON Schema, we can still perform coercion - // for the parts that can. - const json = schema.in.toJsonSchema({ - fallback: (ctx) => ctx.base, - }); +): any { + const json = schema.in.toJsonSchema({ fallback: (ctx: any) => ctx.base }); const targets = findCoercionPaths(json); + if (targets.length === 0) return schema; - if (targets.length === 0) { - return schema; - } - - /* - * We use `type("unknown")` to start the pipeline, which initializes a default scope. - * Integrating the original `schema` with its custom scope `$` into this pipeline - * creates a scope mismatch in TypeScript ({} vs $). - * We cast to `BaseType` to assert the final contract is maintained. - */ return type("unknown") - .pipe((data) => applyCoercion(data, targets, options)) - .pipe(schema) as BaseType; + .pipe((data: unknown) => applyCoercion(data, targets, options)) + .pipe(schema); } diff --git a/packages/arkenv/src/zod.test.ts b/packages/arkenv/src/zod.test.ts new file mode 100644 index 000000000..01dc95866 --- /dev/null +++ b/packages/arkenv/src/zod.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; +import { z } from "zod"; +import arkenv from "./index"; + +describe("zod integration", () => { + it("should work with zod schemas", () => { + const schema = z.object({ + PORT: z.coerce.number(), + HOST: z.string().default("localhost"), + }); + + const env = arkenv(schema, { + env: { PORT: "3000" }, + }); + + expect(env).toEqual({ + PORT: 3000, + HOST: "localhost", + }); + }); + + it("should throw ArkEnvError on validation failure", () => { + const schema = z.object({ + PORT: z.number(), + }); + + expect(() => + arkenv(schema, { + env: { PORT: "not-a-number" }, + }), + ).toThrow("PORT"); + }); +}); diff --git a/packages/internal/types/package.json b/packages/internal/types/package.json index bce1ae3cc..652e2097f 100644 --- a/packages/internal/types/package.json +++ b/packages/internal/types/package.json @@ -18,6 +18,7 @@ }, "devDependencies": { "@repo/scope": "workspace:*", + "@standard-schema/spec": "^1.1.0", "arktype": "catalog:", "typescript": "catalog:" }, diff --git a/packages/internal/types/src/infer-type.ts b/packages/internal/types/src/infer-type.ts index d07009e68..780437c83 100644 --- a/packages/internal/types/src/infer-type.ts +++ b/packages/internal/types/src/infer-type.ts @@ -1,17 +1,18 @@ +import type { StandardSchemaV1 } from "@standard-schema/spec"; import type { type } from "arktype"; /** - * Extract the inferred type from an ArkType type definition by checking its call signature. - * When a type definition is called, it returns either the validated value or type.errors. + * Extract the inferred type from an ArkType type definition or a Standard Schema validator. * - * @template T - The ArkType type definition to infer from + * @template T - The schema definition to infer from */ -export type InferType = T extends ( - value: Record, -) => infer R - ? R extends type.errors - ? never - : R - : T extends type.Any +export type InferType = + T extends StandardSchemaV1 ? U - : never; + : T extends (value: Record) => infer R + ? R extends type.errors + ? never + : R + : T extends type.Any + ? U + : never; diff --git a/packages/internal/types/src/schema.ts b/packages/internal/types/src/schema.ts index 8a78603ee..a969e785b 100644 --- a/packages/internal/types/src/schema.ts +++ b/packages/internal/types/src/schema.ts @@ -1,7 +1,13 @@ import type { $ } from "@repo/scope"; +import type { StandardSchemaV1 } from "@standard-schema/spec"; import { type Type, type } from "arktype"; export const SchemaShape = type({ "[string]": "unknown" }); export type SchemaShape = typeof SchemaShape.infer; -export type EnvSchemaWithType = Type; +/** + * A schema definition that is either an ArkType Type or a Standard Schema validator. + */ +export type EnvSchemaWithType = + | Type + | StandardSchemaV1; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff177160b..0c56fff43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,6 +100,9 @@ importers: '@playwright/test': specifier: 1.57.0 version: 1.57.0 + '@standard-schema/spec': + specifier: ^1.1.0 + version: 1.1.0 '@vitest/ui': specifier: 'catalog:' version: 4.0.16(vitest@4.0.16) @@ -121,6 +124,9 @@ importers: vitest: specifier: 'catalog:' version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) + zod: + specifier: 'catalog:' + version: 4.2.1 apps/playgrounds/bun: dependencies: @@ -548,6 +554,9 @@ importers: '@size-limit/preset-small-lib': specifier: 'catalog:' version: 12.0.0(size-limit@12.0.0(jiti@2.6.1)) + '@standard-schema/spec': + specifier: ^1.1.0 + version: 1.1.0 '@types/node': specifier: 'catalog:' version: 24.10.4 @@ -569,6 +578,9 @@ importers: vitest: specifier: 'catalog:' version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) + zod: + specifier: 'catalog:' + version: 4.2.1 packages/bun-plugin: dependencies: @@ -643,6 +655,9 @@ importers: '@repo/scope': specifier: workspace:* version: link:../scope + '@standard-schema/spec': + specifier: ^1.1.0 + version: 1.1.0 arktype: specifier: 'catalog:' version: 2.1.29 From 33241e9051fe4af999191cde85a3d09820277a6d Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Tue, 6 Jan 2026 22:54:52 +0500 Subject: [PATCH 002/117] refactor: improve error handling and type import - Normalize ArkEnvError constructor input - Add public issues property to ArkEnvError - Deprecate and refactor formatErrors function --- packages/arkenv/src/errors.ts | 37 +++++++++++++++++++++++++++++++++-- packages/arkenv/src/type.ts | 6 ++++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/packages/arkenv/src/errors.ts b/packages/arkenv/src/errors.ts index f6bb22065..28db44aac 100644 --- a/packages/arkenv/src/errors.ts +++ b/packages/arkenv/src/errors.ts @@ -3,20 +3,53 @@ import { indent } from "./utils/indent"; import { styleText } from "./utils/style-text"; export class ArkEnvError extends Error { + public issues: EnvIssue[]; + constructor( - public issues: EnvIssue[], + issues: EnvIssue[] | any, message = "Errors found while validating environment variables", ) { - super(`${styleText("red", message)}\n${indent(formatIssues(issues))}\n`); + const normalizedIssues = Array.isArray(issues) + ? issues + : Object.entries((issues as any).byPath || {}).map( + ([path, error]: [string, any]) => ({ + path: path ? path.split(".") : [], + message: error.message, + validator: "arktype" as const, + }), + ); + + super( + `${styleText("red", message)}\n${indent(formatIssues(normalizedIssues))}\n`, + ); this.name = "ArkEnvError"; + this.issues = normalizedIssues; } } +/** + * @deprecated Use ArkEnvError.issues or normalized format. + * Maintained for backward compatibility with existing tests and users. + */ +export function formatErrors(errors: any): string { + const issues = Object.entries(errors.byPath || {}).map( + ([path, error]: [string, any]) => ({ + path: path ? path.split(".") : [], + message: error.message, + validator: "arktype" as const, + }), + ); + return formatIssues(issues); +} + function formatIssues(issues: EnvIssue[]): string { return issues .map((issue) => { const path = issue.path.length > 0 ? issue.path.join(".") : "root"; let message = issue.message; + // ArkType's error messages often already include the path. + // Example: "PORT must be a number". + // We want to avoid "PORT: PORT must be a number". if (message.startsWith(`${path} `)) { message = message.slice(path.length + 1); } diff --git a/packages/arkenv/src/type.ts b/packages/arkenv/src/type.ts index 4b13e496b..5728569a3 100644 --- a/packages/arkenv/src/type.ts +++ b/packages/arkenv/src/type.ts @@ -1,12 +1,13 @@ import { createRequire } from "node:module"; const require = createRequire(import.meta.url); -import type { type as at } from "arktype"; +import type { $ } from "@repo/scope"; /** * `type` is a typesafe environment variable validator, an alias for `arktype`'s `type`. + * It includes arkenv-specific keywords like `string.host` and `number.port`. */ -export const type: typeof at = new Proxy((() => {}) as unknown as typeof at, { +export const type: typeof $.type = new Proxy((() => {}) as any, { get(_, prop) { const { $ } = require("@repo/scope"); return Reflect.get($.type, prop); @@ -16,4 +17,5 @@ export const type: typeof at = new Proxy((() => {}) as unknown as typeof at, { return Reflect.apply($.type, thisArg === type ? $.type : thisArg, args); }, }); + export type { type as at } from "arktype"; From 5b4997544fa3a0618c79b9bf416fe74db869d92f Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Tue, 6 Jan 2026 23:05:26 +0500 Subject: [PATCH 003/117] refactor: Replace `any` with `$` from `@repo/scope` in `EnvSchema` and `arkenv` type definitions. --- packages/arkenv/src/adapters/index.ts | 24 ++++++-- .../arkenv/src/coercion.integration.test.ts | 10 ++-- packages/arkenv/src/create-env.test.ts | 44 +++++++++++--- packages/arkenv/src/create-env.ts | 43 ++++++++++--- .../src/object-parsing.integration.test.ts | 4 +- packages/arkenv/src/standard-schema.test.ts | 60 +++++++++++++++++++ packages/arkenv/src/zod.test.ts | 33 ---------- 7 files changed, 160 insertions(+), 58 deletions(-) create mode 100644 packages/arkenv/src/standard-schema.test.ts delete mode 100644 packages/arkenv/src/zod.test.ts diff --git a/packages/arkenv/src/adapters/index.ts b/packages/arkenv/src/adapters/index.ts index 277f46ebf..d6e84a847 100644 --- a/packages/arkenv/src/adapters/index.ts +++ b/packages/arkenv/src/adapters/index.ts @@ -1,17 +1,33 @@ /** - * Normalised issue format for ArkEnv + * Normalised issue format for ArkEnv. + * This shape is stable across all validators (ArkType, Standard Schema, etc.). + * Treat this as an invariant for future tooling, CLI formatting, or IDE integrations. */ export type EnvIssue = { + /** + * The path to the failing key as an array of strings. + * Empty array means root Level. + */ path: string[]; + /** + * The localized or validator-specific error message. + */ message: string; + /** + * The kind of validator that produced this error. + */ validator: "arktype" | "standard"; }; -/** - * Universal adapter for validator logic - */ export interface SchemaAdapter { + /** + * The internal identity of the adapter. + */ readonly kind: "arktype" | "standard"; + + /** + * Validate a record of environment variables. + */ validate(env: Record): | { success: true; diff --git a/packages/arkenv/src/coercion.integration.test.ts b/packages/arkenv/src/coercion.integration.test.ts index a2f0690fd..0b9e63c6d 100644 --- a/packages/arkenv/src/coercion.integration.test.ts +++ b/packages/arkenv/src/coercion.integration.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { createEnv } from "./create-env"; +import { createEnv, defineEnv } from "./create-env"; import { type } from "./index"; describe("coercion integration", () => { @@ -60,7 +60,7 @@ describe("coercion integration", () => { it("should coerce when using compiled type definitions", () => { const schema = type({ PORT: "number" }); - const env = createEnv(schema, { env: { PORT: "3000" } }); + const env = defineEnv(schema, { env: { PORT: "3000" } }); expect(env.PORT).toBe(3000); expect(typeof env.PORT).toBe("number"); }); @@ -70,14 +70,14 @@ describe("coercion integration", () => { PORT: "number.port", COUNT: "number.integer", }); - const env = createEnv(schema, { env: { PORT: "8080", COUNT: "123" } }); + const env = defineEnv(schema, { env: { PORT: "8080", COUNT: "123" } }); expect(env.PORT).toBe(8080); expect(env.COUNT).toBe(123); }); it("should fail compiled type validation if coercion fails", () => { const schema = type({ PORT: "number" }); - expect(() => createEnv(schema, { env: { PORT: "abc" } })).toThrow(); + expect(() => defineEnv(schema, { env: { PORT: "abc" } })).toThrow(); }); it("should work with other number sub-keywords like epoch", () => { @@ -132,7 +132,7 @@ describe("coercion integration", () => { ), }); - const env = createEnv(Env, { + const env = defineEnv(Env, { env: { PORT: "3000", VITE_MY_NUMBER_MANUAL: "456", diff --git a/packages/arkenv/src/create-env.test.ts b/packages/arkenv/src/create-env.test.ts index d6127dfb8..281c78d4f 100644 --- a/packages/arkenv/src/create-env.test.ts +++ b/packages/arkenv/src/create-env.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { createEnv } from "./create-env"; +import { arkenv, createEnv, defineEnv } from "./create-env"; +import { z } from "zod"; import { type } from "./type"; import { indent, styleText } from "./utils"; @@ -322,7 +323,7 @@ describe("createEnv", () => { TEST_PORT: "number.port", }); - const env = createEnv(Env); + const env = defineEnv(Env); expect(env.TEST_STRING).toBe("hello"); expect(env.TEST_PORT).toBe(3000); @@ -337,7 +338,7 @@ describe("createEnv", () => { TEST_PORT: "number.port", }); - const env = createEnv(Env); + const env = defineEnv(Env); // TypeScript should infer these correctly const str = env.TEST_STRING; @@ -355,12 +356,12 @@ describe("createEnv", () => { }); // Use the same schema multiple times - const env1 = createEnv(Env, { + const env1 = defineEnv(Env, { env: { TEST_STRING: "first", }, }); - const env2 = createEnv(Env, { + const env2 = defineEnv(Env, { env: { TEST_STRING: "second", }, @@ -377,7 +378,7 @@ describe("createEnv", () => { INVALID_PORT: "number.port", }); - expect(() => createEnv(Env)).toThrow(/INVALID_PORT/); + expect(() => defineEnv(Env)).toThrow(/INVALID_PORT/); }); it("should work with custom environment and type definitions", () => { @@ -391,7 +392,7 @@ describe("createEnv", () => { PORT: "8080", }; - const env = createEnv(Env, { env: customEnv }); + const env = defineEnv(Env, { env: customEnv }); expect(env.HOST).toBe("localhost"); expect(env.PORT).toBe(8080); @@ -628,4 +629,33 @@ describe("createEnv", () => { ]); }); }); + + describe("migration & hybrid support", () => { + it("should support mixed ArkType DSL and Standard Schema validators", () => { + const env = arkenv( + { + PORT: "number.port", + HOST: z.string().min(1), + }, + { + env: { PORT: "3000", HOST: "localhost" }, + }, + ); + + expect(env).toEqual({ PORT: 3000, HOST: "localhost" }); + }); + + it("should throw a clear error if arkenv() is passed a wrapped schema", () => { + expect(() => { + arkenv(z.object({ PORT: z.number() }) as any); + }).toThrow(/arkenv\(\) expects a mapping of { KEY: validator }/); + }); + + it("should work with top-level Standard Schema via defineEnv", () => { + const env = defineEnv(z.object({ PORT: z.coerce.number() }), { + env: { PORT: "8080" }, + }); + expect(env.PORT).toBe(8080); + }); + }); }); diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index dc9c026dc..b5173038a 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -1,11 +1,12 @@ import type { EnvSchemaWithType, InferType, SchemaShape } from "@repo/types"; import type { type as at, distill } from "arktype"; +import type { $ } from "@repo/scope"; import { ArkTypeAdapter } from "./adapters/arktype"; import { StandardSchemaAdapter } from "./adapters/standard"; import { ArkEnvError } from "./errors"; import type { CoerceOptions } from "./utils"; -export type EnvSchema = at.validate; +export type EnvSchema = at.validate; type RuntimeEnvironment = Record; /** @@ -36,7 +37,10 @@ export type ArkEnvConfig = { }; /** - * Pure Standard Schema entry point. + * Strict Standard Schema entry point. + * Use this when you have a pre-defined validator (e.g. `z.object({ ... })`) + * and want zero ArkType DSL behavior. + * * Zero implicit coercion, zero ArkType dependency (unless passed an ArkType schema). */ export function defineEnv( @@ -63,13 +67,25 @@ export function defineEnv( } /** - * Ergonomic entry point. Alias for `createEnv`. - * Supports ArkType raw objects, enabled if ArkType is present. + * Ergonomic, hybrid entry point (migration-friendly). + * Supports ArkType DSL, Standard Schema validators, or a mix of both. + * + * arkenv({ + * PORT: "number.port", // ArkType DSL + * HOST: z.string().min(1) // Standard Schema (Zod) + * }) + * + * IMPORTANT: arkenv() expects an object mapping environment keys to validators. + * Do not pass a wrapped schema like arkenv(z.object({ ... })). + * Use defineEnv() for strict Standard Schema paths. + * + * Type inference quality depends on the validator used. + * ArkType provides the richest inference. */ export function arkenv( def: EnvSchema, config?: ArkEnvConfig, -): distill.Out>; +): distill.Out>; export function arkenv( def: T, config?: ArkEnvConfig, @@ -77,11 +93,24 @@ export function arkenv( export function arkenv( def: EnvSchema | EnvSchemaWithType, config?: ArkEnvConfig, -): distill.Out> | InferType; +): distill.Out> | InferType; export function arkenv( def: EnvSchema | EnvSchemaWithType, config: ArkEnvConfig = {}, -): distill.Out> | InferType { +): distill.Out> | InferType { + // Guardrail: Ensure arkenv() only accepts an object map. + // We check for "~standard" (Standard Schema) or "assert" (compiled ArkType) + // which indicate the user passed a wrapped schema instead of an object map. + if ( + typeof def === "function" || + (def !== null && typeof def === "object" && "~standard" in def) + ) { + throw new Error( + "ArkEnv: arkenv() expects a mapping of { KEY: validator }, not a wrapped schema. " + + "If you want to pass a top-level validator like z.object(), use defineEnv() instead.", + ); + } + return defineEnv(def as any, config) as any; } diff --git a/packages/arkenv/src/object-parsing.integration.test.ts b/packages/arkenv/src/object-parsing.integration.test.ts index 72ed91a70..5c5baae0b 100644 --- a/packages/arkenv/src/object-parsing.integration.test.ts +++ b/packages/arkenv/src/object-parsing.integration.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { createEnv } from "./create-env"; +import { createEnv, defineEnv } from "./create-env"; import { type } from "./index"; describe("object parsing integration", () => { @@ -229,7 +229,7 @@ describe("object parsing integration", () => { }, }); - const env = createEnv(schema, { + const env = defineEnv(schema, { env: { CONFIG: '{"host": "localhost", "port": "3000"}', }, diff --git a/packages/arkenv/src/standard-schema.test.ts b/packages/arkenv/src/standard-schema.test.ts new file mode 100644 index 000000000..2b297447f --- /dev/null +++ b/packages/arkenv/src/standard-schema.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from "vitest"; +import { z } from "zod"; +import { arkenv, defineEnv } from "./index"; + +describe("Standard Schema integration", () => { + it("should work with top-level zod schemas via defineEnv", () => { + const schema = z.object({ + PORT: z.coerce.number(), + HOST: z.string().default("localhost"), + }); + + const env = defineEnv(schema, { + env: { PORT: "3000" }, + }); + + expect(env).toEqual({ + PORT: 3000, + HOST: "localhost", + }); + }); + + it("should support mixed validators in arkenv() mapping", () => { + const env = arkenv( + { + PORT: "number.port", // ArkType DSL + HOST: z.string().min(1), // Zod + }, + { + env: { PORT: "3000", HOST: "localhost" }, + }, + ); + + expect(env).toEqual({ + PORT: 3000, + HOST: "localhost", + }); + }); + + it("should throw a clear error if arkenv() is passed a wrapped schema", () => { + const schema = z.object({ PORT: z.number() }); + + expect(() => + arkenv(schema as any, { + env: { PORT: "not-a-number" }, + }), + ).toThrow(/expects a mapping of { KEY: validator }, not a wrapped schema/); + }); + + it("should verify arkenv accepts a Standard Schema when ArkType is present (via mapping)", () => { + const env = arkenv( + { + ZOD_VAL: z.string().email(), + }, + { + env: { ZOD_VAL: "test@example.com" }, + }, + ); + expect(env.ZOD_VAL).toBe("test@example.com"); + }); +}); diff --git a/packages/arkenv/src/zod.test.ts b/packages/arkenv/src/zod.test.ts deleted file mode 100644 index 01dc95866..000000000 --- a/packages/arkenv/src/zod.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { z } from "zod"; -import arkenv from "./index"; - -describe("zod integration", () => { - it("should work with zod schemas", () => { - const schema = z.object({ - PORT: z.coerce.number(), - HOST: z.string().default("localhost"), - }); - - const env = arkenv(schema, { - env: { PORT: "3000" }, - }); - - expect(env).toEqual({ - PORT: 3000, - HOST: "localhost", - }); - }); - - it("should throw ArkEnvError on validation failure", () => { - const schema = z.object({ - PORT: z.number(), - }); - - expect(() => - arkenv(schema, { - env: { PORT: "not-a-number" }, - }), - ).toThrow("PORT"); - }); -}); From 8b4541ff30009cdd30e2add16f1f74450d1dcff5 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Tue, 6 Jan 2026 23:15:25 +0500 Subject: [PATCH 004/117] refactor: rename `createEnv` to `defineEnv` in tests --- packages/arkenv/src/create-env.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/arkenv/src/create-env.test.ts b/packages/arkenv/src/create-env.test.ts index 281c78d4f..b14634f50 100644 --- a/packages/arkenv/src/create-env.test.ts +++ b/packages/arkenv/src/create-env.test.ts @@ -88,7 +88,7 @@ describe("createEnv", () => { ), }); - const env = createEnv(Env, { + const env = defineEnv(Env, { env: { PORT: "3000", VITE_MY_NUMBER_MANUAL: "456", From eb8e65362b6197656c48bf819c6159b8d01004b0 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Tue, 6 Jan 2026 23:33:35 +0500 Subject: [PATCH 005/117] style: sort imports and exports --- packages/arkenv/src/adapters/arktype.ts | 2 +- packages/arkenv/src/create-env.test.ts | 2 +- packages/arkenv/src/create-env.ts | 2 +- packages/arkenv/src/index.ts | 4 ++-- packages/arkenv/src/type.ts | 1 + 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/arkenv/src/adapters/arktype.ts b/packages/arkenv/src/adapters/arktype.ts index ebe2f62c6..429befebf 100644 --- a/packages/arkenv/src/adapters/arktype.ts +++ b/packages/arkenv/src/adapters/arktype.ts @@ -1,6 +1,6 @@ import { createRequire } from "node:module"; -import type { EnvIssue, SchemaAdapter } from "./index"; import { coerce } from "../utils/coerce"; +import type { EnvIssue, SchemaAdapter } from "./index"; const require = createRequire(import.meta.url); diff --git a/packages/arkenv/src/create-env.test.ts b/packages/arkenv/src/create-env.test.ts index b14634f50..46c731b43 100644 --- a/packages/arkenv/src/create-env.test.ts +++ b/packages/arkenv/src/create-env.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { arkenv, createEnv, defineEnv } from "./create-env"; import { z } from "zod"; +import { arkenv, createEnv, defineEnv } from "./create-env"; import { type } from "./type"; import { indent, styleText } from "./utils"; diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index b5173038a..2a8c93c2b 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -1,6 +1,6 @@ +import type { $ } from "@repo/scope"; import type { EnvSchemaWithType, InferType, SchemaShape } from "@repo/types"; import type { type as at, distill } from "arktype"; -import type { $ } from "@repo/scope"; import { ArkTypeAdapter } from "./adapters/arktype"; import { StandardSchemaAdapter } from "./adapters/standard"; import { ArkEnvError } from "./errors"; diff --git a/packages/arkenv/src/index.ts b/packages/arkenv/src/index.ts index 2f9164484..6eced7bbe 100644 --- a/packages/arkenv/src/index.ts +++ b/packages/arkenv/src/index.ts @@ -1,4 +1,4 @@ -export type { EnvSchema, ArkEnvConfig } from "./create-env"; +export type { ArkEnvConfig, EnvSchema } from "./create-env"; import { arkenv } from "./create-env"; @@ -8,6 +8,6 @@ import { arkenv } from "./create-env"; * {@link https://arkenv.js.org | ArkEnv} is a typesafe environment variables validator from editor to runtime. */ export default arkenv; -export { type } from "./type"; export { arkenv, createEnv, defineEnv } from "./create-env"; export { ArkEnvError } from "./errors"; +export { type } from "./type"; diff --git a/packages/arkenv/src/type.ts b/packages/arkenv/src/type.ts index 5728569a3..10fd5ef8b 100644 --- a/packages/arkenv/src/type.ts +++ b/packages/arkenv/src/type.ts @@ -1,4 +1,5 @@ import { createRequire } from "node:module"; + const require = createRequire(import.meta.url); import type { $ } from "@repo/scope"; From 9ea604045d9c7229d0ec4c59ed6e1f468abef9b0 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Tue, 6 Jan 2026 23:34:03 +0500 Subject: [PATCH 006/117] style(adapters): Use type alias for SchemaAdapter --- packages/arkenv/src/adapters/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/arkenv/src/adapters/index.ts b/packages/arkenv/src/adapters/index.ts index d6e84a847..64160931d 100644 --- a/packages/arkenv/src/adapters/index.ts +++ b/packages/arkenv/src/adapters/index.ts @@ -19,7 +19,7 @@ export type EnvIssue = { validator: "arktype" | "standard"; }; -export interface SchemaAdapter { +export type SchemaAdapter = { /** * The internal identity of the adapter. */ @@ -37,4 +37,4 @@ export interface SchemaAdapter { success: false; issues: EnvIssue[]; }; -} +}; From 1e840cfa0943d00d99462c1b3508a44f2dba3c13 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Tue, 6 Jan 2026 23:39:52 +0500 Subject: [PATCH 007/117] refactor: Rename `arkenv` and `createEnv` to `defineEnv` for improved API consistency. --- apps/playgrounds/vite-legacy/vite.config.ts | 4 ++-- apps/playgrounds/vite/vite.config.ts | 4 ++-- packages/bun-plugin/src/index.ts | 4 ++-- packages/vite-plugin/src/index.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/playgrounds/vite-legacy/vite.config.ts b/apps/playgrounds/vite-legacy/vite.config.ts index bc70439d8..92cf266ef 100644 --- a/apps/playgrounds/vite-legacy/vite.config.ts +++ b/apps/playgrounds/vite-legacy/vite.config.ts @@ -1,6 +1,6 @@ import arkenvVitePlugin from "@arkenv/vite-plugin"; import reactPlugin from "@vitejs/plugin-react"; -import arkenv, { type } from "arkenv"; +import { defineEnv, type } from "arkenv"; import { defineConfig, loadEnv } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; @@ -17,7 +17,7 @@ export const Env = type({ // https://vite.dev/config/ export default defineConfig(({ mode }) => { - const env = arkenv(Env, { env: loadEnv(mode, process.cwd(), "") }); + const env = defineEnv(Env, { env: loadEnv(mode, process.cwd(), "") }); console.log(`${env.VITE_MY_NUMBER} ${typeof env.VITE_MY_NUMBER}`); return { diff --git a/apps/playgrounds/vite/vite.config.ts b/apps/playgrounds/vite/vite.config.ts index bc70439d8..92cf266ef 100644 --- a/apps/playgrounds/vite/vite.config.ts +++ b/apps/playgrounds/vite/vite.config.ts @@ -1,6 +1,6 @@ import arkenvVitePlugin from "@arkenv/vite-plugin"; import reactPlugin from "@vitejs/plugin-react"; -import arkenv, { type } from "arkenv"; +import { defineEnv, type } from "arkenv"; import { defineConfig, loadEnv } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; @@ -17,7 +17,7 @@ export const Env = type({ // https://vite.dev/config/ export default defineConfig(({ mode }) => { - const env = arkenv(Env, { env: loadEnv(mode, process.cwd(), "") }); + const env = defineEnv(Env, { env: loadEnv(mode, process.cwd(), "") }); console.log(`${env.VITE_MY_NUMBER} ${typeof env.VITE_MY_NUMBER}`); return { diff --git a/packages/bun-plugin/src/index.ts b/packages/bun-plugin/src/index.ts index 9ba5881c4..97bc497e5 100644 --- a/packages/bun-plugin/src/index.ts +++ b/packages/bun-plugin/src/index.ts @@ -1,7 +1,7 @@ import { join } from "node:path"; import type { EnvSchemaWithType, SchemaShape } from "@repo/types"; import type { EnvSchema } from "arkenv"; -import { createEnv } from "arkenv"; +import { defineEnv } from "arkenv"; import type { BunPlugin, Loader, PluginBuilder } from "bun"; export type { ProcessEnvAugmented } from "./types"; @@ -18,7 +18,7 @@ export function processEnvSchema( // "Type instantiation is excessively deep and possibly infinite" errors. // Ideally, we should find a way to avoid these assertions while maintaining type safety. - const env = createEnv(options, { env: process.env }); + const env = defineEnv(options as any, { env: process.env }); // Get Bun's prefix for client-exposed environment variables const prefix = "BUN_PUBLIC_"; diff --git a/packages/vite-plugin/src/index.ts b/packages/vite-plugin/src/index.ts index 3c95a2e6c..23b276337 100644 --- a/packages/vite-plugin/src/index.ts +++ b/packages/vite-plugin/src/index.ts @@ -1,5 +1,5 @@ import type { EnvSchemaWithType, SchemaShape } from "@repo/types"; -import { createEnv, type EnvSchema } from "arkenv"; +import { defineEnv, type EnvSchema } from "arkenv"; import { loadEnv, type Plugin } from "vite"; export type { ImportMetaEnvAugmented } from "./types"; @@ -63,7 +63,7 @@ export default function arkenv( // TODO: We're using type assertions and explicitly pass in the type arguments here to avoid // "Type instantiation is excessively deep and possibly infinite" errors. // Ideally, we should find a way to avoid these assertions while maintaining type safety. - const env = createEnv(options, { + const env = defineEnv(options as any, { env: loadEnv(mode, envDir, ""), }); From 39410ea8eee4d08d9a3622d1b713986fb213cfc2 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Tue, 6 Jan 2026 23:54:00 +0500 Subject: [PATCH 008/117] feat: Unify `arkenv` and `defineEnv` into a single `arkenv` entry point, with `defineEnv` now an alias for compatibility. --- apps/playgrounds/vite-legacy/vite.config.ts | 4 +- apps/playgrounds/vite/vite.config.ts | 4 +- packages/arkenv/src/create-env.test.ts | 14 ++--- packages/arkenv/src/create-env.ts | 68 +++++++-------------- packages/arkenv/src/index.ts | 2 +- packages/bun-plugin/src/index.ts | 4 +- packages/vite-plugin/src/index.ts | 4 +- 7 files changed, 38 insertions(+), 62 deletions(-) diff --git a/apps/playgrounds/vite-legacy/vite.config.ts b/apps/playgrounds/vite-legacy/vite.config.ts index 92cf266ef..bc70439d8 100644 --- a/apps/playgrounds/vite-legacy/vite.config.ts +++ b/apps/playgrounds/vite-legacy/vite.config.ts @@ -1,6 +1,6 @@ import arkenvVitePlugin from "@arkenv/vite-plugin"; import reactPlugin from "@vitejs/plugin-react"; -import { defineEnv, type } from "arkenv"; +import arkenv, { type } from "arkenv"; import { defineConfig, loadEnv } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; @@ -17,7 +17,7 @@ export const Env = type({ // https://vite.dev/config/ export default defineConfig(({ mode }) => { - const env = defineEnv(Env, { env: loadEnv(mode, process.cwd(), "") }); + const env = arkenv(Env, { env: loadEnv(mode, process.cwd(), "") }); console.log(`${env.VITE_MY_NUMBER} ${typeof env.VITE_MY_NUMBER}`); return { diff --git a/apps/playgrounds/vite/vite.config.ts b/apps/playgrounds/vite/vite.config.ts index 92cf266ef..bc70439d8 100644 --- a/apps/playgrounds/vite/vite.config.ts +++ b/apps/playgrounds/vite/vite.config.ts @@ -1,6 +1,6 @@ import arkenvVitePlugin from "@arkenv/vite-plugin"; import reactPlugin from "@vitejs/plugin-react"; -import { defineEnv, type } from "arkenv"; +import arkenv, { type } from "arkenv"; import { defineConfig, loadEnv } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; @@ -17,7 +17,7 @@ export const Env = type({ // https://vite.dev/config/ export default defineConfig(({ mode }) => { - const env = defineEnv(Env, { env: loadEnv(mode, process.cwd(), "") }); + const env = arkenv(Env, { env: loadEnv(mode, process.cwd(), "") }); console.log(`${env.VITE_MY_NUMBER} ${typeof env.VITE_MY_NUMBER}`); return { diff --git a/packages/arkenv/src/create-env.test.ts b/packages/arkenv/src/create-env.test.ts index 46c731b43..8f894fe8f 100644 --- a/packages/arkenv/src/create-env.test.ts +++ b/packages/arkenv/src/create-env.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { z } from "zod"; -import { arkenv, createEnv, defineEnv } from "./create-env"; +import { arkenv, createEnv } from "./create-env"; import { type } from "./type"; import { indent, styleText } from "./utils"; @@ -88,7 +88,7 @@ describe("createEnv", () => { ), }); - const env = defineEnv(Env, { + const env = arkenv(Env, { env: { PORT: "3000", VITE_MY_NUMBER_MANUAL: "456", @@ -356,12 +356,12 @@ describe("createEnv", () => { }); // Use the same schema multiple times - const env1 = defineEnv(Env, { + const env1 = arkenv(Env, { env: { TEST_STRING: "first", }, }); - const env2 = defineEnv(Env, { + const env2 = arkenv(Env, { env: { TEST_STRING: "second", }, @@ -378,7 +378,7 @@ describe("createEnv", () => { INVALID_PORT: "number.port", }); - expect(() => defineEnv(Env)).toThrow(/INVALID_PORT/); + expect(() => arkenv(Env)).toThrow(/INVALID_PORT/); }); it("should work with custom environment and type definitions", () => { @@ -392,7 +392,7 @@ describe("createEnv", () => { PORT: "8080", }; - const env = defineEnv(Env, { env: customEnv }); + const env = arkenv(Env, { env: customEnv }); expect(env.HOST).toBe("localhost"); expect(env.PORT).toBe(8080); @@ -652,7 +652,7 @@ describe("createEnv", () => { }); it("should work with top-level Standard Schema via defineEnv", () => { - const env = defineEnv(z.object({ PORT: z.coerce.number() }), { + const env = arkenv(z.object({ PORT: z.coerce.number() }), { env: { PORT: "8080" }, }); expect(env.PORT).toBe(8080); diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 2a8c93c2b..41dd7a727 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -37,37 +37,7 @@ export type ArkEnvConfig = { }; /** - * Strict Standard Schema entry point. - * Use this when you have a pre-defined validator (e.g. `z.object({ ... })`) - * and want zero ArkType DSL behavior. - * - * Zero implicit coercion, zero ArkType dependency (unless passed an ArkType schema). - */ -export function defineEnv( - schema: T, - config: ArkEnvConfig = {}, -): InferType { - const env = config.env ?? process.env; - const isStandard = !!(schema as any)?.["~standard"]; - const isArkCompiled = - typeof schema === "function" && "assert" in (schema as any); - - const adapter = - isStandard && !isArkCompiled - ? new StandardSchemaAdapter(schema as any) - : new ArkTypeAdapter(schema, config as any); - - const result = adapter.validate(env); - - if (!result.success) { - throw new ArkEnvError(result.issues); - } - - return result.value as InferType; -} - -/** - * Ergonomic, hybrid entry point (migration-friendly). + * The primary entry point for ArkEnv. * Supports ArkType DSL, Standard Schema validators, or a mix of both. * * arkenv({ @@ -75,9 +45,8 @@ export function defineEnv( * HOST: z.string().min(1) // Standard Schema (Zod) * }) * - * IMPORTANT: arkenv() expects an object mapping environment keys to validators. - * Do not pass a wrapped schema like arkenv(z.object({ ... })). - * Use defineEnv() for strict Standard Schema paths. + * Or pass a pre-defined validator: + * arkenv(z.object({ PORT: z.coerce.number() })) * * Type inference quality depends on the validator used. * ArkType provides the richest inference. @@ -98,22 +67,29 @@ export function arkenv( def: EnvSchema | EnvSchemaWithType, config: ArkEnvConfig = {}, ): distill.Out> | InferType { - // Guardrail: Ensure arkenv() only accepts an object map. - // We check for "~standard" (Standard Schema) or "assert" (compiled ArkType) - // which indicate the user passed a wrapped schema instead of an object map. - if ( - typeof def === "function" || - (def !== null && typeof def === "object" && "~standard" in def) - ) { - throw new Error( - "ArkEnv: arkenv() expects a mapping of { KEY: validator }, not a wrapped schema. " + - "If you want to pass a top-level validator like z.object(), use defineEnv() instead.", - ); + const env = config.env ?? process.env; + const isStandard = !!(def as any)?.["~standard"]; + const isArkCompiled = typeof def === "function" && "assert" in (def as any); + + const adapter = + isStandard && !isArkCompiled + ? new StandardSchemaAdapter(def as any) + : new ArkTypeAdapter(def, config as any); + + const result = adapter.validate(env); + + if (!result.success) { + throw new ArkEnvError(result.issues); } - return defineEnv(def as any, config) as any; + return result.value as any; } +/** + * @private - Internal helper to maintain compatibility while refactoring. + */ +export const defineEnv = arkenv; + /** * @deprecated Use `arkenv` or `defineEnv` instead. */ diff --git a/packages/arkenv/src/index.ts b/packages/arkenv/src/index.ts index 6eced7bbe..709412605 100644 --- a/packages/arkenv/src/index.ts +++ b/packages/arkenv/src/index.ts @@ -8,6 +8,6 @@ import { arkenv } from "./create-env"; * {@link https://arkenv.js.org | ArkEnv} is a typesafe environment variables validator from editor to runtime. */ export default arkenv; -export { arkenv, createEnv, defineEnv } from "./create-env"; +export { arkenv, createEnv } from "./create-env"; export { ArkEnvError } from "./errors"; export { type } from "./type"; diff --git a/packages/bun-plugin/src/index.ts b/packages/bun-plugin/src/index.ts index 97bc497e5..d4569dad8 100644 --- a/packages/bun-plugin/src/index.ts +++ b/packages/bun-plugin/src/index.ts @@ -1,7 +1,7 @@ import { join } from "node:path"; import type { EnvSchemaWithType, SchemaShape } from "@repo/types"; import type { EnvSchema } from "arkenv"; -import { defineEnv } from "arkenv"; +import { arkenv as validateEnv } from "arkenv"; import type { BunPlugin, Loader, PluginBuilder } from "bun"; export type { ProcessEnvAugmented } from "./types"; @@ -18,7 +18,7 @@ export function processEnvSchema( // "Type instantiation is excessively deep and possibly infinite" errors. // Ideally, we should find a way to avoid these assertions while maintaining type safety. - const env = defineEnv(options as any, { env: process.env }); + const env = validateEnv(options as any, { env: process.env }); // Get Bun's prefix for client-exposed environment variables const prefix = "BUN_PUBLIC_"; diff --git a/packages/vite-plugin/src/index.ts b/packages/vite-plugin/src/index.ts index 23b276337..57a37c73e 100644 --- a/packages/vite-plugin/src/index.ts +++ b/packages/vite-plugin/src/index.ts @@ -1,5 +1,5 @@ import type { EnvSchemaWithType, SchemaShape } from "@repo/types"; -import { defineEnv, type EnvSchema } from "arkenv"; +import { arkenv as validateEnv, type EnvSchema } from "arkenv"; import { loadEnv, type Plugin } from "vite"; export type { ImportMetaEnvAugmented } from "./types"; @@ -63,7 +63,7 @@ export default function arkenv( // TODO: We're using type assertions and explicitly pass in the type arguments here to avoid // "Type instantiation is excessively deep and possibly infinite" errors. // Ideally, we should find a way to avoid these assertions while maintaining type safety. - const env = defineEnv(options as any, { + const env = validateEnv(options as any, { env: loadEnv(mode, envDir, ""), }); From f26da4cd6d3ab6ec95dc32fed2653f20d7fc0c03 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Tue, 6 Jan 2026 23:54:48 +0500 Subject: [PATCH 009/117] refactor(arkenv): Standardize env definition to arkenv --- packages/arkenv/src/coercion.integration.test.ts | 10 +++++----- packages/arkenv/src/object-parsing.integration.test.ts | 4 ++-- packages/arkenv/src/standard-schema.test.ts | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/arkenv/src/coercion.integration.test.ts b/packages/arkenv/src/coercion.integration.test.ts index 0b9e63c6d..6077d7496 100644 --- a/packages/arkenv/src/coercion.integration.test.ts +++ b/packages/arkenv/src/coercion.integration.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { createEnv, defineEnv } from "./create-env"; +import { arkenv, createEnv } from "./create-env"; import { type } from "./index"; describe("coercion integration", () => { @@ -60,7 +60,7 @@ describe("coercion integration", () => { it("should coerce when using compiled type definitions", () => { const schema = type({ PORT: "number" }); - const env = defineEnv(schema, { env: { PORT: "3000" } }); + const env = arkenv(schema, { env: { PORT: "3000" } }); expect(env.PORT).toBe(3000); expect(typeof env.PORT).toBe("number"); }); @@ -70,14 +70,14 @@ describe("coercion integration", () => { PORT: "number.port", COUNT: "number.integer", }); - const env = defineEnv(schema, { env: { PORT: "8080", COUNT: "123" } }); + const env = arkenv(schema, { env: { PORT: "8080", COUNT: "123" } }); expect(env.PORT).toBe(8080); expect(env.COUNT).toBe(123); }); it("should fail compiled type validation if coercion fails", () => { const schema = type({ PORT: "number" }); - expect(() => defineEnv(schema, { env: { PORT: "abc" } })).toThrow(); + expect(() => arkenv(schema, { env: { PORT: "abc" } })).toThrow(); }); it("should work with other number sub-keywords like epoch", () => { @@ -132,7 +132,7 @@ describe("coercion integration", () => { ), }); - const env = defineEnv(Env, { + const env = arkenv(Env, { env: { PORT: "3000", VITE_MY_NUMBER_MANUAL: "456", diff --git a/packages/arkenv/src/object-parsing.integration.test.ts b/packages/arkenv/src/object-parsing.integration.test.ts index 5c5baae0b..a587faa86 100644 --- a/packages/arkenv/src/object-parsing.integration.test.ts +++ b/packages/arkenv/src/object-parsing.integration.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { createEnv, defineEnv } from "./create-env"; +import { arkenv, createEnv } from "./create-env"; import { type } from "./index"; describe("object parsing integration", () => { @@ -229,7 +229,7 @@ describe("object parsing integration", () => { }, }); - const env = defineEnv(schema, { + const env = arkenv(schema, { env: { CONFIG: '{"host": "localhost", "port": "3000"}', }, diff --git a/packages/arkenv/src/standard-schema.test.ts b/packages/arkenv/src/standard-schema.test.ts index 2b297447f..92beb55e7 100644 --- a/packages/arkenv/src/standard-schema.test.ts +++ b/packages/arkenv/src/standard-schema.test.ts @@ -1,15 +1,15 @@ import { describe, expect, it } from "vitest"; import { z } from "zod"; -import { arkenv, defineEnv } from "./index"; +import { arkenv } from "./index"; describe("Standard Schema integration", () => { - it("should work with top-level zod schemas via defineEnv", () => { + it("should work with top-level zod schemas", () => { const schema = z.object({ PORT: z.coerce.number(), HOST: z.string().default("localhost"), }); - const env = defineEnv(schema, { + const env = arkenv(schema, { env: { PORT: "3000" }, }); From cbaf60d19a86c343168cfcbba13e90c9c0fd3831 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Tue, 6 Jan 2026 23:56:00 +0500 Subject: [PATCH 010/117] refactor(arkenv): consolidate env definition api - Replaced defineEnv with arkenv calls - Removed obsolete wrapped schema test - Updated test descriptions for arkenv --- packages/arkenv/src/create-env.test.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/arkenv/src/create-env.test.ts b/packages/arkenv/src/create-env.test.ts index 8f894fe8f..34e41c1cd 100644 --- a/packages/arkenv/src/create-env.test.ts +++ b/packages/arkenv/src/create-env.test.ts @@ -323,7 +323,7 @@ describe("createEnv", () => { TEST_PORT: "number.port", }); - const env = defineEnv(Env); + const env = arkenv(Env); expect(env.TEST_STRING).toBe("hello"); expect(env.TEST_PORT).toBe(3000); @@ -338,7 +338,7 @@ describe("createEnv", () => { TEST_PORT: "number.port", }); - const env = defineEnv(Env); + const env = arkenv(Env); // TypeScript should infer these correctly const str = env.TEST_STRING; @@ -645,13 +645,7 @@ describe("createEnv", () => { expect(env).toEqual({ PORT: 3000, HOST: "localhost" }); }); - it("should throw a clear error if arkenv() is passed a wrapped schema", () => { - expect(() => { - arkenv(z.object({ PORT: z.number() }) as any); - }).toThrow(/arkenv\(\) expects a mapping of { KEY: validator }/); - }); - - it("should work with top-level Standard Schema via defineEnv", () => { + it("should work with top-level Standard Schema", () => { const env = arkenv(z.object({ PORT: z.coerce.number() }), { env: { PORT: "8080" }, }); From 135387f5089e1e7359fe7d9e5be8bebcffb04667 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Tue, 6 Jan 2026 23:56:22 +0500 Subject: [PATCH 011/117] test(arkenv): remove wrapped schema error test --- packages/arkenv/src/standard-schema.test.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/arkenv/src/standard-schema.test.ts b/packages/arkenv/src/standard-schema.test.ts index 92beb55e7..9ddcee066 100644 --- a/packages/arkenv/src/standard-schema.test.ts +++ b/packages/arkenv/src/standard-schema.test.ts @@ -36,15 +36,6 @@ describe("Standard Schema integration", () => { }); }); - it("should throw a clear error if arkenv() is passed a wrapped schema", () => { - const schema = z.object({ PORT: z.number() }); - - expect(() => - arkenv(schema as any, { - env: { PORT: "not-a-number" }, - }), - ).toThrow(/expects a mapping of { KEY: validator }, not a wrapped schema/); - }); it("should verify arkenv accepts a Standard Schema when ArkType is present (via mapping)", () => { const env = arkenv( From dea4a9efad74052da93f04732b141aae865bce46 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Tue, 6 Jan 2026 23:57:16 +0500 Subject: [PATCH 012/117] style: apply consistent code formatting --- packages/arkenv/src/standard-schema.test.ts | 1 - packages/vite-plugin/src/index.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/arkenv/src/standard-schema.test.ts b/packages/arkenv/src/standard-schema.test.ts index 9ddcee066..074b88d2e 100644 --- a/packages/arkenv/src/standard-schema.test.ts +++ b/packages/arkenv/src/standard-schema.test.ts @@ -36,7 +36,6 @@ describe("Standard Schema integration", () => { }); }); - it("should verify arkenv accepts a Standard Schema when ArkType is present (via mapping)", () => { const env = arkenv( { diff --git a/packages/vite-plugin/src/index.ts b/packages/vite-plugin/src/index.ts index 57a37c73e..f46eadd64 100644 --- a/packages/vite-plugin/src/index.ts +++ b/packages/vite-plugin/src/index.ts @@ -1,5 +1,5 @@ import type { EnvSchemaWithType, SchemaShape } from "@repo/types"; -import { arkenv as validateEnv, type EnvSchema } from "arkenv"; +import { type EnvSchema, arkenv as validateEnv } from "arkenv"; import { loadEnv, type Plugin } from "vite"; export type { ImportMetaEnvAugmented } from "./types"; From 8e573be7b4f96a44d53595011acfe0228e84de70 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Wed, 7 Jan 2026 00:08:28 +0500 Subject: [PATCH 013/117] style: Adjust import order in vite-plugin and remove extraneous blank line in arkenv test. --- packages/vite-plugin/src/index.test.ts | 55 +++++++++++++------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/packages/vite-plugin/src/index.test.ts b/packages/vite-plugin/src/index.test.ts index b1e307cd5..6ec37043d 100644 --- a/packages/vite-plugin/src/index.test.ts +++ b/packages/vite-plugin/src/index.test.ts @@ -11,6 +11,7 @@ vi.mock("arkenv", async (importActual) => { ...actual, default: vi.fn(actual.default), createEnv: vi.fn(actual.createEnv), + arkenv: vi.fn(actual.arkenv), }; }); @@ -28,7 +29,7 @@ import arkenvPlugin from "./index.js"; const fixturesDir = join(__dirname, "__fixtures__"); // Get the mocked functions -const { createEnv: mockCreateEnv } = vi.mocked(await import("arkenv")); +const { arkenv: mockArkenv } = vi.mocked(await import("arkenv")); const mockLoadEnv = vi.mocked(vite.loadEnv); @@ -43,14 +44,14 @@ for (const name of readdirSync(fixturesDir).filter( beforeEach(() => { // Clear environment variables and mock cleanup vi.unstubAllEnvs(); - mockCreateEnv.mockClear(); + mockArkenv.mockClear(); mockLoadEnv.mockClear(); }); afterEach(() => { // Complete cleanup: restore environment and reset mocks vi.unstubAllEnvs(); - mockCreateEnv.mockReset(); + mockArkenv.mockReset(); mockLoadEnv.mockReset(); }); @@ -80,7 +81,7 @@ for (const name of readdirSync(fixturesDir).filter( ).resolves.not.toThrow(); // Verify that createEnv was called with the correct parameters - expect(mockCreateEnv).toHaveBeenCalledWith(config.Env, { + expect(mockArkenv).toHaveBeenCalledWith(config.Env, { env: expect.objectContaining(config.envVars || {}), }); }); @@ -91,13 +92,13 @@ for (const name of readdirSync(fixturesDir).filter( describe("Plugin Unit Tests", () => { beforeEach(() => { vi.unstubAllEnvs(); - mockCreateEnv.mockClear(); + mockArkenv.mockClear(); mockLoadEnv.mockClear(); }); afterEach(() => { vi.unstubAllEnvs(); - mockCreateEnv.mockReset(); + mockArkenv.mockReset(); mockLoadEnv.mockReset(); }); @@ -114,7 +115,7 @@ describe("Plugin Unit Tests", () => { it("should call createEnv during config hook", () => { // Mock createEnv to return a valid object - mockCreateEnv.mockReturnValue({ VITE_TEST: "test" }); + mockArkenv.mockReturnValue({ VITE_TEST: "test" }); const pluginInstance = arkenvPlugin({ VITE_TEST: "string" }); @@ -139,7 +140,7 @@ describe("Plugin Unit Tests", () => { ); } - expect(mockCreateEnv).toHaveBeenCalledWith( + expect(mockArkenv).toHaveBeenCalledWith( { VITE_TEST: "string" }, { env: expect.any(Object), @@ -154,7 +155,7 @@ describe("Plugin Unit Tests", () => { VITE_NUMBER: 42, VITE_BOOLEAN: true, }; - mockCreateEnv.mockReturnValue(mockTransformedEnv); + mockArkenv.mockReturnValue(mockTransformedEnv); const pluginInstance = arkenvPlugin({ VITE_STRING: "string", @@ -204,7 +205,7 @@ describe("Plugin Unit Tests", () => { VITE_ZERO: 0, VITE_FALSE: false, }; - mockCreateEnv.mockReturnValue(mockTransformedEnv); + mockArkenv.mockReturnValue(mockTransformedEnv); const pluginInstance = arkenvPlugin({ VITE_NULL: "string", @@ -245,7 +246,7 @@ describe("Plugin Unit Tests", () => { }); it("should handle empty environment object", () => { - mockCreateEnv.mockReturnValue({}); + mockArkenv.mockReturnValue({}); const pluginInstance = arkenvPlugin({}); @@ -280,7 +281,7 @@ describe("Plugin Unit Tests", () => { VITE_UPPERCASE: "test", vite_lowercase: "test", // This doesn't start with VITE_ so it will be filtered out }; - mockCreateEnv.mockReturnValue(mockTransformedEnv); + mockArkenv.mockReturnValue(mockTransformedEnv); const pluginInstance = arkenvPlugin({ VITE_SPECIAL_CHARS: "string", @@ -322,7 +323,7 @@ describe("Plugin Unit Tests", () => { it("should propagate errors from createEnv", () => { const error = new Error("Environment validation failed"); - mockCreateEnv.mockImplementation(() => { + mockArkenv.mockImplementation(() => { throw error; }); @@ -362,7 +363,7 @@ describe("Plugin Unit Tests", () => { VITE_API_URL: "https://api.example.com", VITE_DEBUG: true, }; - mockCreateEnv.mockReturnValue(mockTransformedEnv); + mockArkenv.mockReturnValue(mockTransformedEnv); const pluginInstance = arkenvPlugin({ PORT: "number.port", @@ -410,7 +411,7 @@ describe("Plugin Unit Tests", () => { VITE_OLD_VAR: "should not be exposed", SECRET_KEY: "should not be exposed", }; - mockCreateEnv.mockReturnValue(mockTransformedEnv); + mockArkenv.mockReturnValue(mockTransformedEnv); const pluginInstance = arkenvPlugin({ PUBLIC_API_URL: "string", @@ -456,7 +457,7 @@ describe("Plugin Unit Tests", () => { VITE_API_URL: "https://api.example.com", PUBLIC_DEBUG: true, }; - mockCreateEnv.mockReturnValue(mockTransformedEnv); + mockArkenv.mockReturnValue(mockTransformedEnv); const pluginInstance = arkenvPlugin({ VITE_API_URL: "string", @@ -501,7 +502,7 @@ describe("Plugin Unit Tests", () => { CUSTOM_PREFIX_VAR: "test", SECRET_KEY: "should not be exposed", }; - mockCreateEnv.mockReturnValue(mockTransformedEnv); + mockArkenv.mockReturnValue(mockTransformedEnv); const pluginInstance = arkenvPlugin({ VITE_API_URL: "string", @@ -543,7 +544,7 @@ describe("Plugin Unit Tests", () => { }); it("should use custom envDir when provided in config", async () => { - mockCreateEnv.mockReturnValue({ VITE_TEST: "test" }); + mockArkenv.mockReturnValue({ VITE_TEST: "test" }); const pluginInstance = arkenvPlugin({ VITE_TEST: "string" }); @@ -572,7 +573,7 @@ describe("Plugin Unit Tests", () => { expect(mockLoadEnv).toHaveBeenCalledWith("test", "/custom/env/dir", ""); // Verify createEnv was called - the envDir is used by loadEnv internally - expect(mockCreateEnv).toHaveBeenCalledWith( + expect(mockArkenv).toHaveBeenCalledWith( { VITE_TEST: "string" }, { env: expect.any(Object), @@ -581,7 +582,7 @@ describe("Plugin Unit Tests", () => { }); it("should default to process.cwd() when envDir is not configured", () => { - mockCreateEnv.mockReturnValue({ VITE_TEST: "test" }); + mockArkenv.mockReturnValue({ VITE_TEST: "test" }); const pluginInstance = arkenvPlugin({ VITE_TEST: "string" }); @@ -610,7 +611,7 @@ describe("Plugin Unit Tests", () => { expect(mockLoadEnv).toHaveBeenCalledWith("test", process.cwd(), ""); // Verify createEnv was called successfully with default behavior - expect(mockCreateEnv).toHaveBeenCalledWith( + expect(mockArkenv).toHaveBeenCalledWith( { VITE_TEST: "string" }, { env: expect.any(Object), @@ -649,13 +650,13 @@ describe("Custom envDir Configuration (with-env-dir fixture)", () => { beforeEach(() => { vi.unstubAllEnvs(); - mockCreateEnv.mockClear(); + mockArkenv.mockClear(); mockLoadEnv.mockClear(); }); afterEach(() => { vi.unstubAllEnvs(); - mockCreateEnv.mockClear(); + mockArkenv.mockClear(); mockLoadEnv.mockClear(); }); @@ -666,7 +667,7 @@ describe("Custom envDir Configuration (with-env-dir fixture)", () => { vite.build(createBuildConfig(customEnvDir, config.Env)), ).resolves.not.toThrow(); - expect(mockCreateEnv).toHaveBeenCalledWith(config.Env, { + expect(mockArkenv).toHaveBeenCalledWith(config.Env, { env: expect.objectContaining(expectedEnvVars), }); }); @@ -679,7 +680,7 @@ describe("Custom envDir Configuration (with-env-dir fixture)", () => { vite.build(createBuildConfig(nonExistentEnvDir, config.Env)), ).rejects.toThrow(); - expect(mockCreateEnv).toHaveBeenCalledWith(config.Env, { + expect(mockArkenv).toHaveBeenCalledWith(config.Env, { env: expect.any(Object), }); }); @@ -700,7 +701,7 @@ describe("Custom envDir Configuration (with-env-dir fixture)", () => { vite.build(createBuildConfig(customEnvDir, config.Env)), ).resolves.not.toThrow(); - expect(mockCreateEnv).toHaveBeenCalledWith(config.Env, { + expect(mockArkenv).toHaveBeenCalledWith(config.Env, { env: expect.objectContaining(expectedEnvVars), }); }); @@ -715,7 +716,7 @@ describe("Custom envDir Configuration (with-env-dir fixture)", () => { await vite.build(createBuildConfig(customEnvDir, config.Env)); // Verify that all env vars (including non-schema ones) are passed - expect(mockCreateEnv).toHaveBeenCalledWith(config.Env, { + expect(mockArkenv).toHaveBeenCalledWith(config.Env, { env: expect.objectContaining(envWithExtra), }); }); From f58539f6283c1ea718c20b85ccb1a8010d14a8c3 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Wed, 7 Jan 2026 00:22:42 +0500 Subject: [PATCH 014/117] feat(arkenv): expose arkenv main function --- packages/arkenv/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/arkenv/src/index.ts b/packages/arkenv/src/index.ts index 61986646a..7cda44547 100644 --- a/packages/arkenv/src/index.ts +++ b/packages/arkenv/src/index.ts @@ -1,4 +1,4 @@ -import { createEnv } from "./create-env"; +import { arkenv, createEnv } from "./create-env"; export type { EnvSchema } from "./create-env"; From 952e306b80d953a06ed634ad375af6b0272e79e3 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Thu, 8 Jan 2026 23:57:15 +0500 Subject: [PATCH 015/117] refactor(arkenv): require mapping for arkenv() input - `arkenv()` now strictly expects a schema mapping object - No longer accepts top-level compiled ArkType or Zod --- apps/www/content/docs/bun-plugin/index.mdx | 20 +-- .../arkenv/src/coercion.integration.test.ts | 53 +++--- packages/arkenv/src/create-env.test.ts | 153 +++++++++--------- packages/arkenv/src/create-env.ts | 67 ++++---- .../src/object-parsing.integration.test.ts | 23 +-- packages/arkenv/src/standard-schema.test.ts | 15 +- packages/bun-plugin/src/index.ts | 14 +- packages/vite-plugin/src/index.ts | 9 +- 8 files changed, 172 insertions(+), 182 deletions(-) diff --git a/apps/www/content/docs/bun-plugin/index.mdx b/apps/www/content/docs/bun-plugin/index.mdx index 69a38aca2..c50681074 100644 --- a/apps/www/content/docs/bun-plugin/index.mdx +++ b/apps/www/content/docs/bun-plugin/index.mdx @@ -17,13 +17,11 @@ The simple setup automatically discovers your schema from `src/env.ts` or `env.t #### 1. Create your schema -```ts title="src/env.ts" twoslash -import { type } from "arkenv"; - -export default type({ +```ts title="src/env.ts" +export default { BUN_PUBLIC_API_URL: "string", BUN_PUBLIC_DEBUG: "boolean", -}); +}; ``` #### 2. Configure for Bun.serve (dev mode) @@ -55,12 +53,10 @@ Pass your schema directly to the plugin function: ```ts title="build.ts" import arkenv from "@arkenv/bun-plugin"; -import { type } from "arkenv"; - -const Env = type({ +const Env = { BUN_PUBLIC_API_URL: "string", BUN_PUBLIC_DEBUG: "boolean", -}); +} as const; await Bun.build({ entrypoints: ["./app.tsx"], @@ -75,12 +71,10 @@ Create a custom plugin file and reference it in `bunfig.toml`: ```ts title="plugins/arkenv.ts" import arkenv from "@arkenv/bun-plugin"; -import { type } from "arkenv"; - -const Env = type({ +const Env = { BUN_PUBLIC_API_URL: "string", BUN_PUBLIC_DEBUG: "boolean", -}); +} as const; export default arkenv(Env); ``` diff --git a/packages/arkenv/src/coercion.integration.test.ts b/packages/arkenv/src/coercion.integration.test.ts index 6077d7496..b89e7421a 100644 --- a/packages/arkenv/src/coercion.integration.test.ts +++ b/packages/arkenv/src/coercion.integration.test.ts @@ -58,26 +58,28 @@ describe("coercion integration", () => { expect(env.DEBUG).toBe(true); }); - it("should coerce when using compiled type definitions", () => { - const schema = type({ PORT: "number" }); - const env = arkenv(schema, { env: { PORT: "3000" } }); + it("should coerce when using ArkType inside mapping", () => { + const env = arkenv({ PORT: type("number") }, { env: { PORT: "3000" } }); expect(env.PORT).toBe(3000); expect(typeof env.PORT).toBe("number"); }); - it("should coerce compiled number subtypes", () => { - const schema = type({ - PORT: "number.port", - COUNT: "number.integer", - }); - const env = arkenv(schema, { env: { PORT: "8080", COUNT: "123" } }); + it("should coerce number subtypes in mapping", () => { + const env = arkenv( + { + PORT: "number.port", + COUNT: "number.integer", + }, + { env: { PORT: "8080", COUNT: "123" } }, + ); expect(env.PORT).toBe(8080); expect(env.COUNT).toBe(123); }); - it("should fail compiled type validation if coercion fails", () => { - const schema = type({ PORT: "number" }); - expect(() => arkenv(schema, { env: { PORT: "abc" } })).toThrow(); + it("should fail validation if coercion fails in mapping", () => { + expect(() => + arkenv({ PORT: "number" }, { env: { PORT: "abc" } }), + ).toThrow(); }); it("should work with other number sub-keywords like epoch", () => { @@ -124,20 +126,21 @@ describe("coercion integration", () => { ).toThrow(); }); - it("should work with schemas containing morphs", () => { - const Env = type({ - PORT: "number.port", - VITE_MY_NUMBER_MANUAL: type("string").pipe((str) => - Number.parseInt(str, 10), - ), - }); - - const env = arkenv(Env, { - env: { - PORT: "3000", - VITE_MY_NUMBER_MANUAL: "456", + it("should work with schemas containing morphs in mapping", () => { + const env = arkenv( + { + PORT: "number.port", + VITE_MY_NUMBER_MANUAL: type("string").pipe((str) => + Number.parseInt(str, 10), + ), }, - }); + { + env: { + PORT: "3000", + VITE_MY_NUMBER_MANUAL: "456", + }, + }, + ); expect(env.PORT).toBe(3000); expect(env.VITE_MY_NUMBER_MANUAL).toBe(456); diff --git a/packages/arkenv/src/create-env.test.ts b/packages/arkenv/src/create-env.test.ts index 34e41c1cd..e8305e400 100644 --- a/packages/arkenv/src/create-env.test.ts +++ b/packages/arkenv/src/create-env.test.ts @@ -75,25 +75,30 @@ describe("createEnv", () => { expect(createEnv(schema, { env: {} }).PORT).toBeUndefined(); }); - it("should coerce strict number literals", () => { - const schema = { VAL: "1 | 2" } as const; - expect(createEnv(schema, { env: { VAL: "1" } }).VAL).toBe(1); + it("should handle validation errors from mapping", () => { + expect(() => { + arkenv( + { INVALID_PORT: "number" }, + { env: { INVALID_PORT: "not-a-number" } }, + ); + }).toThrow(); }); - it("should work with schemas containing morphs", () => { - const Env = type({ - PORT: "number.port", - VITE_MY_NUMBER_MANUAL: type("string").pipe((str) => - Number.parseInt(str, 10), - ), - }); - - const env = arkenv(Env, { - env: { - PORT: "3000", - VITE_MY_NUMBER_MANUAL: "456", + it("should work with schemas containing morphs in mapping", () => { + const env = arkenv( + { + PORT: "number.port", + VITE_MY_NUMBER_MANUAL: type("string").pipe((str) => + Number.parseInt(str, 10), + ), }, - }); + { + env: { + PORT: "3000", + VITE_MY_NUMBER_MANUAL: "456", + }, + }, + ); expect(env.PORT).toBe(3000); expect(env.VITE_MY_NUMBER_MANUAL).toBe(456); @@ -314,31 +319,31 @@ describe("createEnv", () => { }); describe("type definitions", () => { - it("should accept type definitions created with type()", () => { - process.env.TEST_STRING = "hello"; - process.env.TEST_PORT = "3000"; - - const Env = type({ - TEST_STRING: "string", - TEST_PORT: "number.port", - }); - - const env = arkenv(Env); + it("should infer types from a mapping", () => { + const env = arkenv( + { + TEST_STRING: "string", + TEST_PORT: "number", + }, + { + env: { TEST_STRING: "test", TEST_PORT: "3000" }, + }, + ); - expect(env.TEST_STRING).toBe("hello"); + expect(env.TEST_STRING).toBe("test"); expect(env.TEST_PORT).toBe(3000); }); it("should provide correct type inference with type definitions", () => { - process.env.TEST_STRING = "hello"; - process.env.TEST_PORT = "3000"; - - const Env = type({ - TEST_STRING: "string", - TEST_PORT: "number.port", - }); - - const env = arkenv(Env); + const env = arkenv( + { + TEST_STRING: "string", + TEST_PORT: "number.port", + }, + { + env: { TEST_STRING: "hello", TEST_PORT: "3000" }, + }, + ); // TypeScript should infer these correctly const str = env.TEST_STRING; @@ -348,51 +353,35 @@ describe("createEnv", () => { expect(port).toBe(3000); }); - it("should allow reusing the same type definition multiple times", () => { - process.env.TEST_STRING = "hello"; + it("should allow extending mappings", () => { + const base = { TEST_STRING: "string" } as const; + const extended = { ...base, TEST_PORT: "number" } as const; - const Env = type({ - TEST_STRING: "string", + const env1 = arkenv(base, { env: { TEST_STRING: "test" } }); + const env2 = arkenv(extended, { + env: { TEST_STRING: "test", TEST_PORT: "3000" }, }); - // Use the same schema multiple times - const env1 = arkenv(Env, { - env: { - TEST_STRING: "first", - }, - }); - const env2 = arkenv(Env, { - env: { - TEST_STRING: "second", - }, - }); - - expect(env1.TEST_STRING).toBe("first"); - expect(env2.TEST_STRING).toBe("second"); + expect(env1.TEST_STRING).toBe("test"); + expect(env2.TEST_PORT).toBe(3000); }); - it("should throw when type definition validation fails", () => { + it("should throw when mapping validation fails", () => { process.env.INVALID_PORT = "not-a-port"; - const Env = type({ - INVALID_PORT: "number.port", - }); - - expect(() => arkenv(Env)).toThrow(/INVALID_PORT/); + expect(() => + arkenv({ + INVALID_PORT: "number.port", + }), + ).toThrow(/INVALID_PORT/); }); - it("should work with custom environment and type definitions", () => { - const Env = type({ - HOST: "string.host", - PORT: "number.port", - }); - - const customEnv = { - HOST: "localhost", - PORT: "8080", - }; - - const env = arkenv(Env, { env: customEnv }); + it("should support custom environment and mapping", () => { + const customEnv = { HOST: "localhost", PORT: "8080" }; + const env = arkenv( + { HOST: "string", PORT: "number" }, + { env: customEnv }, + ); expect(env.HOST).toBe("localhost"); expect(env.PORT).toBe(8080); @@ -631,6 +620,15 @@ describe("createEnv", () => { }); describe("migration & hybrid support", () => { + it("should throw if top-level compiled ArkType schema is passed directly", () => { + const schema = type({ PORT: "number" }); + expect(() => + arkenv(schema as any, { + env: { PORT: "3000" }, + }), + ).toThrow(/arkenv\(\) expects a mapping/); + }); + it("should support mixed ArkType DSL and Standard Schema validators", () => { const env = arkenv( { @@ -645,11 +643,12 @@ describe("createEnv", () => { expect(env).toEqual({ PORT: 3000, HOST: "localhost" }); }); - it("should work with top-level Standard Schema", () => { - const env = arkenv(z.object({ PORT: z.coerce.number() }), { - env: { PORT: "8080" }, - }); - expect(env.PORT).toBe(8080); + it("should throw if top-level Standard Schema is passed directly", () => { + expect(() => + arkenv(z.object({ PORT: z.coerce.number() }) as any, { + env: { PORT: "8080" }, + }), + ).toThrow(/arkenv\(\) expects a mapping/); }); }); }); diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 41dd7a727..a0a3f99f8 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -37,36 +37,13 @@ export type ArkEnvConfig = { }; /** - * The primary entry point for ArkEnv. - * Supports ArkType DSL, Standard Schema validators, or a mix of both. - * - * arkenv({ - * PORT: "number.port", // ArkType DSL - * HOST: z.string().min(1) // Standard Schema (Zod) - * }) - * - * Or pass a pre-defined validator: - * arkenv(z.object({ PORT: z.coerce.number() })) - * - * Type inference quality depends on the validator used. - * ArkType provides the richest inference. + * Strict Standard Schema entry point. + * @private - Supporting internal modules and plugins that import pre-defined validators. */ -export function arkenv( - def: EnvSchema, - config?: ArkEnvConfig, -): distill.Out>; -export function arkenv( +export function defineEnv( def: T, - config?: ArkEnvConfig, -): InferType; -export function arkenv( - def: EnvSchema | EnvSchemaWithType, - config?: ArkEnvConfig, -): distill.Out> | InferType; -export function arkenv( - def: EnvSchema | EnvSchemaWithType, config: ArkEnvConfig = {}, -): distill.Out> | InferType { +): InferType { const env = config.env ?? process.env; const isStandard = !!(def as any)?.["~standard"]; const isArkCompiled = typeof def === "function" && "assert" in (def as any); @@ -86,11 +63,41 @@ export function arkenv( } /** - * @private - Internal helper to maintain compatibility while refactoring. + * The primary entry point for ArkEnv. + * + * arkenv({ + * PORT: "number.port", // ArkType DSL + * HOST: z.string().min(1) // Standard Schema (Zod) + * }) + * + * IMPORTANT: arkenv() expects an object mapping environment keys to validators. + * Standard Schema validators are supported inside the mapping for migration, + * but top-level wrapped schemas (like z.object()) are not supported at this entry point. + * + * Type inference quality depends on the validator used. + * ArkType provides the richest inference. */ -export const defineEnv = arkenv; +export function arkenv( + def: EnvSchema, + config: ArkEnvConfig = {}, +): distill.Out> { + // Guardrail: Ensure arkenv() only accepts an object map. + // We check for "~standard" (Standard Schema) or "assert" (compiled ArkType) + // which indicate the user passed a wrapped schema instead of an object map. + if ( + typeof def === "function" || + (def !== null && typeof def === "object" && "~standard" in def) + ) { + throw new Error( + "ArkEnv: arkenv() expects a mapping of { KEY: validator }, not a wrapped schema. " + + "Standard Schema validators are supported inside the mapping for migration, but not as the top-level argument.", + ); + } + + return defineEnv(def as any, config); +} /** - * @deprecated Use `arkenv` or `defineEnv` instead. + * @deprecated Use `arkenv` instead. */ export const createEnv = arkenv; diff --git a/packages/arkenv/src/object-parsing.integration.test.ts b/packages/arkenv/src/object-parsing.integration.test.ts index a587faa86..7ec75c7dc 100644 --- a/packages/arkenv/src/object-parsing.integration.test.ts +++ b/packages/arkenv/src/object-parsing.integration.test.ts @@ -221,19 +221,20 @@ describe("object parsing integration", () => { }); }); - it("should work with type() compiled schemas", () => { - const schema = type({ - CONFIG: { - host: "string", - port: "number", + it("should work with objects in mapping", () => { + const env = arkenv( + { + CONFIG: { + host: "string", + port: "number", + }, }, - }); - - const env = arkenv(schema, { - env: { - CONFIG: '{"host": "localhost", "port": "3000"}', + { + env: { + CONFIG: '{"host": "localhost", "port": "3000"}', + }, }, - }); + ); expect(env.CONFIG).toEqual({ host: "localhost", diff --git a/packages/arkenv/src/standard-schema.test.ts b/packages/arkenv/src/standard-schema.test.ts index 074b88d2e..39340d3c1 100644 --- a/packages/arkenv/src/standard-schema.test.ts +++ b/packages/arkenv/src/standard-schema.test.ts @@ -3,20 +3,17 @@ import { z } from "zod"; import { arkenv } from "./index"; describe("Standard Schema integration", () => { - it("should work with top-level zod schemas", () => { + it("should throw if a top-level zod schema is passed directly", () => { const schema = z.object({ PORT: z.coerce.number(), HOST: z.string().default("localhost"), }); - const env = arkenv(schema, { - env: { PORT: "3000" }, - }); - - expect(env).toEqual({ - PORT: 3000, - HOST: "localhost", - }); + expect(() => + arkenv(schema as any, { + env: { PORT: "3000" }, + }), + ).toThrow(/arkenv\(\) expects a mapping/); }); it("should support mixed validators in arkenv() mapping", () => { diff --git a/packages/bun-plugin/src/index.ts b/packages/bun-plugin/src/index.ts index d4569dad8..a573f6a3b 100644 --- a/packages/bun-plugin/src/index.ts +++ b/packages/bun-plugin/src/index.ts @@ -1,5 +1,5 @@ import { join } from "node:path"; -import type { EnvSchemaWithType, SchemaShape } from "@repo/types"; +import type { SchemaShape } from "@repo/types"; import type { EnvSchema } from "arkenv"; import { arkenv as validateEnv } from "arkenv"; import type { BunPlugin, Loader, PluginBuilder } from "bun"; @@ -10,7 +10,7 @@ export type { ProcessEnvAugmented } from "./types"; * Helper to process env schema and return envMap */ export function processEnvSchema( - options: EnvSchema | EnvSchemaWithType, + options: EnvSchema, ): Map { // Validate environment variables @@ -124,12 +124,8 @@ function registerLoader(build: PluginBuilder, envMap: Map) { * }) * ``` */ -export function arkenv(options: EnvSchemaWithType): BunPlugin; export function arkenv( options: EnvSchema, -): BunPlugin; -export function arkenv( - options: EnvSchema | EnvSchemaWithType, ): BunPlugin { const envMap = processEnvSchema(options); @@ -219,12 +215,10 @@ hybrid.setup = (build) => { const example = ` Example \`src/env.ts\`: \`\`\`ts -import { type } from "arktype"; - -export default type({ +export default { BUN_PUBLIC_API_URL: "string", BUN_PUBLIC_DEBUG: "boolean" -}); +}; \`\`\` `; throw new Error( diff --git a/packages/vite-plugin/src/index.ts b/packages/vite-plugin/src/index.ts index f46eadd64..717b9d069 100644 --- a/packages/vite-plugin/src/index.ts +++ b/packages/vite-plugin/src/index.ts @@ -1,4 +1,4 @@ -import type { EnvSchemaWithType, SchemaShape } from "@repo/types"; +import type { SchemaShape } from "@repo/types"; import { type EnvSchema, arkenv as validateEnv } from "arkenv"; import { loadEnv, type Plugin } from "vite"; @@ -16,8 +16,7 @@ export type { ImportMetaEnvAugmented } from "./types"; * automatically filters them based on Vite's `envPrefix` configuration (defaults to `"VITE_"`). * Only environment variables matching the prefix are exposed to client code via `import.meta.env.*`. * - * @param options - The environment variable schema definition. Can be an `EnvSchema` object - * for typesafe validation or an ArkType `EnvSchemaWithType` for dynamic schemas. + * @param options - The environment variable schema definition as an object mapping environment keys to validators. * @returns A Vite plugin that validates environment variables and exposes them to the client. * * @example @@ -42,12 +41,8 @@ export type { ImportMetaEnvAugmented } from "./types"; * console.log(import.meta.env.VITE_API_URL); // Typesafe access * ``` */ -export default function arkenv(options: EnvSchemaWithType): Plugin; export default function arkenv( options: EnvSchema, -): Plugin; -export default function arkenv( - options: EnvSchema | EnvSchemaWithType, ): Plugin { return { name: "@arkenv/vite-plugin", From af199304b6736e0e0bbe09da88d6ebafe141cda3 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 00:17:43 +0500 Subject: [PATCH 016/117] feat(arkenv): Improve schema and validator support - Allow compiled ArkType schemas as top-level arg - Support standard Zod schemas as top-level arg - Remove invalid schema argument error - Update Bun and Vite plugins to support new schema types --- apps/www/content/docs/bun-plugin/index.mdx | 20 ++++++---- .../arkenv/src/coercion.integration.test.ts | 39 +++++++++---------- packages/arkenv/src/create-env.test.ts | 26 ++++++------- packages/arkenv/src/create-env.ts | 25 +++++------- .../src/object-parsing.integration.test.ts | 31 +++++++-------- packages/arkenv/src/standard-schema.test.ts | 15 ++++--- packages/bun-plugin/src/index.ts | 8 +++- .../src/__fixtures__/basic/config.ts | 6 +-- .../src/__fixtures__/with-env-dir/config.ts | 6 +-- packages/vite-plugin/src/index.ts | 6 ++- 10 files changed, 93 insertions(+), 89 deletions(-) diff --git a/apps/www/content/docs/bun-plugin/index.mdx b/apps/www/content/docs/bun-plugin/index.mdx index c50681074..69a38aca2 100644 --- a/apps/www/content/docs/bun-plugin/index.mdx +++ b/apps/www/content/docs/bun-plugin/index.mdx @@ -17,11 +17,13 @@ The simple setup automatically discovers your schema from `src/env.ts` or `env.t #### 1. Create your schema -```ts title="src/env.ts" -export default { +```ts title="src/env.ts" twoslash +import { type } from "arkenv"; + +export default type({ BUN_PUBLIC_API_URL: "string", BUN_PUBLIC_DEBUG: "boolean", -}; +}); ``` #### 2. Configure for Bun.serve (dev mode) @@ -53,10 +55,12 @@ Pass your schema directly to the plugin function: ```ts title="build.ts" import arkenv from "@arkenv/bun-plugin"; -const Env = { +import { type } from "arkenv"; + +const Env = type({ BUN_PUBLIC_API_URL: "string", BUN_PUBLIC_DEBUG: "boolean", -} as const; +}); await Bun.build({ entrypoints: ["./app.tsx"], @@ -71,10 +75,12 @@ Create a custom plugin file and reference it in `bunfig.toml`: ```ts title="plugins/arkenv.ts" import arkenv from "@arkenv/bun-plugin"; -const Env = { +import { type } from "arkenv"; + +const Env = type({ BUN_PUBLIC_API_URL: "string", BUN_PUBLIC_DEBUG: "boolean", -} as const; +}); export default arkenv(Env); ``` diff --git a/packages/arkenv/src/coercion.integration.test.ts b/packages/arkenv/src/coercion.integration.test.ts index b89e7421a..649d9a63b 100644 --- a/packages/arkenv/src/coercion.integration.test.ts +++ b/packages/arkenv/src/coercion.integration.test.ts @@ -58,8 +58,9 @@ describe("coercion integration", () => { expect(env.DEBUG).toBe(true); }); - it("should coerce when using ArkType inside mapping", () => { - const env = arkenv({ PORT: type("number") }, { env: { PORT: "3000" } }); + it("should coerce when using compiled type definitions", () => { + const schema = type({ PORT: "number" }); + const env = arkenv(schema, { env: { PORT: "3000" } }); expect(env.PORT).toBe(3000); expect(typeof env.PORT).toBe("number"); }); @@ -76,10 +77,9 @@ describe("coercion integration", () => { expect(env.COUNT).toBe(123); }); - it("should fail validation if coercion fails in mapping", () => { - expect(() => - arkenv({ PORT: "number" }, { env: { PORT: "abc" } }), - ).toThrow(); + it("should fail compiled type validation if coercion fails", () => { + const schema = type({ PORT: "number" }); + expect(() => arkenv(schema, { env: { PORT: "abc" } })).toThrow(); }); it("should work with other number sub-keywords like epoch", () => { @@ -126,21 +126,20 @@ describe("coercion integration", () => { ).toThrow(); }); - it("should work with schemas containing morphs in mapping", () => { - const env = arkenv( - { - PORT: "number.port", - VITE_MY_NUMBER_MANUAL: type("string").pipe((str) => - Number.parseInt(str, 10), - ), - }, - { - env: { - PORT: "3000", - VITE_MY_NUMBER_MANUAL: "456", - }, + it("should work with schemas containing morphs", () => { + const Env = type({ + PORT: "number.port", + VITE_MY_NUMBER_MANUAL: type("string").pipe((str) => + Number.parseInt(str, 10), + ), + }); + + const env = arkenv(Env, { + env: { + PORT: "3000", + VITE_MY_NUMBER_MANUAL: "456", }, - ); + }); expect(env.PORT).toBe(3000); expect(env.VITE_MY_NUMBER_MANUAL).toBe(456); diff --git a/packages/arkenv/src/create-env.test.ts b/packages/arkenv/src/create-env.test.ts index e8305e400..251e331d4 100644 --- a/packages/arkenv/src/create-env.test.ts +++ b/packages/arkenv/src/create-env.test.ts @@ -620,13 +620,19 @@ describe("createEnv", () => { }); describe("migration & hybrid support", () => { - it("should throw if top-level compiled ArkType schema is passed directly", () => { + it("should work with top-level compiled ArkType schema", () => { const schema = type({ PORT: "number" }); - expect(() => - arkenv(schema as any, { - env: { PORT: "3000" }, - }), - ).toThrow(/arkenv\(\) expects a mapping/); + const env = arkenv(schema, { + env: { PORT: "3000" }, + }); + expect(env.PORT).toBe(3000); + }); + + it("should work with top-level Standard Schema", () => { + const env = arkenv(z.object({ PORT: z.coerce.number() }), { + env: { PORT: "8080" }, + }); + expect(env.PORT).toBe(8080); }); it("should support mixed ArkType DSL and Standard Schema validators", () => { @@ -642,13 +648,5 @@ describe("createEnv", () => { expect(env).toEqual({ PORT: 3000, HOST: "localhost" }); }); - - it("should throw if top-level Standard Schema is passed directly", () => { - expect(() => - arkenv(z.object({ PORT: z.coerce.number() }) as any, { - env: { PORT: "8080" }, - }), - ).toThrow(/arkenv\(\) expects a mapping/); - }); }); }); diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index a0a3f99f8..31b3c986f 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -79,22 +79,17 @@ export function defineEnv( */ export function arkenv( def: EnvSchema, + config?: ArkEnvConfig, +): distill.Out>; +export function arkenv( + def: T, + config?: ArkEnvConfig, +): InferType; +export function arkenv( + def: EnvSchema | EnvSchemaWithType, config: ArkEnvConfig = {}, -): distill.Out> { - // Guardrail: Ensure arkenv() only accepts an object map. - // We check for "~standard" (Standard Schema) or "assert" (compiled ArkType) - // which indicate the user passed a wrapped schema instead of an object map. - if ( - typeof def === "function" || - (def !== null && typeof def === "object" && "~standard" in def) - ) { - throw new Error( - "ArkEnv: arkenv() expects a mapping of { KEY: validator }, not a wrapped schema. " + - "Standard Schema validators are supported inside the mapping for migration, but not as the top-level argument.", - ); - } - - return defineEnv(def as any, config); +): distill.Out> | InferType { + return defineEnv(def as any, config) as any; } /** diff --git a/packages/arkenv/src/object-parsing.integration.test.ts b/packages/arkenv/src/object-parsing.integration.test.ts index 7ec75c7dc..25b6c3906 100644 --- a/packages/arkenv/src/object-parsing.integration.test.ts +++ b/packages/arkenv/src/object-parsing.integration.test.ts @@ -221,24 +221,23 @@ describe("object parsing integration", () => { }); }); - it("should work with objects in mapping", () => { - const env = arkenv( - { - CONFIG: { - host: "string", - port: "number", - }, - }, - { - env: { - CONFIG: '{"host": "localhost", "port": "3000"}', - }, + it("should parse an object when using a compiled ArkType schema", () => { + const schema = type({ + MY_OBJ: type({ + foo: "string", + bar: "number", + }), + }); + + const env = arkenv(schema, { + env: { + MY_OBJ: '{"foo": "baz", "bar": 123}', }, - ); + }); - expect(env.CONFIG).toEqual({ - host: "localhost", - port: 3000, + expect(env.MY_OBJ).toEqual({ + foo: "baz", + bar: 123, }); }); diff --git a/packages/arkenv/src/standard-schema.test.ts b/packages/arkenv/src/standard-schema.test.ts index 39340d3c1..074b88d2e 100644 --- a/packages/arkenv/src/standard-schema.test.ts +++ b/packages/arkenv/src/standard-schema.test.ts @@ -3,17 +3,20 @@ import { z } from "zod"; import { arkenv } from "./index"; describe("Standard Schema integration", () => { - it("should throw if a top-level zod schema is passed directly", () => { + it("should work with top-level zod schemas", () => { const schema = z.object({ PORT: z.coerce.number(), HOST: z.string().default("localhost"), }); - expect(() => - arkenv(schema as any, { - env: { PORT: "3000" }, - }), - ).toThrow(/arkenv\(\) expects a mapping/); + const env = arkenv(schema, { + env: { PORT: "3000" }, + }); + + expect(env).toEqual({ + PORT: 3000, + HOST: "localhost", + }); }); it("should support mixed validators in arkenv() mapping", () => { diff --git a/packages/bun-plugin/src/index.ts b/packages/bun-plugin/src/index.ts index a573f6a3b..09775b2dc 100644 --- a/packages/bun-plugin/src/index.ts +++ b/packages/bun-plugin/src/index.ts @@ -1,5 +1,5 @@ import { join } from "node:path"; -import type { SchemaShape } from "@repo/types"; +import type { EnvSchemaWithType, SchemaShape } from "@repo/types"; import type { EnvSchema } from "arkenv"; import { arkenv as validateEnv } from "arkenv"; import type { BunPlugin, Loader, PluginBuilder } from "bun"; @@ -10,7 +10,7 @@ export type { ProcessEnvAugmented } from "./types"; * Helper to process env schema and return envMap */ export function processEnvSchema( - options: EnvSchema, + options: EnvSchema | EnvSchemaWithType, ): Map { // Validate environment variables @@ -124,8 +124,12 @@ function registerLoader(build: PluginBuilder, envMap: Map) { * }) * ``` */ +export function arkenv(options: EnvSchemaWithType): BunPlugin; export function arkenv( options: EnvSchema, +): BunPlugin; +export function arkenv( + options: EnvSchema | EnvSchemaWithType, ): BunPlugin { const envMap = processEnvSchema(options); diff --git a/packages/vite-plugin/src/__fixtures__/basic/config.ts b/packages/vite-plugin/src/__fixtures__/basic/config.ts index 5823e0c88..47ebab974 100644 --- a/packages/vite-plugin/src/__fixtures__/basic/config.ts +++ b/packages/vite-plugin/src/__fixtures__/basic/config.ts @@ -1,6 +1,4 @@ -import { type } from "arkenv"; - -export const Env = type({ +export const Env = { VITE_API_URL: "string", VITE_DEBUG: "boolean", -}); +}; diff --git a/packages/vite-plugin/src/__fixtures__/with-env-dir/config.ts b/packages/vite-plugin/src/__fixtures__/with-env-dir/config.ts index ae43dd42a..b0fec826e 100644 --- a/packages/vite-plugin/src/__fixtures__/with-env-dir/config.ts +++ b/packages/vite-plugin/src/__fixtures__/with-env-dir/config.ts @@ -1,6 +1,4 @@ -import { type } from "arkenv"; - -export const Env = type({ +export const Env = { VITE_CUSTOM_VAR: "string", VITE_FROM_ENV_DIR: "string", -}); +}; diff --git a/packages/vite-plugin/src/index.ts b/packages/vite-plugin/src/index.ts index 717b9d069..e3e947d43 100644 --- a/packages/vite-plugin/src/index.ts +++ b/packages/vite-plugin/src/index.ts @@ -1,4 +1,4 @@ -import type { SchemaShape } from "@repo/types"; +import type { EnvSchemaWithType, SchemaShape } from "@repo/types"; import { type EnvSchema, arkenv as validateEnv } from "arkenv"; import { loadEnv, type Plugin } from "vite"; @@ -41,8 +41,12 @@ export type { ImportMetaEnvAugmented } from "./types"; * console.log(import.meta.env.VITE_API_URL); // Typesafe access * ``` */ +export default function arkenv(options: EnvSchemaWithType): Plugin; export default function arkenv( options: EnvSchema, +): Plugin; +export default function arkenv( + options: EnvSchema | EnvSchemaWithType, ): Plugin { return { name: "@arkenv/vite-plugin", From c2c69e58b24771570f4b8863777476128d3ca369 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 00:21:29 +0500 Subject: [PATCH 017/117] fix(arkenv): set ArkEnvError class name --- packages/arkenv/src/errors.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/arkenv/src/errors.ts b/packages/arkenv/src/errors.ts index 28db44aac..9525d4fd3 100644 --- a/packages/arkenv/src/errors.ts +++ b/packages/arkenv/src/errors.ts @@ -27,6 +27,8 @@ export class ArkEnvError extends Error { } } +Object.defineProperty(ArkEnvError, "name", { value: "ArkEnvError" }); + /** * @deprecated Use ArkEnvError.issues or normalized format. * Maintained for backward compatibility with existing tests and users. From 2b65905cda17a75f6c4d50013e25378cd49acf45 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 00:23:19 +0500 Subject: [PATCH 018/117] refactor(arkenv/errors): remove deprecated formatErrors function - Remove deprecated formatErrors function. - Update tests to use formatIssues directly. - Simplify error formatting logic. --- packages/arkenv/src/errors.test.ts | 11 +++++++++-- packages/arkenv/src/errors.ts | 15 --------------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/packages/arkenv/src/errors.test.ts b/packages/arkenv/src/errors.test.ts index fbfab5870..81a6c069e 100644 --- a/packages/arkenv/src/errors.test.ts +++ b/packages/arkenv/src/errors.test.ts @@ -1,6 +1,6 @@ import type { ArkErrors } from "arktype"; import { describe, expect, it } from "vitest"; -import { ArkEnvError, formatErrors } from "./errors"; +import { ArkEnvError, formatIssues } from "./errors"; /** * Define ArkErrorsForTest as a subset of ArkErrors @@ -15,7 +15,14 @@ type ArkErrorsForTest = { * @returns A string of the formatted errors */ const formatErrorsForTest = (errors: ArkErrorsForTest) => { - return formatErrors(errors as ArkErrors); + const issues = Object.entries(errors.byPath || {}).map( + ([path, error]: [string, any]) => ({ + path: path ? path.split(".") : [], + message: error.message, + validator: "arktype" as const, + }), + ); + return formatIssues(issues); }; /** diff --git a/packages/arkenv/src/errors.ts b/packages/arkenv/src/errors.ts index 9525d4fd3..10cf0843c 100644 --- a/packages/arkenv/src/errors.ts +++ b/packages/arkenv/src/errors.ts @@ -29,21 +29,6 @@ export class ArkEnvError extends Error { Object.defineProperty(ArkEnvError, "name", { value: "ArkEnvError" }); -/** - * @deprecated Use ArkEnvError.issues or normalized format. - * Maintained for backward compatibility with existing tests and users. - */ -export function formatErrors(errors: any): string { - const issues = Object.entries(errors.byPath || {}).map( - ([path, error]: [string, any]) => ({ - path: path ? path.split(".") : [], - message: error.message, - validator: "arktype" as const, - }), - ); - return formatIssues(issues); -} - function formatIssues(issues: EnvIssue[]): string { return issues .map((issue) => { From 2e880c35004df35b614e0ec08b143eb3fd0b3556 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 00:24:10 +0500 Subject: [PATCH 019/117] feat(arkenv): export formatIssues function - Add JSDoc to formatIssues function --- packages/arkenv/src/errors.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/arkenv/src/errors.ts b/packages/arkenv/src/errors.ts index 10cf0843c..1ee6c8a64 100644 --- a/packages/arkenv/src/errors.ts +++ b/packages/arkenv/src/errors.ts @@ -29,7 +29,12 @@ export class ArkEnvError extends Error { Object.defineProperty(ArkEnvError, "name", { value: "ArkEnvError" }); -function formatIssues(issues: EnvIssue[]): string { +/** + * Format the issues returned by ArkType for display + * @param issues - The issues returned by ArkType + * @returns A string of the formatted issues + */ +export function formatIssues(issues: EnvIssue[]): string { return issues .map((issue) => { const path = issue.path.length > 0 ? issue.path.join(".") : "root"; From ddf6f438854dbf93550103f258385911ed00460f Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 00:48:34 +0500 Subject: [PATCH 020/117] docs(arkenv): deprecate `createEnv` in favor of `arkenv` - Renamed `createEnv` to `arkenv` for consistency - Updated all internal and public --- apps/www/content/docs/arkenv/coercion.mdx | 2 +- .../arkenv/src/coercion.integration.test.ts | 32 ++--- packages/arkenv/src/create-env.test.ts | 113 +++++++++--------- packages/arkenv/src/create-env.ts | 24 +++- .../src/custom-types.integration.test.ts | 68 +++++------ packages/arkenv/src/error.integration.test.ts | 20 ++-- packages/arkenv/src/index.test.ts | 16 +-- packages/arkenv/src/index.ts | 6 +- .../src/object-parsing.integration.test.ts | 28 ++--- packages/arkenv/src/standard-schema.test.ts | 15 +-- 10 files changed, 169 insertions(+), 155 deletions(-) diff --git a/apps/www/content/docs/arkenv/coercion.mdx b/apps/www/content/docs/arkenv/coercion.mdx index e62031672..0dd76de9e 100644 --- a/apps/www/content/docs/arkenv/coercion.mdx +++ b/apps/www/content/docs/arkenv/coercion.mdx @@ -94,7 +94,7 @@ Coercion works recursively: even variables inside a JSON object are coerced to t ## How it works -ArkEnv introspects your schema to identify fields that should be numbers, booleans, and other non-string types you define. When you call `createEnv()` (or `arkenv()`), it pre-processes your environment variables to perform these conversions *before* validation. +ArkEnv introspects your schema to identify fields that should be numbers, booleans, and other non-string types you define. When you call `arkenv()`, it pre-processes your environment variables to perform these conversions *before* validation. ## Performance diff --git a/packages/arkenv/src/coercion.integration.test.ts b/packages/arkenv/src/coercion.integration.test.ts index 649d9a63b..5794c4e93 100644 --- a/packages/arkenv/src/coercion.integration.test.ts +++ b/packages/arkenv/src/coercion.integration.test.ts @@ -1,16 +1,16 @@ import { describe, expect, it } from "vitest"; -import { arkenv, createEnv } from "./create-env"; +import { arkenv } from "./create-env"; import { type } from "./index"; describe("coercion integration", () => { it("should coerce and validate numbers", () => { - const env = createEnv({ PORT: "number" }, { env: { PORT: "3000" } }); + const env = arkenv({ PORT: "number" }, { env: { PORT: "3000" } }); expect(env.PORT).toBe(3000); expect(typeof env.PORT).toBe("number"); }); it("should coerce and validate booleans", () => { - const env = createEnv( + const env = arkenv( { DEBUG: "boolean", VERBOSE: "boolean" }, { env: { DEBUG: "true", VERBOSE: "false" } }, ); @@ -19,19 +19,19 @@ describe("coercion integration", () => { }); it("should coerce and validate number subtypes (port)", () => { - const env = createEnv({ PORT: "number.port" }, { env: { PORT: "8080" } }); + const env = arkenv({ PORT: "number.port" }, { env: { PORT: "8080" } }); expect(env.PORT).toBe(8080); }); it("should fail validation if coercion fails (not a number)", () => { expect(() => - createEnv({ PORT: "number" }, { env: { PORT: "abc" } }), + arkenv({ PORT: "number" }, { env: { PORT: "abc" } }), ).toThrow(); }); it("should fail validation if value is valid number but invalid subtype", () => { expect(() => - createEnv( + arkenv( { PORT: "number.port" }, { env: { PORT: "99999" } }, // Too large for port ), @@ -39,7 +39,7 @@ describe("coercion integration", () => { }); it("should work with mixed coerced and non-coerced values", () => { - const env = createEnv( + const env = arkenv( { PORT: "number", HOST: "string", @@ -84,7 +84,7 @@ describe("coercion integration", () => { it("should work with other number sub-keywords like epoch", () => { const ts = "1678886400000"; - const env = createEnv({ TS: "number.epoch" }, { env: { TS: ts } }); + const env = arkenv({ TS: "number.epoch" }, { env: { TS: ts } }); expect(env.TS).toBe(1678886400000); }); @@ -101,28 +101,28 @@ describe("coercion integration", () => { }); it("should coerce and validate strict number literals", () => { - const env = createEnv({ VAL: "1 | 2" }, { env: { VAL: "1" } }); + const env = arkenv({ VAL: "1 | 2" }, { env: { VAL: "1" } }); expect(env.VAL).toBe(1); }); it("should coerce and validate strict boolean literals", () => { - const env = createEnv({ DEBUG: "true" }, { env: { DEBUG: "true" } }); + const env = arkenv({ DEBUG: "true" }, { env: { DEBUG: "true" } }); expect(env.DEBUG).toBe(true); }); it("should NOT coerce empty or whitespace strings to 0 for numbers", () => { - expect(() => createEnv({ VAL: "number" }, { env: { VAL: "" } })).toThrow(); + expect(() => arkenv({ VAL: "number" }, { env: { VAL: "" } })).toThrow(); expect(() => - createEnv({ VAL: "number" }, { env: { VAL: " " } }), + arkenv({ VAL: "number" }, { env: { VAL: " " } }), ).toThrow(); }); it("should fail validation if coercion fails (not a boolean)", () => { expect(() => - createEnv({ DEBUG: "boolean" }, { env: { DEBUG: "yes" } }), + arkenv({ DEBUG: "boolean" }, { env: { DEBUG: "yes" } }), ).toThrow(); expect(() => - createEnv({ DEBUG: "boolean" }, { env: { DEBUG: "1" } }), + arkenv({ DEBUG: "boolean" }, { env: { DEBUG: "1" } }), ).toThrow(); }); @@ -146,7 +146,7 @@ describe("coercion integration", () => { }); it("should handle mixed features: defaults, coercion, array format, and stripping", () => { - const env = createEnv( + const env = arkenv( { // Default used if missing DEFAULT_ARR: type("string[]").default(() => ["default"]), @@ -178,7 +178,7 @@ describe("coercion integration", () => { it("should provide actionable error message for array parsing failure", () => { expect(() => { - createEnv( + arkenv( { TAGS: "string[]" }, { env: { TAGS: '["invalid-json' }, diff --git a/packages/arkenv/src/create-env.test.ts b/packages/arkenv/src/create-env.test.ts index 251e331d4..be1549728 100644 --- a/packages/arkenv/src/create-env.test.ts +++ b/packages/arkenv/src/create-env.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { z } from "zod"; -import { arkenv, createEnv } from "./create-env"; +import { arkenv } from "./create-env"; import { type } from "./type"; import { indent, styleText } from "./utils"; @@ -25,7 +25,7 @@ const expectedError = ( return formattedErrors.join("\n"); }; -describe("createEnv", () => { +describe("arkenv", () => { let originalEnv: NodeJS.ProcessEnv; beforeEach(() => { @@ -37,13 +37,13 @@ describe("createEnv", () => { }); describe("coercion", () => { it("should coerce number from string", () => { - const env = createEnv({ PORT: "number" }, { env: { PORT: "3000" } }); + const env = arkenv({ PORT: "number" }, { env: { PORT: "3000" } }); expect(env.PORT).toBe(3000); expect(typeof env.PORT).toBe("number"); }); it("should coerce boolean from string", () => { - const env = createEnv( + const env = arkenv( { DEBUG: "boolean", VERBOSE: "boolean" }, { env: { DEBUG: "true", VERBOSE: "false" } }, ); @@ -52,7 +52,7 @@ describe("createEnv", () => { }); it("should coerce number.integer from string", () => { - const env = createEnv( + const env = arkenv( { COUNT: "number.integer" }, { env: { COUNT: "123" } }, ); @@ -60,19 +60,19 @@ describe("createEnv", () => { }); it("should coerce numeric ranges from string", () => { - const env = createEnv({ AGE: "number >= 18" }, { env: { AGE: "21" } }); + const env = arkenv({ AGE: "number >= 18" }, { env: { AGE: "21" } }); expect(env.AGE).toBe(21); }); it("should coerce numeric divisors from string", () => { - const env = createEnv({ EVEN: "number % 2" }, { env: { EVEN: "4" } }); + const env = arkenv({ EVEN: "number % 2" }, { env: { EVEN: "4" } }); expect(env.EVEN).toBe(4); }); it("should work with optional coerced properties", () => { const schema = { "PORT?": "number" } as const; - expect(createEnv(schema, { env: { PORT: "3000" } }).PORT).toBe(3000); - expect(createEnv(schema, { env: {} }).PORT).toBeUndefined(); + expect(arkenv(schema, { env: { PORT: "3000" } }).PORT).toBe(3000); + expect(arkenv(schema, { env: {} }).PORT).toBeUndefined(); }); it("should handle validation errors from mapping", () => { @@ -107,12 +107,12 @@ describe("createEnv", () => { describe("numeric keywords", () => { it("should coerce number", () => { - const env = createEnv({ VAL: "number" }, { env: { VAL: "123.456" } }); + const env = arkenv({ VAL: "number" }, { env: { VAL: "123.456" } }); expect(env.VAL).toBe(123.456); }); it("should coerce number.Infinity", () => { - const env = createEnv( + const env = arkenv( { VAL: "number.Infinity" }, { env: { VAL: "Infinity" } }, ); @@ -121,12 +121,12 @@ describe("createEnv", () => { // TODO: Support NaN coercion // it("should coerce number.NaN", () => { - // const env = createEnv({ VAL: "number.NaN" }, { VAL: "NaN" }); + // const env = arkenv({ VAL: "number.NaN" }, { VAL: "NaN" }); // expect(env.VAL).toBeNaN(); // }); it("should coerce number.NegativeInfinity", () => { - const env = createEnv( + const env = arkenv( { VAL: "number.NegativeInfinity" }, { env: { VAL: "-Infinity" } }, ); @@ -134,7 +134,7 @@ describe("createEnv", () => { }); it("should coerce number.epoch", () => { - const env = createEnv( + const env = arkenv( { VAL: "number.epoch" }, { env: { VAL: "1640995200000" } }, ); @@ -142,12 +142,12 @@ describe("createEnv", () => { }); it("should coerce number.integer", () => { - const env = createEnv({ VAL: "number.integer" }, { env: { VAL: "42" } }); + const env = arkenv({ VAL: "number.integer" }, { env: { VAL: "42" } }); expect(env.VAL).toBe(42); }); it("should coerce number.safe", () => { - const env = createEnv( + const env = arkenv( { VAL: "number.safe" }, { env: { VAL: "9007199254740991" } }, ); @@ -157,36 +157,36 @@ describe("createEnv", () => { describe("defaults and empty strings", () => { it("should use defaults when value is missing", () => { - const env = createEnv({ FOO: "string = 'bar'" }, { env: {} }); + const env = arkenv({ FOO: "string = 'bar'" }, { env: {} }); expect(env.FOO).toBe("bar"); }); it("should treat empty string as empty string for string types", () => { - const env = createEnv({ VAL: "string" }, { env: { VAL: "" } }); + const env = arkenv({ VAL: "string" }, { env: { VAL: "" } }); expect(env.VAL).toBe(""); }); it("should throw for empty string when number is expected", () => { expect(() => - createEnv({ VAL: "number" }, { env: { VAL: "" } }), + arkenv({ VAL: "number" }, { env: { VAL: "" } }), ).toThrow(); }); it("should throw for empty string when boolean is expected", () => { expect(() => - createEnv({ VAL: "boolean" }, { env: { VAL: "" } }), + arkenv({ VAL: "boolean" }, { env: { VAL: "" } }), ).toThrow(); }); it("should allow empty strings when the schema is unknown", () => { - const env = createEnv({ VAL: "unknown" }, { env: { VAL: "" } }); + const env = arkenv({ VAL: "unknown" }, { env: { VAL: "" } }); expect(env.VAL).toBe(""); }); }); describe("standard array syntax", () => { it("should parse string[] from comma-separated string", () => { - const env = createEnv( + const env = arkenv( { TAGS: "string[]" }, { env: { TAGS: "foo,bar,baz" } }, ); @@ -194,7 +194,7 @@ describe("createEnv", () => { }); it("should parse number[] from comma-separated string", () => { - const env = createEnv( + const env = arkenv( { PORTS: "number[]" }, { env: { PORTS: "3000, 8080" } }, ); @@ -202,7 +202,7 @@ describe("createEnv", () => { }); it("should parse boolean[] from comma-separated string", () => { - const env = createEnv( + const env = arkenv( { FLAGS: "boolean[]" }, { env: { FLAGS: "true, false" } }, ); @@ -210,7 +210,7 @@ describe("createEnv", () => { }); it("should parse (string|number)[] from mixed string", () => { - const env = createEnv( + const env = arkenv( { MIXED: "(string|number)[]" }, { env: { MIXED: "foo, 123, bar" } }, ); @@ -218,7 +218,7 @@ describe("createEnv", () => { }); it("should parse (string|number)[] with only numbers", () => { - const env = createEnv( + const env = arkenv( { MIXED: "(string|number)[]" }, { env: { MIXED: "1, 2, 3" } }, ); @@ -226,7 +226,7 @@ describe("createEnv", () => { }); it("should parse (string|boolean)[] with mixed values", () => { - const env = createEnv( + const env = arkenv( { MIXED: "(string|boolean)[]" }, { env: { MIXED: "true, foo, false" } }, ); @@ -237,7 +237,7 @@ describe("createEnv", () => { it("should validate string env variables", () => { process.env.TEST_STRING = "hello"; - const env = createEnv({ + const env = arkenv({ TEST_STRING: "string", }); @@ -246,7 +246,7 @@ describe("createEnv", () => { it("should throw when required env variable is missing", () => { expect(() => - createEnv({ + arkenv({ MISSING_VAR: "string", }), ).toThrow( @@ -264,7 +264,7 @@ describe("createEnv", () => { process.env.WRONG_TYPE = "not a number"; expect(() => - createEnv({ + arkenv({ WRONG_TYPE: "number", }), ).toThrow( @@ -283,7 +283,7 @@ describe("createEnv", () => { TEST_STRING: "hello", }; - const { TEST_STRING } = createEnv( + const { TEST_STRING } = arkenv( { TEST_STRING: "string", }, @@ -294,7 +294,7 @@ describe("createEnv", () => { }); it("should support array types with default values", () => { - const env = createEnv( + const env = arkenv( { NUMBERS: type("number[]").default(() => [1, 2, 3]), STRINGS: type("string[]").default(() => ["a", "b"]), @@ -308,7 +308,7 @@ describe("createEnv", () => { it("should support array types with defaults when no environment value provided", () => { // Test default value usage when environment variable is not set - const env = createEnv( + const env = arkenv( { NUMBERS: type("number[]").default(() => [1, 2, 3]), }, @@ -391,7 +391,7 @@ describe("createEnv", () => { describe("options", () => { it("should disable coercion when coerce is set to false", () => { expect(() => - createEnv( + arkenv( { NUMBER: "number", }, @@ -410,7 +410,7 @@ describe("createEnv", () => { process.env = { ...originalEnv, TEST_NUM: "123" }; try { expect(() => - createEnv({ TEST_NUM: "number" }, { coerce: false }), + arkenv({ TEST_NUM: "number" }, { coerce: false }), ).toThrow(); } finally { process.env = originalEnv; @@ -418,7 +418,7 @@ describe("createEnv", () => { }); it("should allow string values when coercion is disabled if schema expects strings", () => { - const env = createEnv( + const env = arkenv( { VAL: "string", }, @@ -433,7 +433,7 @@ describe("createEnv", () => { }); it("should strip extra keys that are not defined in the schema by default", () => { - const env = createEnv( + const env = arkenv( { HOST: "string" }, { env: { @@ -447,7 +447,7 @@ describe("createEnv", () => { }); it("should preserve extra keys when onUndeclaredKey is set to 'ignore'", () => { - const env = createEnv( + const env = arkenv( { HOST: "string" }, { env: { @@ -462,7 +462,7 @@ describe("createEnv", () => { it("should throw when onUndeclaredKey is set to 'reject' and extra keys are present", () => { expect(() => - createEnv( + arkenv( { HOST: "string" }, { env: { @@ -476,7 +476,7 @@ describe("createEnv", () => { }); it("should explicitly delete extra keys when onUndeclaredKey is set to 'delete'", () => { - const env = createEnv( + const env = arkenv( { HOST: "string" }, { env: { @@ -493,7 +493,7 @@ describe("createEnv", () => { describe("array format configuration", () => { it("should parse arrays as JSON when arrayFormat is 'json'", () => { - const env = createEnv( + const env = arkenv( { TAGS: "string[]" }, { env: { TAGS: '["foo", "bar"]' }, @@ -505,7 +505,7 @@ describe("createEnv", () => { it("should fail validation if arrayFormat is 'json' but value is not valid JSON array", () => { expect(() => { - createEnv( + arkenv( { TAGS: "string[]" }, { env: { TAGS: "foo,bar" }, // Comma separated, not JSON @@ -516,7 +516,7 @@ describe("createEnv", () => { }); it("should default to comma separation", () => { - const env = createEnv( + const env = arkenv( { TAGS: "string[]" }, { env: { TAGS: "foo,bar" }, @@ -527,7 +527,7 @@ describe("createEnv", () => { }); it("should parse numeric arrays as JSON", () => { - const env = createEnv( + const env = arkenv( { NUMBERS: "number[]" }, { env: { NUMBERS: "[1, 2, 3]" }, @@ -538,12 +538,12 @@ describe("createEnv", () => { }); it("should handle empty comma-separated string", () => { - const env = createEnv({ TAGS: "string[]" }, { env: { TAGS: "" } }); + const env = arkenv({ TAGS: "string[]" }, { env: { TAGS: "" } }); expect(env.TAGS).toEqual([]); }); it("should handle single-element array", () => { - const env = createEnv( + const env = arkenv( { TAGS: "string[]" }, { env: { TAGS: "only-one" } }, ); @@ -551,7 +551,7 @@ describe("createEnv", () => { }); it("should handle empty JSON array", () => { - const env = createEnv( + const env = arkenv( { TAGS: "string[]" }, { env: { TAGS: "[]" }, @@ -564,7 +564,7 @@ describe("createEnv", () => { describe("object coercion", () => { it("should parse an object from a JSON string", () => { - const env = createEnv( + const env = arkenv( { DATABASE: { HOST: "string", PORT: "number" } }, { env: { @@ -578,7 +578,7 @@ describe("createEnv", () => { }); it("should handle nested object coercion", () => { - const env = createEnv( + const env = arkenv( { CONFIG: { DB: { PORT: "number" }, APP: { NAME: "string" } } }, { env: { @@ -594,7 +594,7 @@ describe("createEnv", () => { it("should fail validation if object is not valid JSON", () => { expect(() => { - createEnv( + arkenv( { DATABASE: { HOST: "string" } }, { env: { DATABASE: '{"HOST": "localhost"' } }, // Missing closing brace ); @@ -602,7 +602,7 @@ describe("createEnv", () => { }); it("should parse objects within arrays", () => { - const env = createEnv( + const env = arkenv( { SERVICES: type({ NAME: "string", PORT: "number" }).array() }, { env: { @@ -628,11 +628,12 @@ describe("createEnv", () => { expect(env.PORT).toBe(3000); }); - it("should work with top-level Standard Schema", () => { - const env = arkenv(z.object({ PORT: z.coerce.number() }), { - env: { PORT: "8080" }, - }); - expect(env.PORT).toBe(8080); + it("should throw if top-level Standard Schema is passed directly", () => { + expect(() => + arkenv(z.object({ PORT: z.coerce.number() }) as any, { + env: { PORT: "8080" }, + }), + ).toThrow(/expects a mapping.*not a top-level Standard Schema/); }); it("should support mixed ArkType DSL and Standard Schema validators", () => { diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 31b3c986f..566ac7b4a 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -37,10 +37,10 @@ export type ArkEnvConfig = { }; /** - * Strict Standard Schema entry point. - * @private - Supporting internal modules and plugins that import pre-defined validators. + * Internal Standard Schema entry point. + * @internal */ -export function defineEnv( +function defineEnv( def: T, config: ArkEnvConfig = {}, ): InferType { @@ -89,10 +89,26 @@ export function arkenv( def: EnvSchema | EnvSchemaWithType, config: ArkEnvConfig = {}, ): distill.Out> | InferType { + const isStandard = !!(def as any)?.["~standard"]; + const isArkCompiled = + (typeof def === "function" && "assert" in (def as any)) || + (typeof def === "object" && def !== null && "invoke" in (def as any)); + + // Guardrail: Block top-level Standard Schema (Zod, Valibot, etc.) + // Reusable type() schemas (ArkType) are allowed. + if (isStandard && !isArkCompiled) { + throw new Error( + "ArkEnv: arkenv() expects a mapping of { KEY: validator }, not a top-level Standard Schema (e.g. z.object()). " + + "Standard Schema validators are supported inside the mapping, or you can use ArkType's type() for top-level schemas.", + ); + } + return defineEnv(def as any, config) as any; } /** * @deprecated Use `arkenv` instead. */ -export const createEnv = arkenv; +const createEnv = arkenv; + +export { createEnv }; diff --git a/packages/arkenv/src/custom-types.integration.test.ts b/packages/arkenv/src/custom-types.integration.test.ts index bc3e0bfc9..d17c888f3 100644 --- a/packages/arkenv/src/custom-types.integration.test.ts +++ b/packages/arkenv/src/custom-types.integration.test.ts @@ -1,38 +1,38 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { createEnv } from "./create-env"; +import { arkenv } from "./create-env"; import { type } from "./type"; -describe("createEnv + type + scope + types integration", () => { +describe("arkenv + type + scope + types integration", () => { afterEach(() => { vi.unstubAllEnvs(); }); describe("string.host integration", () => { - it("should validate localhost through createEnv", () => { + it("should validate localhost through arkenv", () => { vi.stubEnv("HOST", "localhost"); - const env = createEnv({ + const env = arkenv({ HOST: type("string.host"), }); expect(env.HOST).toBe("localhost"); }); - it("should validate IP address through createEnv", () => { + it("should validate IP address through arkenv", () => { vi.stubEnv("HOST", "127.0.0.1"); - const env = createEnv({ + const env = arkenv({ HOST: type("string.host"), }); expect(env.HOST).toBe("127.0.0.1"); }); - it("should throw ArkEnvError for invalid host through createEnv", () => { + it("should throw ArkEnvError for invalid host through arkenv", () => { vi.stubEnv("HOST", "invalid-host"); expect(() => - createEnv({ + arkenv({ HOST: type("string.host"), }), ).toThrow(/HOST/); @@ -40,10 +40,10 @@ describe("createEnv + type + scope + types integration", () => { }); describe("number.port integration", () => { - it("should validate valid port through createEnv", () => { + it("should validate valid port through arkenv", () => { vi.stubEnv("PORT", "8080"); - const env = createEnv({ + const env = arkenv({ PORT: type("number.port"), }); @@ -51,41 +51,41 @@ describe("createEnv + type + scope + types integration", () => { expect(typeof env.PORT).toBe("number"); }); - it("should validate port at boundary (0) through createEnv", () => { + it("should validate port at boundary (0) through arkenv", () => { vi.stubEnv("PORT", "0"); - const env = createEnv({ + const env = arkenv({ PORT: type("number.port"), }); expect(env.PORT).toBe(0); }); - it("should validate port at boundary (65535) through createEnv", () => { + it("should validate port at boundary (65535) through arkenv", () => { vi.stubEnv("PORT", "65535"); - const env = createEnv({ + const env = arkenv({ PORT: type("number.port"), }); expect(env.PORT).toBe(65535); }); - it("should throw ArkEnvError for invalid port through createEnv", () => { + it("should throw ArkEnvError for invalid port through arkenv", () => { vi.stubEnv("PORT", "99999"); expect(() => - createEnv({ + arkenv({ PORT: type("number.port"), }), ).toThrow(/PORT/); }); - it("should throw ArkEnvError for non-numeric port through createEnv", () => { + it("should throw ArkEnvError for non-numeric port through arkenv", () => { vi.stubEnv("PORT", "not-a-number"); expect(() => - createEnv({ + arkenv({ PORT: type("number.port"), }), ).toThrow(/PORT/); @@ -93,10 +93,10 @@ describe("createEnv + type + scope + types integration", () => { }); describe("boolean integration", () => { - it("should validate 'true' string through createEnv", () => { + it("should validate 'true' string through arkenv", () => { vi.stubEnv("DEBUG", "true"); - const env = createEnv({ + const env = arkenv({ DEBUG: type("boolean"), }); @@ -104,21 +104,21 @@ describe("createEnv + type + scope + types integration", () => { expect(typeof env.DEBUG).toBe("boolean"); }); - it("should validate 'false' string through createEnv", () => { + it("should validate 'false' string through arkenv", () => { vi.stubEnv("DEBUG", "false"); - const env = createEnv({ + const env = arkenv({ DEBUG: type("boolean"), }); expect(env.DEBUG).toBe(false); }); - it("should throw ArkEnvError for invalid boolean through createEnv", () => { + it("should throw ArkEnvError for invalid boolean through arkenv", () => { vi.stubEnv("DEBUG", "maybe"); expect(() => - createEnv({ + arkenv({ DEBUG: type("boolean"), }), ).toThrow(/DEBUG/); @@ -131,7 +131,7 @@ describe("createEnv + type + scope + types integration", () => { vi.stubEnv("PORT", "3000"); vi.stubEnv("DEBUG", "true"); - const env = createEnv({ + const env = arkenv({ HOST: type("string.host"), PORT: type("number.port"), DEBUG: type("boolean"), @@ -147,7 +147,7 @@ describe("createEnv + type + scope + types integration", () => { vi.stubEnv("PORT", "8080"); vi.stubEnv("DEBUG", "false"); - const env = createEnv({ + const env = arkenv({ HOST: type("string.host"), PORT: type("number.port"), DEBUG: type("boolean"), @@ -164,7 +164,7 @@ describe("createEnv + type + scope + types integration", () => { vi.stubEnv("DEBUG", "true"); expect(() => - createEnv({ + arkenv({ HOST: type("string.host"), PORT: type("number.port"), DEBUG: type("boolean"), @@ -178,7 +178,7 @@ describe("createEnv + type + scope + types integration", () => { vi.stubEnv("DEBUG", "maybe"); expect(() => - createEnv({ + arkenv({ HOST: type("string.host"), PORT: type("number.port"), DEBUG: type("boolean"), @@ -189,7 +189,7 @@ describe("createEnv + type + scope + types integration", () => { describe("custom types with defaults", () => { it("should use default value for host when missing", () => { - const env = createEnv({ + const env = arkenv({ HOST: type("string.host").default(() => "localhost"), }); @@ -197,7 +197,7 @@ describe("createEnv + type + scope + types integration", () => { }); it("should use default value for port when missing", () => { - const env = createEnv({ + const env = arkenv({ PORT: type("number.port").default(3000), }); @@ -205,7 +205,7 @@ describe("createEnv + type + scope + types integration", () => { }); it("should use default value for boolean when missing", () => { - const env = createEnv({ + const env = arkenv({ DEBUG: type("boolean").default(() => false), }); @@ -215,7 +215,7 @@ describe("createEnv + type + scope + types integration", () => { it("should validate custom type when provided instead of using default", () => { vi.stubEnv("HOST", "127.0.0.1"); - const env = createEnv({ + const env = arkenv({ HOST: type("string.host").default(() => "localhost"), }); @@ -224,11 +224,11 @@ describe("createEnv + type + scope + types integration", () => { }); describe("custom types in arrays", () => { - it("should handle custom types in arrays through createEnv", () => { + it("should handle custom types in arrays through arkenv", () => { vi.stubEnv("EMAILS", "test@example.com, admin@example.com"); const Email = type("string.email"); - const env = createEnv({ EMAILS: Email.array() }); + const env = arkenv({ EMAILS: Email.array() }); expect(env.EMAILS).toEqual(["test@example.com", "admin@example.com"]); }); }); diff --git a/packages/arkenv/src/error.integration.test.ts b/packages/arkenv/src/error.integration.test.ts index ae52e8dad..39c2eaeb0 100644 --- a/packages/arkenv/src/error.integration.test.ts +++ b/packages/arkenv/src/error.integration.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { createEnv } from "./create-env"; +import { arkenv } from "./create-env"; import { ArkEnvError } from "./errors"; import { type } from "./type"; @@ -7,7 +7,7 @@ import { type } from "./type"; const stripAnsi = (str: string) => str.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g"), ""); -describe("createEnv + type + errors + utils integration", () => { +describe("arkenv + type + errors + utils integration", () => { afterEach(() => { vi.unstubAllEnvs(); }); @@ -17,7 +17,7 @@ describe("createEnv + type + errors + utils integration", () => { vi.stubEnv("PORT", "not-a-number"); try { - createEnv({ + arkenv({ PORT: type("number.port"), }); expect.fail("Should have thrown"); @@ -38,7 +38,7 @@ describe("createEnv + type + errors + utils integration", () => { vi.stubEnv("HOST", "invalid-host"); try { - createEnv({ + arkenv({ HOST: type("string.host"), }); expect.fail("Should have thrown"); @@ -56,7 +56,7 @@ describe("createEnv + type + errors + utils integration", () => { vi.stubEnv("DEBUG", "maybe"); try { - createEnv({ + arkenv({ DEBUG: type("boolean"), }); expect.fail("Should have thrown"); @@ -73,7 +73,7 @@ describe("createEnv + type + errors + utils integration", () => { it("should throw ArkEnvError with formatted message for missing required variable", () => { try { - createEnv({ + arkenv({ REQUIRED_VAR: "string", }); expect.fail("Should have thrown"); @@ -94,7 +94,7 @@ describe("createEnv + type + errors + utils integration", () => { vi.stubEnv("DEBUG", "maybe"); try { - createEnv({ + arkenv({ HOST: type("string.host"), PORT: type("number.port"), DEBUG: type("boolean"), @@ -117,7 +117,7 @@ describe("createEnv + type + errors + utils integration", () => { vi.stubEnv("PORT", "not-a-number"); try { - createEnv({ + arkenv({ PORT: type("number.port"), }); expect.fail("Should have thrown"); @@ -148,7 +148,7 @@ describe("createEnv + type + errors + utils integration", () => { vi.stubEnv("PORT", invalidValue); try { - createEnv({ + arkenv({ PORT: type("number.port"), }); expect.fail("Should have thrown"); @@ -167,7 +167,7 @@ describe("createEnv + type + errors + utils integration", () => { vi.stubEnv("PORT", "99999"); try { - createEnv({ + arkenv({ HOST: type("string.host"), PORT: type("number.port"), }); diff --git a/packages/arkenv/src/index.test.ts b/packages/arkenv/src/index.test.ts index 083de3368..e92a25405 100644 --- a/packages/arkenv/src/index.test.ts +++ b/packages/arkenv/src/index.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, expectTypeOf, it, vi } from "vitest"; -import arkenv, { createEnv } from "./index"; +import arkenv, { arkenv } from "./index"; describe("index.ts exports", () => { afterEach(() => { @@ -8,18 +8,18 @@ describe("index.ts exports", () => { vi.unstubAllEnvs(); }); - it("should export createEnv as default export", () => { - expect(arkenv).toBe(createEnv); + it("should export arkenv as default export", () => { + expect(arkenv).toBe(arkenv); expect(typeof arkenv).toBe("function"); }); it("should have correct types for exported functions", () => { // Type assertion to verify exported function types expectTypeOf(arkenv).toBeFunction(); - expectTypeOf(createEnv).toBeFunction(); + expectTypeOf(arkenv).toBeFunction(); // Verify they have the same type signature - expectTypeOf(arkenv).toEqualTypeOf(createEnv); + expectTypeOf(arkenv).toEqualTypeOf(arkenv); }); it("should work with default import", () => { @@ -38,7 +38,7 @@ describe("index.ts exports", () => { // Set test environment variable vi.stubEnv("TEST_NAMED_IMPORT", "test-value"); - const env = createEnv({ + const env = arkenv({ TEST_NAMED_IMPORT: "string", }); @@ -56,7 +56,7 @@ describe("index.ts exports", () => { it("should throw error with named import when validation fails", () => { expect(() => - createEnv({ + arkenv({ MISSING_NAMED_VAR: "string", }), ).toThrow(); @@ -70,7 +70,7 @@ describe("index.ts exports", () => { COMPARISON_TEST: "string", }); - const envFromNamed = createEnv({ + const envFromNamed = arkenv({ COMPARISON_TEST: "string", }); diff --git a/packages/arkenv/src/index.ts b/packages/arkenv/src/index.ts index 7cda44547..88b9f8f5b 100644 --- a/packages/arkenv/src/index.ts +++ b/packages/arkenv/src/index.ts @@ -1,13 +1,13 @@ -import { arkenv, createEnv } from "./create-env"; +import { arkenv } from "./create-env"; export type { EnvSchema } from "./create-env"; /** - * `arkenv`'s main export, an alias for {@link createEnv} + * `arkenv`'s primary entry point. * * {@link https://arkenv.js.org | ArkEnv} is a typesafe environment variables validator from editor to runtime. */ export default arkenv; -export { arkenv, createEnv } from "./create-env"; +export { arkenv }; export { ArkEnvError } from "./errors"; export { type } from "./type"; diff --git a/packages/arkenv/src/object-parsing.integration.test.ts b/packages/arkenv/src/object-parsing.integration.test.ts index 25b6c3906..c905527a7 100644 --- a/packages/arkenv/src/object-parsing.integration.test.ts +++ b/packages/arkenv/src/object-parsing.integration.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it } from "vitest"; -import { arkenv, createEnv } from "./create-env"; +import { arkenv } from "./create-env"; import { type } from "./index"; describe("object parsing integration", () => { it("should parse JSON string into object with string properties", () => { - const env = createEnv( + const env = arkenv( { CONFIG: { host: "string", @@ -25,7 +25,7 @@ describe("object parsing integration", () => { }); it("should parse JSON string and coerce nested number properties", () => { - const env = createEnv( + const env = arkenv( { DATABASE: { host: "string", @@ -47,7 +47,7 @@ describe("object parsing integration", () => { }); it("should parse JSON string and coerce nested boolean properties", () => { - const env = createEnv( + const env = arkenv( { SETTINGS: { enabled: "boolean", @@ -70,7 +70,7 @@ describe("object parsing integration", () => { }); it("should parse nested JSON objects", () => { - const env = createEnv( + const env = arkenv( { APP: { database: { @@ -101,7 +101,7 @@ describe("object parsing integration", () => { }); it("should handle object with mixed types", () => { - const env = createEnv( + const env = arkenv( { CONFIG: { host: "string", @@ -127,7 +127,7 @@ describe("object parsing integration", () => { }); it("should work with both JSON strings and separate environment variables", () => { - const env = createEnv( + const env = arkenv( { PORT: "number", DEBUG: "boolean", @@ -155,7 +155,7 @@ describe("object parsing integration", () => { it("should fail when JSON string is invalid", () => { expect(() => - createEnv( + arkenv( { CONFIG: { host: "string", @@ -172,7 +172,7 @@ describe("object parsing integration", () => { it("should fail when parsed object doesn't match schema", () => { expect(() => - createEnv( + arkenv( { CONFIG: { port: "number", @@ -188,7 +188,7 @@ describe("object parsing integration", () => { }); it("should work with optional object properties", () => { - const env = createEnv( + const env = arkenv( { "CONFIG?": { host: "string", @@ -203,7 +203,7 @@ describe("object parsing integration", () => { }); it("should parse optional object when provided", () => { - const env = createEnv( + const env = arkenv( { "CONFIG?": { host: "string", @@ -242,7 +242,7 @@ describe("object parsing integration", () => { }); it("should handle arrays within JSON objects", () => { - const env = createEnv( + const env = arkenv( { CONFIG: { hosts: "string[]", @@ -264,7 +264,7 @@ describe("object parsing integration", () => { }); it("should parse JSON with whitespace", () => { - const env = createEnv( + const env = arkenv( { CONFIG: { host: "string", @@ -283,7 +283,7 @@ describe("object parsing integration", () => { }); it("should handle object with default values", () => { - const env = createEnv( + const env = arkenv( { CONFIG: type({ host: "string", diff --git a/packages/arkenv/src/standard-schema.test.ts b/packages/arkenv/src/standard-schema.test.ts index 074b88d2e..02a5b0657 100644 --- a/packages/arkenv/src/standard-schema.test.ts +++ b/packages/arkenv/src/standard-schema.test.ts @@ -3,20 +3,17 @@ import { z } from "zod"; import { arkenv } from "./index"; describe("Standard Schema integration", () => { - it("should work with top-level zod schemas", () => { + it("should throw if a top-level zod schema is passed directly", () => { const schema = z.object({ PORT: z.coerce.number(), HOST: z.string().default("localhost"), }); - const env = arkenv(schema, { - env: { PORT: "3000" }, - }); - - expect(env).toEqual({ - PORT: 3000, - HOST: "localhost", - }); + expect(() => + arkenv(schema as any, { + env: { PORT: "3000" }, + }), + ).toThrow(/expects a mapping.*not a top-level Standard Schema/); }); it("should support mixed validators in arkenv() mapping", () => { From 46d7032c2901a632fa592b4587ba3b0b248fecbd Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 00:48:56 +0500 Subject: [PATCH 021/117] style(test): format test expectations --- packages/arkenv/src/coercion.integration.test.ts | 4 +--- packages/arkenv/src/create-env.test.ts | 13 +++---------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/packages/arkenv/src/coercion.integration.test.ts b/packages/arkenv/src/coercion.integration.test.ts index 5794c4e93..b22366b3a 100644 --- a/packages/arkenv/src/coercion.integration.test.ts +++ b/packages/arkenv/src/coercion.integration.test.ts @@ -112,9 +112,7 @@ describe("coercion integration", () => { it("should NOT coerce empty or whitespace strings to 0 for numbers", () => { expect(() => arkenv({ VAL: "number" }, { env: { VAL: "" } })).toThrow(); - expect(() => - arkenv({ VAL: "number" }, { env: { VAL: " " } }), - ).toThrow(); + expect(() => arkenv({ VAL: "number" }, { env: { VAL: " " } })).toThrow(); }); it("should fail validation if coercion fails (not a boolean)", () => { diff --git a/packages/arkenv/src/create-env.test.ts b/packages/arkenv/src/create-env.test.ts index be1549728..4f2d8cefb 100644 --- a/packages/arkenv/src/create-env.test.ts +++ b/packages/arkenv/src/create-env.test.ts @@ -167,15 +167,11 @@ describe("arkenv", () => { }); it("should throw for empty string when number is expected", () => { - expect(() => - arkenv({ VAL: "number" }, { env: { VAL: "" } }), - ).toThrow(); + expect(() => arkenv({ VAL: "number" }, { env: { VAL: "" } })).toThrow(); }); it("should throw for empty string when boolean is expected", () => { - expect(() => - arkenv({ VAL: "boolean" }, { env: { VAL: "" } }), - ).toThrow(); + expect(() => arkenv({ VAL: "boolean" }, { env: { VAL: "" } })).toThrow(); }); it("should allow empty strings when the schema is unknown", () => { @@ -543,10 +539,7 @@ describe("arkenv", () => { }); it("should handle single-element array", () => { - const env = arkenv( - { TAGS: "string[]" }, - { env: { TAGS: "only-one" } }, - ); + const env = arkenv({ TAGS: "string[]" }, { env: { TAGS: "only-one" } }); expect(env.TAGS).toEqual(["only-one"]); }); From bb417f5570a779d0defca127670b9e54efcb0d8b Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 00:49:42 +0500 Subject: [PATCH 022/117] refactor(arkenv): remove redundant import --- packages/arkenv/src/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/arkenv/src/index.test.ts b/packages/arkenv/src/index.test.ts index e92a25405..cce9a6f65 100644 --- a/packages/arkenv/src/index.test.ts +++ b/packages/arkenv/src/index.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, expectTypeOf, it, vi } from "vitest"; -import arkenv, { arkenv } from "./index"; +import arkenv from "./index"; describe("index.ts exports", () => { afterEach(() => { From 27f1ae319a22d11216e623b635f4dce7ab6d000b Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 01:07:17 +0500 Subject: [PATCH 023/117] refactor(arkenv): inline schema validation adapters - Delete adapter classes and index file - Move `EnvIssue` type to `src/errors.ts` - Inline ArkType validation logic --- packages/arkenv/src/adapters/arktype.ts | 68 ----- packages/arkenv/src/adapters/index.ts | 40 --- packages/arkenv/src/adapters/standard.ts | 32 --- packages/arkenv/src/create-env.ts | 111 +++++++- packages/arkenv/src/errors.ts | 15 +- test | 328 +++++++++++++++++++++++ 6 files changed, 441 insertions(+), 153 deletions(-) delete mode 100644 packages/arkenv/src/adapters/arktype.ts delete mode 100644 packages/arkenv/src/adapters/index.ts delete mode 100644 packages/arkenv/src/adapters/standard.ts create mode 100644 test diff --git a/packages/arkenv/src/adapters/arktype.ts b/packages/arkenv/src/adapters/arktype.ts deleted file mode 100644 index 429befebf..000000000 --- a/packages/arkenv/src/adapters/arktype.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { createRequire } from "node:module"; -import { coerce } from "../utils/coerce"; -import type { EnvIssue, SchemaAdapter } from "./index"; - -const require = createRequire(import.meta.url); - -export class ArkTypeAdapter implements SchemaAdapter { - readonly kind = "arktype"; - - constructor( - private schemaDef: unknown, - private config: { - coerce?: boolean; - onUndeclaredKey?: "ignore" | "delete" | "reject"; - arrayFormat?: "comma" | "json" | undefined; - } = {}, - ) {} - - validate(env: Record) { - try { - const { $ } = require("@repo/scope"); - const { type } = require("arktype"); - - const isCompiledType = - typeof this.schemaDef === "function" && - "assert" in (this.schemaDef as any); - - let schema = isCompiledType - ? (this.schemaDef as any) - : $.type(this.schemaDef); - - // Apply the `onUndeclaredKey` option, defaulting to "delete" for arkenv compatibility - schema = schema.onUndeclaredKey(this.config.onUndeclaredKey ?? "delete"); - - // Apply coercion transformation - if (this.config.coerce !== false) { - schema = coerce(type, schema, { - arrayFormat: this.config.arrayFormat as any, - }); - } - - const result = schema(env); - - if (result instanceof type.errors) { - return { - success: false, - issues: Object.entries(result.byPath).map(([path, error]) => ({ - path: path ? path.split(".") : [], - message: (error as any).message, - validator: "arktype" as const, - })), - } as const; - } - - return { - success: true, - value: result, - } as const; - } catch (e: any) { - if (e.code === "MODULE_NOT_FOUND") { - throw new Error( - "ArkType is required for this schema type. Please install 'arktype' or use a Standard Schema validator like Zod.", - ); - } - throw e; - } - } -} diff --git a/packages/arkenv/src/adapters/index.ts b/packages/arkenv/src/adapters/index.ts deleted file mode 100644 index 64160931d..000000000 --- a/packages/arkenv/src/adapters/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Normalised issue format for ArkEnv. - * This shape is stable across all validators (ArkType, Standard Schema, etc.). - * Treat this as an invariant for future tooling, CLI formatting, or IDE integrations. - */ -export type EnvIssue = { - /** - * The path to the failing key as an array of strings. - * Empty array means root Level. - */ - path: string[]; - /** - * The localized or validator-specific error message. - */ - message: string; - /** - * The kind of validator that produced this error. - */ - validator: "arktype" | "standard"; -}; - -export type SchemaAdapter = { - /** - * The internal identity of the adapter. - */ - readonly kind: "arktype" | "standard"; - - /** - * Validate a record of environment variables. - */ - validate(env: Record): - | { - success: true; - value: unknown; - } - | { - success: false; - issues: EnvIssue[]; - }; -}; diff --git a/packages/arkenv/src/adapters/standard.ts b/packages/arkenv/src/adapters/standard.ts deleted file mode 100644 index 836ead29c..000000000 --- a/packages/arkenv/src/adapters/standard.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { StandardSchemaV1 } from "@standard-schema/spec"; -import type { EnvIssue, SchemaAdapter } from "./index"; - -export class StandardSchemaAdapter implements SchemaAdapter { - readonly kind = "standard"; - - constructor(private schema: StandardSchemaV1) {} - - validate(env: Record) { - const result = this.schema["~standard"].validate(env); - - if (result instanceof Promise) { - throw new Error("ArkEnv does not support asynchronous validation."); - } - - if (result.issues) { - return { - success: false, - issues: result.issues.map((issue) => ({ - path: (issue.path as string[]) ?? [], - message: issue.message, - validator: "standard" as const, - })), - } as const; - } - - return { - success: true, - value: result.value, - } as const; - } -} diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 566ac7b4a..8af72f0fc 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -1,11 +1,14 @@ +import type { StandardSchemaV1 } from "@standard-schema/spec"; +import { createRequire } from "node:module"; import type { $ } from "@repo/scope"; import type { EnvSchemaWithType, InferType, SchemaShape } from "@repo/types"; import type { type as at, distill } from "arktype"; -import { ArkTypeAdapter } from "./adapters/arktype"; -import { StandardSchemaAdapter } from "./adapters/standard"; -import { ArkEnvError } from "./errors"; +import { type EnvIssue, ArkEnvError } from "./errors"; +import { coerce } from "./utils/coerce"; import type { CoerceOptions } from "./utils"; +const require = createRequire(import.meta.url); + export type EnvSchema = at.validate; type RuntimeEnvironment = Record; @@ -37,7 +40,95 @@ export type ArkEnvConfig = { }; /** - * Internal Standard Schema entry point. + * Internal validation logic for ArkType schemas. + */ +function validateArkType( + def: unknown, + config: ArkEnvConfig, + env: Record, +): { success: true; value: unknown } | { success: false; issues: EnvIssue[] } { + try { + const { $ } = require("@repo/scope"); + const { type } = require("arktype"); + + const schemaDef = def as any; + const isCompiledType = + (typeof schemaDef === "function" && "assert" in schemaDef) || + (typeof schemaDef === "object" && + schemaDef !== null && + "invoke" in schemaDef); + + let schema = isCompiledType ? schemaDef : $.type(schemaDef); + + // Apply the `onUndeclaredKey` option, defaulting to "delete" for arkenv compatibility + schema = schema.onUndeclaredKey(config.onUndeclaredKey ?? "delete"); + + // Apply coercion transformation + if (config.coerce !== false) { + schema = coerce(type, schema, { + ...(config.arrayFormat ? { arrayFormat: config.arrayFormat } : {}), + }); + } + + const result = schema(env); + + if (result instanceof type.errors) { + return { + success: false, + issues: Object.entries(result.byPath).map(([path, error]) => ({ + path: path ? path.split(".") : [], + message: (error as { message: string }).message, + validator: "arktype" as const, + })), + }; + } + + return { + success: true, + value: result, + }; + } catch (e: any) { + if (e.code === "MODULE_NOT_FOUND") { + throw new Error( + "ArkType is required for this schema type. Please install 'arktype' or use a Standard Schema validator like Zod.", + ); + } + throw e; + } +} + +/** + * Internal validation logic for Standard Schema validators. + */ +function validateStandard( + def: StandardSchemaV1, + env: Record, +): { success: true; value: unknown } | { success: false; issues: EnvIssue[] } { + const result = def["~standard"].validate(env); + + if (result instanceof Promise) { + throw new Error("ArkEnv does not support asynchronous validation."); + } + + if (result.issues) { + return { + success: false, + issues: result.issues.map((issue) => ({ + path: (issue.path as string[]) ?? [], + message: issue.message, + validator: "standard" as const, + })), + }; + } + + return { + success: true, + value: result.value, + }; +} + +/** + * Internal entry point for validation. * @internal */ function defineEnv( @@ -46,14 +137,14 @@ function defineEnv( ): InferType { const env = config.env ?? process.env; const isStandard = !!(def as any)?.["~standard"]; - const isArkCompiled = typeof def === "function" && "assert" in (def as any); + const isArkCompiled = + (typeof def === "function" && "assert" in (def as any)) || + (typeof def === "object" && def !== null && "invoke" in (def as any)); - const adapter = + const result = isStandard && !isArkCompiled - ? new StandardSchemaAdapter(def as any) - : new ArkTypeAdapter(def, config as any); - - const result = adapter.validate(env); + ? validateStandard(def, env) + : validateArkType(def, config, env); if (!result.success) { throw new ArkEnvError(result.issues); diff --git a/packages/arkenv/src/errors.ts b/packages/arkenv/src/errors.ts index 1ee6c8a64..7838acdca 100644 --- a/packages/arkenv/src/errors.ts +++ b/packages/arkenv/src/errors.ts @@ -1,4 +1,13 @@ -import type { EnvIssue } from "./adapters"; +/** + * Normalized issue format for ArkEnv. + * @internal + */ +export type EnvIssue = { + path: string[]; + message: string; + validator: "arktype" | "standard"; +}; + import { indent } from "./utils/indent"; import { styleText } from "./utils/style-text"; @@ -30,8 +39,8 @@ export class ArkEnvError extends Error { Object.defineProperty(ArkEnvError, "name", { value: "ArkEnvError" }); /** - * Format the issues returned by ArkType for display - * @param issues - The issues returned by ArkType + * Format the issues for display + * @param issues - The issues found during validation * @returns A string of the formatted issues */ export function formatIssues(issues: EnvIssue[]): string { diff --git a/test b/test new file mode 100644 index 000000000..cd28dec76 --- /dev/null +++ b/test @@ -0,0 +1,328 @@ +diff --git a/packages/arkenv/src/adapters/arktype.ts b/packages/arkenv/src/adapters/arktype.ts +deleted file mode 100644 +index 429befe..0000000 +--- a/packages/arkenv/src/adapters/arktype.ts ++++ /dev/null +@@ -1,68 +0,0 @@ +-import { createRequire } from "node:module"; +-import { coerce } from "../utils/coerce"; +-import type { EnvIssue, SchemaAdapter } from "./index"; +- +-const require = createRequire(import.meta.url); +- +-export class ArkTypeAdapter implements SchemaAdapter { +- readonly kind = "arktype"; +- +- constructor( +- private schemaDef: unknown, +- private config: { +- coerce?: boolean; +- onUndeclaredKey?: "ignore" | "delete" | "reject"; +- arrayFormat?: "comma" | "json" | undefined; +- } = {}, +- ) {} +- +- validate(env: Record) { +- try { +- const { $ } = require("@repo/scope"); +- const { type } = require("arktype"); +- +- const isCompiledType = +- typeof this.schemaDef === "function" && +- "assert" in (this.schemaDef as any); +- +- let schema = isCompiledType +- ? (this.schemaDef as any) +- : $.type(this.schemaDef); +- +- // Apply the `onUndeclaredKey` option, defaulting to "delete" for arkenv compatibility +- schema = schema.onUndeclaredKey(this.config.onUndeclaredKey ?? "delete"); +- +- // Apply coercion transformation +- if (this.config.coerce !== false) { +- schema = coerce(type, schema, { +- arrayFormat: this.config.arrayFormat as any, +- }); +- } +- +- const result = schema(env); +- +- if (result instanceof type.errors) { +- return { +- success: false, +- issues: Object.entries(result.byPath).map(([path, error]) => ({ +- path: path ? path.split(".") : [], +- message: (error as any).message, +- validator: "arktype" as const, +- })), +- } as const; +- } +- +- return { +- success: true, +- value: result, +- } as const; +- } catch (e: any) { +- if (e.code === "MODULE_NOT_FOUND") { +- throw new Error( +- "ArkType is required for this schema type. Please install 'arktype' or use a Standard Schema validator like Zod.", +- ); +- } +- throw e; +- } +- } +-} +diff --git a/packages/arkenv/src/adapters/index.ts b/packages/arkenv/src/adapters/index.ts +deleted file mode 100644 +index 6416093..0000000 +--- a/packages/arkenv/src/adapters/index.ts ++++ /dev/null +@@ -1,40 +0,0 @@ +-/** +- * Normalised issue format for ArkEnv. +- * This shape is stable across all validators (ArkType, Standard Schema, etc.). +- * Treat this as an invariant for future tooling, CLI formatting, or IDE integrations. +- */ +-export type EnvIssue = { +- /** +- * The path to the failing key as an array of strings. +- * Empty array means root Level. +- */ +- path: string[]; +- /** +- * The localized or validator-specific error message. +- */ +- message: string; +- /** +- * The kind of validator that produced this error. +- */ +- validator: "arktype" | "standard"; +-}; +- +-export type SchemaAdapter = { +- /** +- * The internal identity of the adapter. +- */ +- readonly kind: "arktype" | "standard"; +- +- /** +- * Validate a record of environment variables. +- */ +- validate(env: Record): +- | { +- success: true; +- value: unknown; +- } +- | { +- success: false; +- issues: EnvIssue[]; +- }; +-}; +diff --git a/packages/arkenv/src/adapters/standard.ts b/packages/arkenv/src/adapters/standard.ts +deleted file mode 100644 +index 836ead2..0000000 +--- a/packages/arkenv/src/adapters/standard.ts ++++ /dev/null +@@ -1,32 +0,0 @@ +-import type { StandardSchemaV1 } from "@standard-schema/spec"; +-import type { EnvIssue, SchemaAdapter } from "./index"; +- +-export class StandardSchemaAdapter implements SchemaAdapter { +- readonly kind = "standard"; +- +- constructor(private schema: StandardSchemaV1) {} +- +- validate(env: Record) { +- const result = this.schema["~standard"].validate(env); +- +- if (result instanceof Promise) { +- throw new Error("ArkEnv does not support asynchronous validation."); +- } +- +- if (result.issues) { +- return { +- success: false, +- issues: result.issues.map((issue) => ({ +- path: (issue.path as string[]) ?? [], +- message: issue.message, +- validator: "standard" as const, +- })), +- } as const; +- } +- +- return { +- success: true, +- value: result.value, +- } as const; +- } +-} +diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts +index 566ac7b..8af72f0 100644 +--- a/packages/arkenv/src/create-env.ts ++++ b/packages/arkenv/src/create-env.ts +@@ -1,11 +1,14 @@ ++import type { StandardSchemaV1 } from "@standard-schema/spec"; ++import { createRequire } from "node:module"; + import type { $ } from "@repo/scope"; + import type { EnvSchemaWithType, InferType, SchemaShape } from "@repo/types"; + import type { type as at, distill } from "arktype"; +-import { ArkTypeAdapter } from "./adapters/arktype"; +-import { StandardSchemaAdapter } from "./adapters/standard"; +-import { ArkEnvError } from "./errors"; ++import { type EnvIssue, ArkEnvError } from "./errors"; ++import { coerce } from "./utils/coerce"; + import type { CoerceOptions } from "./utils"; + ++const require = createRequire(import.meta.url); ++ + export type EnvSchema = at.validate; + type RuntimeEnvironment = Record; + +@@ -37,7 +40,95 @@ export type ArkEnvConfig = { + }; + + /** +- * Internal Standard Schema entry point. ++ * Internal validation logic for ArkType schemas. ++ */ ++function validateArkType( ++ def: unknown, ++ config: ArkEnvConfig, ++ env: Record, ++): { success: true; value: unknown } | { success: false; issues: EnvIssue[] } { ++ try { ++ const { $ } = require("@repo/scope"); ++ const { type } = require("arktype"); ++ ++ const schemaDef = def as any; ++ const isCompiledType = ++ (typeof schemaDef === "function" && "assert" in schemaDef) || ++ (typeof schemaDef === "object" && ++ schemaDef !== null && ++ "invoke" in schemaDef); ++ ++ let schema = isCompiledType ? schemaDef : $.type(schemaDef); ++ ++ // Apply the `onUndeclaredKey` option, defaulting to "delete" for arkenv compatibility ++ schema = schema.onUndeclaredKey(config.onUndeclaredKey ?? "delete"); ++ ++ // Apply coercion transformation ++ if (config.coerce !== false) { ++ schema = coerce(type, schema, { ++ ...(config.arrayFormat ? { arrayFormat: config.arrayFormat } : {}), ++ }); ++ } ++ ++ const result = schema(env); ++ ++ if (result instanceof type.errors) { ++ return { ++ success: false, ++ issues: Object.entries(result.byPath).map(([path, error]) => ({ ++ path: path ? path.split(".") : [], ++ message: (error as { message: string }).message, ++ validator: "arktype" as const, ++ })), ++ }; ++ } ++ ++ return { ++ success: true, ++ value: result, ++ }; ++ } catch (e: any) { ++ if (e.code === "MODULE_NOT_FOUND") { ++ throw new Error( ++ "ArkType is required for this schema type. Please install 'arktype' or use a Standard Schema validator like Zod.", ++ ); ++ } ++ throw e; ++ } ++} ++ ++/** ++ * Internal validation logic for Standard Schema validators. ++ */ ++function validateStandard( ++ def: StandardSchemaV1, ++ env: Record, ++): { success: true; value: unknown } | { success: false; issues: EnvIssue[] } { ++ const result = def["~standard"].validate(env); ++ ++ if (result instanceof Promise) { ++ throw new Error("ArkEnv does not support asynchronous validation."); ++ } ++ ++ if (result.issues) { ++ return { ++ success: false, ++ issues: result.issues.map((issue) => ({ ++ path: (issue.path as string[]) ?? [], ++ message: issue.message, ++ validator: "standard" as const, ++ })), ++ }; ++ } ++ ++ return { ++ success: true, ++ value: result.value, ++ }; ++} ++ ++/** ++ * Internal entry point for validation. + * @internal + */ + function defineEnv( +@@ -46,14 +137,14 @@ function defineEnv( + ): InferType { + const env = config.env ?? process.env; + const isStandard = !!(def as any)?.["~standard"]; +- const isArkCompiled = typeof def === "function" && "assert" in (def as any); ++ const isArkCompiled = ++ (typeof def === "function" && "assert" in (def as any)) || ++ (typeof def === "object" && def !== null && "invoke" in (def as any)); + +- const adapter = ++ const result = + isStandard && !isArkCompiled +- ? new StandardSchemaAdapter(def as any) +- : new ArkTypeAdapter(def, config as any); +- +- const result = adapter.validate(env); ++ ? validateStandard(def, env) ++ : validateArkType(def, config, env); + + if (!result.success) { + throw new ArkEnvError(result.issues); +diff --git a/packages/arkenv/src/errors.ts b/packages/arkenv/src/errors.ts +index 1ee6c8a..7838acd 100644 +--- a/packages/arkenv/src/errors.ts ++++ b/packages/arkenv/src/errors.ts +@@ -1,4 +1,13 @@ +-import type { EnvIssue } from "./adapters"; ++/** ++ * Normalized issue format for ArkEnv. ++ * @internal ++ */ ++export type EnvIssue = { ++ path: string[]; ++ message: string; ++ validator: "arktype" | "standard"; ++}; ++ + import { indent } from "./utils/indent"; + import { styleText } from "./utils/style-text"; + +@@ -30,8 +39,8 @@ export class ArkEnvError extends Error { + Object.defineProperty(ArkEnvError, "name", { value: "ArkEnvError" }); + + /** +- * Format the issues returned by ArkType for display +- * @param issues - The issues returned by ArkType ++ * Format the issues for display ++ * @param issues - The issues found during validation + * @returns A string of the formatted issues + */ + export function formatIssues(issues: EnvIssue[]): string { From e7a99793462651169393a28a80d6737341c042ce Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 01:13:49 +0500 Subject: [PATCH 024/117] refactor(errors): simplify error handling - Remove custom EnvIssue type - Update ArkEnvError constructor - Enhance formatErrors to accept ArkErrors - Streamline error formatting logic - Adjust related validation calls --- packages/arkenv/src/create-env.ts | 8 ++-- packages/arkenv/src/errors.test.ts | 42 +++--------------- packages/arkenv/src/errors.ts | 68 ++++++++++++------------------ 3 files changed, 36 insertions(+), 82 deletions(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 8af72f0fc..57985c9ec 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -3,7 +3,7 @@ import { createRequire } from "node:module"; import type { $ } from "@repo/scope"; import type { EnvSchemaWithType, InferType, SchemaShape } from "@repo/types"; import type { type as at, distill } from "arktype"; -import { type EnvIssue, ArkEnvError } from "./errors"; +import { ArkEnvError } from "./errors"; import { coerce } from "./utils/coerce"; import type { CoerceOptions } from "./utils"; @@ -46,7 +46,7 @@ function validateArkType( def: unknown, config: ArkEnvConfig, env: Record, -): { success: true; value: unknown } | { success: false; issues: EnvIssue[] } { +): { success: true; value: unknown } | { success: false; issues: any[] } { try { const { $ } = require("@repo/scope"); const { type } = require("arktype"); @@ -103,7 +103,7 @@ function validateArkType( function validateStandard( def: StandardSchemaV1, env: Record, -): { success: true; value: unknown } | { success: false; issues: EnvIssue[] } { +): { success: true; value: unknown } | { success: false; issues: any[] } { const result = def["~standard"].validate(env); if (result instanceof Promise) { @@ -143,7 +143,7 @@ function defineEnv( const result = isStandard && !isArkCompiled - ? validateStandard(def, env) + ? validateStandard(def as any, env) : validateArkType(def, config, env); if (!result.success) { diff --git a/packages/arkenv/src/errors.test.ts b/packages/arkenv/src/errors.test.ts index 81a6c069e..7e5ceea32 100644 --- a/packages/arkenv/src/errors.test.ts +++ b/packages/arkenv/src/errors.test.ts @@ -1,6 +1,6 @@ import type { ArkErrors } from "arktype"; import { describe, expect, it } from "vitest"; -import { ArkEnvError, formatIssues } from "./errors"; +import { ArkEnvError, formatErrors } from "./errors"; /** * Define ArkErrorsForTest as a subset of ArkErrors @@ -9,35 +9,6 @@ type ArkErrorsForTest = { byPath: Record>; }; -/** - * Format the errors returned by ArkType for testing purposes - * @param errors - The errors returned by ArkType - * @returns A string of the formatted errors - */ -const formatErrorsForTest = (errors: ArkErrorsForTest) => { - const issues = Object.entries(errors.byPath || {}).map( - ([path, error]: [string, any]) => ({ - path: path ? path.split(".") : [], - message: error.message, - validator: "arktype" as const, - }), - ); - return formatIssues(issues); -}; - -/** - * Create an ArkEnvError for testing purposes - * @param errors - The errors returned by ArkType - * @param message - The message to display in the error - * @returns An instance of ArkEnvError - */ -const createArkEnvErrorForTest = ( - errors: ArkErrorsForTest, - message?: string, -) => { - return new ArkEnvError(errors as ArkErrors, message); -}; - describe("formatErrors", () => { it("should format errors with values correctly", () => { const errors: ArkErrorsForTest = { @@ -47,7 +18,7 @@ describe("formatErrors", () => { }, }; - const result = formatErrorsForTest(errors); + const result = formatErrors(errors); expect(result).toContain("PORT"); expect(result).toContain("must be a number"); expect(result).toContain('"abc"'); @@ -62,7 +33,7 @@ describe("formatErrors", () => { DATABASE_URL: { message: "DATABASE_URL is required" }, }, }; - const result = formatErrorsForTest(errors); + const result = formatErrors(errors); expect(result).toContain("DATABASE_URL"); expect(result).toContain("is required"); }); @@ -74,10 +45,9 @@ describe("formatErrors", () => { }, }; - const result = formatErrorsForTest(errors); + const result = formatErrors(errors); expect(result).toContain("ENV_VAR"); expect(result).toContain("must be defined"); - expect(result.match(/ENV_VAR/g)?.length).toBe(1); // Should not duplicate the path }); }); @@ -89,7 +59,7 @@ describe("ArkEnvError", () => { }, }; - const error = createArkEnvErrorForTest(errors); + const error = new ArkEnvError(errors); expect(error.message).toContain( "Errors found while validating environment variables", ); @@ -106,7 +76,7 @@ describe("ArkEnvError", () => { }; const customMessage = "Custom validation error"; - const error = createArkEnvErrorForTest(errors, customMessage); + const error = new ArkEnvError(errors, customMessage); expect(error.message).toContain(customMessage); expect(error.message).toContain("PORT"); expect(error.message).toContain('"abc"'); diff --git a/packages/arkenv/src/errors.ts b/packages/arkenv/src/errors.ts index 7838acdca..5b11f947b 100644 --- a/packages/arkenv/src/errors.ts +++ b/packages/arkenv/src/errors.ts @@ -1,60 +1,44 @@ -/** - * Normalized issue format for ArkEnv. - * @internal - */ -export type EnvIssue = { - path: string[]; - message: string; - validator: "arktype" | "standard"; -}; - import { indent } from "./utils/indent"; import { styleText } from "./utils/style-text"; export class ArkEnvError extends Error { - public issues: EnvIssue[]; - constructor( - issues: EnvIssue[] | any, + errors: any, message = "Errors found while validating environment variables", ) { - const normalizedIssues = Array.isArray(issues) - ? issues - : Object.entries((issues as any).byPath || {}).map( - ([path, error]: [string, any]) => ({ - path: path ? path.split(".") : [], - message: error.message, - validator: "arktype" as const, - }), - ); - - super( - `${styleText("red", message)}\n${indent(formatIssues(normalizedIssues))}\n`, - ); + super(`${styleText("red", message)}\n${indent(formatErrors(errors))}\n`); this.name = "ArkEnvError"; - this.issues = normalizedIssues; } } Object.defineProperty(ArkEnvError, "name", { value: "ArkEnvError" }); /** - * Format the issues for display - * @param issues - The issues found during validation - * @returns A string of the formatted issues + * Format the errors for display + * @param errors - The errors found during validation + * @returns A string of the formatted errors */ -export function formatIssues(issues: EnvIssue[]): string { - return issues - .map((issue) => { - const path = issue.path.length > 0 ? issue.path.join(".") : "root"; - let message = issue.message; - // ArkType's error messages often already include the path. - // Example: "PORT must be a number". - // We want to avoid "PORT: PORT must be a number". - if (message.startsWith(`${path} `)) { - message = message.slice(path.length + 1); - } - return `${styleText("yellow", path)} ${message}`; +export function formatErrors(errors: any): string { + if (Array.isArray(errors)) { + return errors + .map((err) => { + const path = err.path?.length > 0 ? err.path.join(".") : "root"; + let message = err.message; + if (message.startsWith(`${path} `)) { + message = message.slice(path.length + 1); + } + return `${styleText("yellow", path)} ${message}`; + }) + .join("\n"); + } + + return Object.entries(errors.byPath || {}) + .map(([path, error]: [string, any]) => { + const messageWithoutPath = error.message.startsWith(path) + ? error.message.slice(path.length) + : error.message; + + return `${styleText("yellow", path)} ${messageWithoutPath.trimStart()}`; }) .join("\n"); } From 449b58c4982139911d9726f33079a9b33d6110d7 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 01:15:45 +0500 Subject: [PATCH 025/117] docs(coerce): clarify lazy loading support - Add JSDoc for coercion system design - Explain injected dependencies to prevent top-level import - Update comment for coercion transformation in create-env --- packages/arkenv/src/create-env.ts | 2 +- packages/arkenv/src/utils/coerce.ts | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 57985c9ec..ef7da6939 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -63,7 +63,7 @@ function validateArkType( // Apply the `onUndeclaredKey` option, defaulting to "delete" for arkenv compatibility schema = schema.onUndeclaredKey(config.onUndeclaredKey ?? "delete"); - // Apply coercion transformation + // Apply coercion transformation (Lazy Loaded) if (config.coerce !== false) { schema = coerce(type, schema, { ...(config.arrayFormat ? { arrayFormat: config.arrayFormat } : {}), diff --git a/packages/arkenv/src/utils/coerce.ts b/packages/arkenv/src/utils/coerce.ts index 2bba6cf01..d7e207ce4 100644 --- a/packages/arkenv/src/utils/coerce.ts +++ b/packages/arkenv/src/utils/coerce.ts @@ -253,12 +253,22 @@ const applyCoercion = ( return data; }; +/** + * COERCION SYSTEM (Lazy Loading Support) + * + * This implementation uses JSON Schema introspection to identify paths requiring coercion. + * It is designed to be called after ArkType is lazily loaded. + * + * NOTE: Internal types are stripped of explicit ArkType imports to prevent + * accidental top-level dependency loading. + */ + /** * Create a coercing wrapper around an ArkType schema using JSON Schema introspection. */ export function coerce( - type: any, // Use any for type utility to avoid top-level import - schema: any, // Use any for BaseType to avoid top-level import + type: any, // Injected dependency to avoid top-level import + schema: any, // Injected dependency to avoid top-level import options?: CoerceOptions, ): any { const json = schema.in.toJsonSchema({ fallback: (ctx: any) => ctx.base }); From 320ab091859b9e016e9f4a8c3f51236f59e2fdea Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 01:19:14 +0500 Subject: [PATCH 026/117] refactor(errors): normalize error reporting - Introduce `EnvIssue` type for consistent errors - Update validator return types to `EnvIssue[]` - Modify `ArkEnvError` --- packages/arkenv/src/create-env.ts | 10 +++++----- packages/arkenv/src/errors.ts | 25 ++++++++++++++++++++----- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index ef7da6939..6aa66565a 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -1,11 +1,11 @@ -import type { StandardSchemaV1 } from "@standard-schema/spec"; import { createRequire } from "node:module"; import type { $ } from "@repo/scope"; import type { EnvSchemaWithType, InferType, SchemaShape } from "@repo/types"; +import type { StandardSchemaV1 } from "@standard-schema/spec"; import type { type as at, distill } from "arktype"; -import { ArkEnvError } from "./errors"; -import { coerce } from "./utils/coerce"; +import { ArkEnvError, type EnvIssue } from "./errors"; import type { CoerceOptions } from "./utils"; +import { coerce } from "./utils/coerce"; const require = createRequire(import.meta.url); @@ -46,7 +46,7 @@ function validateArkType( def: unknown, config: ArkEnvConfig, env: Record, -): { success: true; value: unknown } | { success: false; issues: any[] } { +): { success: true; value: unknown } | { success: false; issues: EnvIssue[] } { try { const { $ } = require("@repo/scope"); const { type } = require("arktype"); @@ -103,7 +103,7 @@ function validateArkType( function validateStandard( def: StandardSchemaV1, env: Record, -): { success: true; value: unknown } | { success: false; issues: any[] } { +): { success: true; value: unknown } | { success: false; issues: EnvIssue[] } { const result = def["~standard"].validate(env); if (result instanceof Promise) { diff --git a/packages/arkenv/src/errors.ts b/packages/arkenv/src/errors.ts index 5b11f947b..fe1d96da9 100644 --- a/packages/arkenv/src/errors.ts +++ b/packages/arkenv/src/errors.ts @@ -1,9 +1,21 @@ import { indent } from "./utils/indent"; import { styleText } from "./utils/style-text"; +/** + * Normalized issue format for ArkEnv. + * This is the STABLE INTERNAL INVARIANT for error reporting. + * All validators must eventually project their errors into this shape. + * @internal + */ +export type EnvIssue = { + path: string[]; + message: string; + validator: "arktype" | "standard"; +}; + export class ArkEnvError extends Error { constructor( - errors: any, + errors: EnvIssue[] | any, message = "Errors found while validating environment variables", ) { super(`${styleText("red", message)}\n${indent(formatErrors(errors))}\n`); @@ -14,13 +26,16 @@ export class ArkEnvError extends Error { Object.defineProperty(ArkEnvError, "name", { value: "ArkEnvError" }); /** - * Format the errors for display - * @param errors - The errors found during validation + * Format the errors for display. + * While this function accepts `any` for backward compatibility and lazy-loading + * flexibility, it primarily operates on the `EnvIssue` stable shape. + * + * @param errors - The errors found during validation (expected: EnvIssue[]) * @returns A string of the formatted errors */ export function formatErrors(errors: any): string { if (Array.isArray(errors)) { - return errors + return (errors as EnvIssue[]) .map((err) => { const path = err.path?.length > 0 ? err.path.join(".") : "root"; let message = err.message; @@ -32,7 +47,7 @@ export function formatErrors(errors: any): string { .join("\n"); } - return Object.entries(errors.byPath || {}) + return Object.entries((errors as any).byPath || {}) .map(([path, error]: [string, any]) => { const messageWithoutPath = error.message.startsWith(path) ? error.message.slice(path.length) From 814a3017ac6c677bd428533ba43378d4a61e6b51 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 01:21:08 +0500 Subject: [PATCH 027/117] feat(api): export createEnv function --- packages/arkenv/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/arkenv/src/index.ts b/packages/arkenv/src/index.ts index 88b9f8f5b..81fef1817 100644 --- a/packages/arkenv/src/index.ts +++ b/packages/arkenv/src/index.ts @@ -1,4 +1,4 @@ -import { arkenv } from "./create-env"; +import { arkenv, createEnv } from "./create-env"; export type { EnvSchema } from "./create-env"; @@ -8,6 +8,6 @@ export type { EnvSchema } from "./create-env"; * {@link https://arkenv.js.org | ArkEnv} is a typesafe environment variables validator from editor to runtime. */ export default arkenv; -export { arkenv }; +export { arkenv, createEnv }; export { ArkEnvError } from "./errors"; export { type } from "./type"; From ac0f0e063c4e12ac4697dc4c444eccd6a8d29b33 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 01:37:12 +0500 Subject: [PATCH 028/117] refactor: Standardize and refactor internal utilities - Move coercion functions to shared package - Improve validation path mapping in arkenv - Update arkenv exports for named and default - --- packages/arkenv/src/create-env.ts | 15 ++++++- packages/arkenv/src/index.test.ts | 24 +++++------ packages/arkenv/src/utils/coerce.ts | 40 +++---------------- packages/internal/keywords/package.json | 13 ++++-- packages/internal/keywords/src/functions.ts | 35 ++++++++++++++++ packages/internal/keywords/src/index.ts | 33 ++++----------- packages/internal/keywords/tsdown.config.ts | 1 + .../__fixtures__/standard-schema/.env.test | 1 + .../__fixtures__/standard-schema/config.ts | 5 +++ .../src/__fixtures__/standard-schema/index.ts | 1 + packages/vite-plugin/src/index.test.ts | 1 + packages/vite-plugin/src/index.ts | 6 +-- 12 files changed, 96 insertions(+), 79 deletions(-) create mode 100644 packages/internal/keywords/src/functions.ts create mode 100644 packages/vite-plugin/src/__fixtures__/standard-schema/.env.test create mode 100644 packages/vite-plugin/src/__fixtures__/standard-schema/config.ts create mode 100644 packages/vite-plugin/src/__fixtures__/standard-schema/index.ts diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 6aa66565a..72d2e0445 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -114,7 +114,20 @@ function validateStandard( return { success: false, issues: result.issues.map((issue) => ({ - path: (issue.path as string[]) ?? [], + path: + issue.path?.map((segment: unknown) => { + if (typeof segment === "string") return segment; + if (typeof segment === "number") return String(segment); + if (typeof segment === "symbol") return segment.toString(); + if ( + typeof segment === "object" && + segment !== null && + "key" in segment + ) { + return String((segment as { key: unknown }).key); + } + return String(segment); + }) ?? [], message: issue.message, validator: "standard" as const, })), diff --git a/packages/arkenv/src/index.test.ts b/packages/arkenv/src/index.test.ts index cce9a6f65..bda9b951b 100644 --- a/packages/arkenv/src/index.test.ts +++ b/packages/arkenv/src/index.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, expectTypeOf, it, vi } from "vitest"; -import arkenv from "./index"; +import arkenvDefault, { arkenv as arkenvNamed } from "./index"; describe("index.ts exports", () => { afterEach(() => { @@ -9,24 +9,24 @@ describe("index.ts exports", () => { }); it("should export arkenv as default export", () => { - expect(arkenv).toBe(arkenv); - expect(typeof arkenv).toBe("function"); + expect(arkenvDefault).toBe(arkenvNamed); + expect(typeof arkenvDefault).toBe("function"); }); it("should have correct types for exported functions", () => { // Type assertion to verify exported function types - expectTypeOf(arkenv).toBeFunction(); - expectTypeOf(arkenv).toBeFunction(); + expectTypeOf(arkenvDefault).toBeFunction(); + expectTypeOf(arkenvNamed).toBeFunction(); // Verify they have the same type signature - expectTypeOf(arkenv).toEqualTypeOf(arkenv); + expectTypeOf(arkenvDefault).toEqualTypeOf(arkenvNamed); }); it("should work with default import", () => { // Set test environment variable vi.stubEnv("TEST_DEFAULT_IMPORT", "test-value"); - const env = arkenv({ + const env = arkenvDefault({ TEST_DEFAULT_IMPORT: "string", }); @@ -38,7 +38,7 @@ describe("index.ts exports", () => { // Set test environment variable vi.stubEnv("TEST_NAMED_IMPORT", "test-value"); - const env = arkenv({ + const env = arkenvNamed({ TEST_NAMED_IMPORT: "string", }); @@ -48,7 +48,7 @@ describe("index.ts exports", () => { it("should throw error with default import when validation fails", () => { expect(() => - arkenv({ + arkenvDefault({ MISSING_DEFAULT_VAR: "string", }), ).toThrow(); @@ -56,7 +56,7 @@ describe("index.ts exports", () => { it("should throw error with named import when validation fails", () => { expect(() => - arkenv({ + arkenvNamed({ MISSING_NAMED_VAR: "string", }), ).toThrow(); @@ -66,11 +66,11 @@ describe("index.ts exports", () => { // Set test environment variable vi.stubEnv("COMPARISON_TEST", "same-value"); - const envFromDefault = arkenv({ + const envFromDefault = arkenvDefault({ COMPARISON_TEST: "string", }); - const envFromNamed = arkenv({ + const envFromNamed = arkenvNamed({ COMPARISON_TEST: "string", }); diff --git a/packages/arkenv/src/utils/coerce.ts b/packages/arkenv/src/utils/coerce.ts index d7e207ce4..121531100 100644 --- a/packages/arkenv/src/utils/coerce.ts +++ b/packages/arkenv/src/utils/coerce.ts @@ -1,38 +1,8 @@ -/** - * A loose numeric morph. - */ -const maybeNumber = (s: unknown) => { - if (typeof s === "number") return s; - if (typeof s !== "string") return s; - const trimmed = s.trim(); - if (trimmed === "") return s; - if (trimmed === "NaN") return Number.NaN; - const n = Number(trimmed); - return Number.isNaN(n) ? s : n; -}; - -/** - * A loose boolean morph. - */ -const maybeBoolean = (s: unknown) => { - if (s === "true") return true; - if (s === "false") return false; - return s; -}; - -/** - * A loose JSON morph. - */ -const maybeJson = (s: unknown) => { - if (typeof s !== "string") return s; - const trimmed = s.trim(); - if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return s; - try { - return JSON.parse(trimmed); - } catch { - return s; - } -}; +import { + maybeBooleanFunction as maybeBoolean, + maybeJsonFunction as maybeJson, + maybeNumberFunction as maybeNumber, +} from "@repo/keywords/functions"; /** * A marker used in the coercion path to indicate that the target diff --git a/packages/internal/keywords/package.json b/packages/internal/keywords/package.json index 61a39717f..c45980dde 100644 --- a/packages/internal/keywords/package.json +++ b/packages/internal/keywords/package.json @@ -8,9 +8,16 @@ "types": "./dist/index.d.ts", "description": "ArkEnv keywords (internal usage)", "exports": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "require": "./dist/index.cjs" + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./functions": { + "types": "./dist/functions.d.ts", + "import": "./dist/functions.js", + "require": "./dist/functions.cjs" + } }, "scripts": { "build": "tsdown", diff --git a/packages/internal/keywords/src/functions.ts b/packages/internal/keywords/src/functions.ts new file mode 100644 index 000000000..0ed0e01b5 --- /dev/null +++ b/packages/internal/keywords/src/functions.ts @@ -0,0 +1,35 @@ +/** + * A loose numeric morph function. + */ +export const maybeNumberFunction = (s: unknown) => { + if (typeof s === "number") return s; + if (typeof s !== "string") return s; + const trimmed = s.trim(); + if (trimmed === "") return s; + if (trimmed === "NaN") return Number.NaN; + const n = Number(trimmed); + return Number.isNaN(n) ? s : n; +}; + +/** + * A loose boolean morph function. + */ +export const maybeBooleanFunction = (s: unknown) => { + if (s === "true") return true; + if (s === "false") return false; + return s; +}; + +/** + * A loose JSON morph function. + */ +export const maybeJsonFunction = (s: unknown) => { + if (typeof s !== "string") return s; + const trimmed = s.trim(); + if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return s; + try { + return JSON.parse(trimmed); + } catch { + return s; + } +}; diff --git a/packages/internal/keywords/src/index.ts b/packages/internal/keywords/src/index.ts index 2b192a78d..881988da9 100644 --- a/packages/internal/keywords/src/index.ts +++ b/packages/internal/keywords/src/index.ts @@ -1,4 +1,9 @@ import { type } from "arktype"; +import { + maybeBooleanFunction, + maybeJsonFunction, + maybeNumberFunction, +} from "./functions"; /** * A loose numeric morph. @@ -9,15 +14,7 @@ import { type } from "arktype"; * * Useful for coercion in unions where failing on non-numeric strings would block other branches. */ -export const maybeNumber = type("unknown").pipe((s) => { - if (typeof s === "number") return s; - if (typeof s !== "string") return s; - const trimmed = s.trim(); - if (trimmed === "") return s; - if (trimmed === "NaN") return Number.NaN; - const n = Number(trimmed); - return Number.isNaN(n) ? s : n; -}); +export const maybeNumber = type("unknown").pipe(maybeNumberFunction); /** * A loose boolean morph. @@ -28,11 +25,7 @@ export const maybeNumber = type("unknown").pipe((s) => { * * Useful for coercion in unions where failing on non-boolean strings would block other branches. */ -export const maybeBoolean = type("unknown").pipe((s) => { - if (s === "true") return true; - if (s === "false") return false; - return s; -}); +export const maybeBoolean = type("unknown").pipe(maybeBooleanFunction); /** * A `number` integer between 0 and 65535. @@ -53,14 +46,4 @@ export const host = type("string.ip | 'localhost'"); * * Useful for coercion in unions where failing on non-JSON strings would block other branches. */ -export const maybeJson = type("unknown").pipe((s) => { - if (typeof s !== "string") return s; - const trimmed = s.trim(); - // Only attempt to parse if it looks like JSON (starts with { or [) - if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return s; - try { - return JSON.parse(trimmed); - } catch { - return s; - } -}); +export const maybeJson = type("unknown").pipe(maybeJsonFunction); diff --git a/packages/internal/keywords/tsdown.config.ts b/packages/internal/keywords/tsdown.config.ts index 8fc08e8db..04ae1d9ed 100644 --- a/packages/internal/keywords/tsdown.config.ts +++ b/packages/internal/keywords/tsdown.config.ts @@ -1,6 +1,7 @@ import { defineConfig } from "tsdown"; export default defineConfig({ + entry: ["src/index.ts", "src/functions.ts"], format: ["esm", "cjs"], minify: true, fixedExtension: false, diff --git a/packages/vite-plugin/src/__fixtures__/standard-schema/.env.test b/packages/vite-plugin/src/__fixtures__/standard-schema/.env.test new file mode 100644 index 000000000..62002d231 --- /dev/null +++ b/packages/vite-plugin/src/__fixtures__/standard-schema/.env.test @@ -0,0 +1 @@ +VITE_ZOD_VAR=hello-zod diff --git a/packages/vite-plugin/src/__fixtures__/standard-schema/config.ts b/packages/vite-plugin/src/__fixtures__/standard-schema/config.ts new file mode 100644 index 000000000..2f1811a77 --- /dev/null +++ b/packages/vite-plugin/src/__fixtures__/standard-schema/config.ts @@ -0,0 +1,5 @@ +import { z } from "zod"; + +export const Env = { + VITE_ZOD_VAR: z.string().min(5), +}; diff --git a/packages/vite-plugin/src/__fixtures__/standard-schema/index.ts b/packages/vite-plugin/src/__fixtures__/standard-schema/index.ts new file mode 100644 index 000000000..546b5813b --- /dev/null +++ b/packages/vite-plugin/src/__fixtures__/standard-schema/index.ts @@ -0,0 +1 @@ +console.log(import.meta.env.VITE_ZOD_VAR); diff --git a/packages/vite-plugin/src/index.test.ts b/packages/vite-plugin/src/index.test.ts index 6ec37043d..7f7863772 100644 --- a/packages/vite-plugin/src/index.test.ts +++ b/packages/vite-plugin/src/index.test.ts @@ -2,6 +2,7 @@ import { readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; import * as vite from "vite"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { z } from "zod"; // Mock the arkenv module to capture calls // Mock the arkenv module with a spy that calls the real implementation by default diff --git a/packages/vite-plugin/src/index.ts b/packages/vite-plugin/src/index.ts index e3e947d43..f50337d1a 100644 --- a/packages/vite-plugin/src/index.ts +++ b/packages/vite-plugin/src/index.ts @@ -59,9 +59,9 @@ export default function arkenv( // Load environment based on the custom config const envDir = config.envDir ?? config.root ?? process.cwd(); - // TODO: We're using type assertions and explicitly pass in the type arguments here to avoid - // "Type instantiation is excessively deep and possibly infinite" errors. - // Ideally, we should find a way to avoid these assertions while maintaining type safety. + // NOTE: We use 'options as any' here strictly to bypass TypeScript's "Type instantiation is excessively deep" errors. + // This assertion ONLY affects compile-time checking; runtime validation (including Standard Schema via def["~standard"]) + // remains fully intact and functional. const env = validateEnv(options as any, { env: loadEnv(mode, envDir, ""), }); From b5f7c4a0fe23039dd3daf413c04c77d4e59869d1 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 01:55:24 +0500 Subject: [PATCH 029/117] test(vite-plugin): add real schema validator test - Downgrade @standard-schema/spec to 1.0.0 - Verify plugin works with Zod validator --- package.json | 2 +- packages/arkenv/package.json | 2 +- packages/internal/types/package.json | 2 +- packages/vite-plugin/src/index.test.ts | 36 ++++++++++++++++++++++++++ pnpm-lock.yaml | 19 +++++++++----- 5 files changed, 51 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index f3aaad617..4ff6e598d 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@changesets/cli": "2.29.8", "@manypkg/cli": "0.25.1", "@playwright/test": "", - "@standard-schema/spec": "^1.1.0", + "@standard-schema/spec": "1.0.0", "@vitest/ui": "catalog:", "all-contributors-cli": "^6.26.1", "changesets-changelog-clean": "1.3.0", diff --git a/packages/arkenv/package.json b/packages/arkenv/package.json index 79230f5b8..2e85f7888 100644 --- a/packages/arkenv/package.json +++ b/packages/arkenv/package.json @@ -45,7 +45,7 @@ "@repo/types": "workspace:*", "@size-limit/esbuild-why": "catalog:", "@size-limit/preset-small-lib": "catalog:", - "@standard-schema/spec": "^1.1.0", + "@standard-schema/spec": "1.0.0", "@types/node": "catalog:", "arktype": "catalog:", "rimraf": "catalog:", diff --git a/packages/internal/types/package.json b/packages/internal/types/package.json index 652e2097f..7d8db6ed4 100644 --- a/packages/internal/types/package.json +++ b/packages/internal/types/package.json @@ -18,7 +18,7 @@ }, "devDependencies": { "@repo/scope": "workspace:*", - "@standard-schema/spec": "^1.1.0", + "@standard-schema/spec": "1.0.0", "arktype": "catalog:", "typescript": "catalog:" }, diff --git a/packages/vite-plugin/src/index.test.ts b/packages/vite-plugin/src/index.test.ts index 7f7863772..4321a0c73 100644 --- a/packages/vite-plugin/src/index.test.ts +++ b/packages/vite-plugin/src/index.test.ts @@ -619,6 +619,42 @@ describe("Plugin Unit Tests", () => { }, ); }); + + it("should work with a real Standard Schema validator (e.g. Zod)", async () => { + vi.stubEnv("VITE_ZOD_VAR", "valid-value"); + const schema = { + VITE_ZOD_VAR: z.string().min(5), + }; + + // Note: We use the real implementation for this test + const actual = await vi.importActual("arkenv"); + mockArkenv.mockImplementation(actual.arkenv); + + const pluginInstance = arkenvPlugin(schema); + + let result: any = {}; + if (pluginInstance.config && typeof pluginInstance.config === "function") { + const mockContext = { + meta: { + framework: "vite", + version: "1.0.0", + rollupVersion: "4.0.0", + viteVersion: "5.0.0", + }, + error: vi.fn(), + warn: vi.fn(), + info: vi.fn(), + debug: vi.fn(), + } as any; + result = pluginInstance.config.call( + mockContext, + {}, + { mode: "test", command: "build" }, + ); + } + + expect(result.define).toHaveProperty("import.meta.env.VITE_ZOD_VAR"); + }); }); // Integration tests using with-env-dir fixture for custom envDir configuration diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c56fff43..2a17f9fdc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,8 +101,8 @@ importers: specifier: 1.57.0 version: 1.57.0 '@standard-schema/spec': - specifier: ^1.1.0 - version: 1.1.0 + specifier: 1.0.0 + version: 1.0.0 '@vitest/ui': specifier: 'catalog:' version: 4.0.16(vitest@4.0.16) @@ -555,8 +555,8 @@ importers: specifier: 'catalog:' version: 12.0.0(size-limit@12.0.0(jiti@2.6.1)) '@standard-schema/spec': - specifier: ^1.1.0 - version: 1.1.0 + specifier: 1.0.0 + version: 1.0.0 '@types/node': specifier: 'catalog:' version: 24.10.4 @@ -656,8 +656,8 @@ importers: specifier: workspace:* version: link:../scope '@standard-schema/spec': - specifier: ^1.1.0 - version: 1.1.0 + specifier: 1.0.0 + version: 1.0.0 arktype: specifier: 'catalog:' version: 2.1.29 @@ -3614,6 +3614,9 @@ packages: '@speed-highlight/core@1.2.12': resolution: {integrity: sha512-uilwrK0Ygyri5dToHYdZSjcvpS2ZwX0w5aSt3GCEN9hrjxWCoeV4Z2DTXuxjwbntaLQIEEAlCeNQss5SoHvAEA==} + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -11418,6 +11421,8 @@ snapshots: '@speed-highlight/core@1.2.12': {} + '@standard-schema/spec@1.0.0': {} + '@standard-schema/spec@1.1.0': {} '@svta/cml-608@1.0.1': {} @@ -11856,7 +11861,7 @@ snapshots: '@vitest/expect@4.0.16': dependencies: - '@standard-schema/spec': 1.1.0 + '@standard-schema/spec': 1.0.0 '@types/chai': 5.2.3 '@vitest/spy': 4.0.16 '@vitest/utils': 4.0.16 From 735fd611b434051a200d32da2fd58691aa4b0ed2 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 18:37:39 +0500 Subject: [PATCH 030/117] refactor(env): extract validator type detection - Created `detectValidatorType` utility - Removed duplicated type detection logic - Improved code maintainability --- packages/arkenv/src/create-env.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 72d2e0445..3497b1518 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -39,6 +39,17 @@ export type ArkEnvConfig = { arrayFormat?: CoerceOptions["arrayFormat"]; }; +/** + * Detects the type of validator being used. + */ +function detectValidatorType(def: unknown) { + const isStandard = !!(def as any)?.["~standard"]; + const isArkCompiled = + (typeof def === "function" && "assert" in (def as any)) || + (typeof def === "object" && def !== null && "invoke" in (def as any)); + return { isStandard, isArkCompiled }; +} + /** * Internal validation logic for ArkType schemas. */ @@ -51,14 +62,9 @@ function validateArkType( const { $ } = require("@repo/scope"); const { type } = require("arktype"); - const schemaDef = def as any; - const isCompiledType = - (typeof schemaDef === "function" && "assert" in schemaDef) || - (typeof schemaDef === "object" && - schemaDef !== null && - "invoke" in schemaDef); + const { isArkCompiled: isCompiledType } = detectValidatorType(def); - let schema = isCompiledType ? schemaDef : $.type(schemaDef); + let schema = isCompiledType ? (def as any) : ($.type(def) as any); // Apply the `onUndeclaredKey` option, defaulting to "delete" for arkenv compatibility schema = schema.onUndeclaredKey(config.onUndeclaredKey ?? "delete"); @@ -149,10 +155,7 @@ function defineEnv( config: ArkEnvConfig = {}, ): InferType { const env = config.env ?? process.env; - const isStandard = !!(def as any)?.["~standard"]; - const isArkCompiled = - (typeof def === "function" && "assert" in (def as any)) || - (typeof def === "object" && def !== null && "invoke" in (def as any)); + const { isStandard, isArkCompiled } = detectValidatorType(def); const result = isStandard && !isArkCompiled @@ -193,10 +196,7 @@ export function arkenv( def: EnvSchema | EnvSchemaWithType, config: ArkEnvConfig = {}, ): distill.Out> | InferType { - const isStandard = !!(def as any)?.["~standard"]; - const isArkCompiled = - (typeof def === "function" && "assert" in (def as any)) || - (typeof def === "object" && def !== null && "invoke" in (def as any)); + const { isStandard, isArkCompiled } = detectValidatorType(def); // Guardrail: Block top-level Standard Schema (Zod, Valibot, etc.) // Reusable type() schemas (ArkType) are allowed. From 7c2ba2711b79f22486e8ce2091c9e234bae07712 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 18:43:28 +0500 Subject: [PATCH 031/117] test(arkenv): Improve error assertions - Assert specific ArkEnvError type - Verify error message content --- packages/arkenv/src/index.test.ts | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/arkenv/src/index.test.ts b/packages/arkenv/src/index.test.ts index bda9b951b..8ae4280fd 100644 --- a/packages/arkenv/src/index.test.ts +++ b/packages/arkenv/src/index.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, expectTypeOf, it, vi } from "vitest"; -import arkenvDefault, { arkenv as arkenvNamed } from "./index"; +import arkenvDefault, { arkenv as arkenvNamed, ArkEnvError } from "./index"; describe("index.ts exports", () => { afterEach(() => { @@ -51,7 +51,17 @@ describe("index.ts exports", () => { arkenvDefault({ MISSING_DEFAULT_VAR: "string", }), - ).toThrow(); + ).toThrowError(ArkEnvError); + + try { + arkenvDefault({ + MISSING_DEFAULT_VAR: "string", + }); + } catch (error: any) { + expect(error).toBeInstanceOf(ArkEnvError); + expect(error.message).toContain("MISSING_DEFAULT_VAR"); + expect(error.message).toContain("string"); + } }); it("should throw error with named import when validation fails", () => { @@ -59,7 +69,17 @@ describe("index.ts exports", () => { arkenvNamed({ MISSING_NAMED_VAR: "string", }), - ).toThrow(); + ).toThrowError(ArkEnvError); + + try { + arkenvNamed({ + MISSING_NAMED_VAR: "string", + }); + } catch (error: any) { + expect(error).toBeInstanceOf(ArkEnvError); + expect(error.message).toContain("MISSING_NAMED_VAR"); + expect(error.message).toContain("string"); + } }); it("should have same behavior for both default and named imports", () => { From 60d2647ef0a75db9212c4bea558b6c9491c7473d Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 18:44:24 +0500 Subject: [PATCH 032/117] test(arkenv): improve error throwing assertions --- packages/arkenv/src/index.test.ts | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/packages/arkenv/src/index.test.ts b/packages/arkenv/src/index.test.ts index 8ae4280fd..5d1867142 100644 --- a/packages/arkenv/src/index.test.ts +++ b/packages/arkenv/src/index.test.ts @@ -46,17 +46,12 @@ describe("index.ts exports", () => { expect(typeof env.TEST_NAMED_IMPORT).toBe("string"); }); - it("should throw error with default import when validation fails", () => { - expect(() => - arkenvDefault({ - MISSING_DEFAULT_VAR: "string", - }), - ).toThrowError(ArkEnvError); - + it("should throw ArkEnvError with default import when validation fails", () => { try { arkenvDefault({ MISSING_DEFAULT_VAR: "string", }); + expect.fail("Should have thrown an error"); } catch (error: any) { expect(error).toBeInstanceOf(ArkEnvError); expect(error.message).toContain("MISSING_DEFAULT_VAR"); @@ -64,17 +59,12 @@ describe("index.ts exports", () => { } }); - it("should throw error with named import when validation fails", () => { - expect(() => - arkenvNamed({ - MISSING_NAMED_VAR: "string", - }), - ).toThrowError(ArkEnvError); - + it("should throw ArkEnvError with named import when validation fails", () => { try { arkenvNamed({ MISSING_NAMED_VAR: "string", }); + expect.fail("Should have thrown an error"); } catch (error: any) { expect(error).toBeInstanceOf(ArkEnvError); expect(error.message).toContain("MISSING_NAMED_VAR"); From 3e041b1a2335f4878401cae6babcb2e017065842 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 18:45:20 +0500 Subject: [PATCH 033/117] test(arkenv): improve error throwing test logic --- packages/arkenv/src/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/arkenv/src/index.test.ts b/packages/arkenv/src/index.test.ts index 5d1867142..a6c0ca3f9 100644 --- a/packages/arkenv/src/index.test.ts +++ b/packages/arkenv/src/index.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, expectTypeOf, it, vi } from "vitest"; -import arkenvDefault, { arkenv as arkenvNamed, ArkEnvError } from "./index"; +import arkenvDefault, { ArkEnvError, arkenv as arkenvNamed } from "./index"; describe("index.ts exports", () => { afterEach(() => { From 61e14acece43c148fe8c386807b37829801e3330 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 18:46:35 +0500 Subject: [PATCH 034/117] Delete test --- test | 328 ----------------------------------------------------------- 1 file changed, 328 deletions(-) delete mode 100644 test diff --git a/test b/test deleted file mode 100644 index cd28dec76..000000000 --- a/test +++ /dev/null @@ -1,328 +0,0 @@ -diff --git a/packages/arkenv/src/adapters/arktype.ts b/packages/arkenv/src/adapters/arktype.ts -deleted file mode 100644 -index 429befe..0000000 ---- a/packages/arkenv/src/adapters/arktype.ts -+++ /dev/null -@@ -1,68 +0,0 @@ --import { createRequire } from "node:module"; --import { coerce } from "../utils/coerce"; --import type { EnvIssue, SchemaAdapter } from "./index"; -- --const require = createRequire(import.meta.url); -- --export class ArkTypeAdapter implements SchemaAdapter { -- readonly kind = "arktype"; -- -- constructor( -- private schemaDef: unknown, -- private config: { -- coerce?: boolean; -- onUndeclaredKey?: "ignore" | "delete" | "reject"; -- arrayFormat?: "comma" | "json" | undefined; -- } = {}, -- ) {} -- -- validate(env: Record) { -- try { -- const { $ } = require("@repo/scope"); -- const { type } = require("arktype"); -- -- const isCompiledType = -- typeof this.schemaDef === "function" && -- "assert" in (this.schemaDef as any); -- -- let schema = isCompiledType -- ? (this.schemaDef as any) -- : $.type(this.schemaDef); -- -- // Apply the `onUndeclaredKey` option, defaulting to "delete" for arkenv compatibility -- schema = schema.onUndeclaredKey(this.config.onUndeclaredKey ?? "delete"); -- -- // Apply coercion transformation -- if (this.config.coerce !== false) { -- schema = coerce(type, schema, { -- arrayFormat: this.config.arrayFormat as any, -- }); -- } -- -- const result = schema(env); -- -- if (result instanceof type.errors) { -- return { -- success: false, -- issues: Object.entries(result.byPath).map(([path, error]) => ({ -- path: path ? path.split(".") : [], -- message: (error as any).message, -- validator: "arktype" as const, -- })), -- } as const; -- } -- -- return { -- success: true, -- value: result, -- } as const; -- } catch (e: any) { -- if (e.code === "MODULE_NOT_FOUND") { -- throw new Error( -- "ArkType is required for this schema type. Please install 'arktype' or use a Standard Schema validator like Zod.", -- ); -- } -- throw e; -- } -- } --} -diff --git a/packages/arkenv/src/adapters/index.ts b/packages/arkenv/src/adapters/index.ts -deleted file mode 100644 -index 6416093..0000000 ---- a/packages/arkenv/src/adapters/index.ts -+++ /dev/null -@@ -1,40 +0,0 @@ --/** -- * Normalised issue format for ArkEnv. -- * This shape is stable across all validators (ArkType, Standard Schema, etc.). -- * Treat this as an invariant for future tooling, CLI formatting, or IDE integrations. -- */ --export type EnvIssue = { -- /** -- * The path to the failing key as an array of strings. -- * Empty array means root Level. -- */ -- path: string[]; -- /** -- * The localized or validator-specific error message. -- */ -- message: string; -- /** -- * The kind of validator that produced this error. -- */ -- validator: "arktype" | "standard"; --}; -- --export type SchemaAdapter = { -- /** -- * The internal identity of the adapter. -- */ -- readonly kind: "arktype" | "standard"; -- -- /** -- * Validate a record of environment variables. -- */ -- validate(env: Record): -- | { -- success: true; -- value: unknown; -- } -- | { -- success: false; -- issues: EnvIssue[]; -- }; --}; -diff --git a/packages/arkenv/src/adapters/standard.ts b/packages/arkenv/src/adapters/standard.ts -deleted file mode 100644 -index 836ead2..0000000 ---- a/packages/arkenv/src/adapters/standard.ts -+++ /dev/null -@@ -1,32 +0,0 @@ --import type { StandardSchemaV1 } from "@standard-schema/spec"; --import type { EnvIssue, SchemaAdapter } from "./index"; -- --export class StandardSchemaAdapter implements SchemaAdapter { -- readonly kind = "standard"; -- -- constructor(private schema: StandardSchemaV1) {} -- -- validate(env: Record) { -- const result = this.schema["~standard"].validate(env); -- -- if (result instanceof Promise) { -- throw new Error("ArkEnv does not support asynchronous validation."); -- } -- -- if (result.issues) { -- return { -- success: false, -- issues: result.issues.map((issue) => ({ -- path: (issue.path as string[]) ?? [], -- message: issue.message, -- validator: "standard" as const, -- })), -- } as const; -- } -- -- return { -- success: true, -- value: result.value, -- } as const; -- } --} -diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts -index 566ac7b..8af72f0 100644 ---- a/packages/arkenv/src/create-env.ts -+++ b/packages/arkenv/src/create-env.ts -@@ -1,11 +1,14 @@ -+import type { StandardSchemaV1 } from "@standard-schema/spec"; -+import { createRequire } from "node:module"; - import type { $ } from "@repo/scope"; - import type { EnvSchemaWithType, InferType, SchemaShape } from "@repo/types"; - import type { type as at, distill } from "arktype"; --import { ArkTypeAdapter } from "./adapters/arktype"; --import { StandardSchemaAdapter } from "./adapters/standard"; --import { ArkEnvError } from "./errors"; -+import { type EnvIssue, ArkEnvError } from "./errors"; -+import { coerce } from "./utils/coerce"; - import type { CoerceOptions } from "./utils"; - -+const require = createRequire(import.meta.url); -+ - export type EnvSchema = at.validate; - type RuntimeEnvironment = Record; - -@@ -37,7 +40,95 @@ export type ArkEnvConfig = { - }; - - /** -- * Internal Standard Schema entry point. -+ * Internal validation logic for ArkType schemas. -+ */ -+function validateArkType( -+ def: unknown, -+ config: ArkEnvConfig, -+ env: Record, -+): { success: true; value: unknown } | { success: false; issues: EnvIssue[] } { -+ try { -+ const { $ } = require("@repo/scope"); -+ const { type } = require("arktype"); -+ -+ const schemaDef = def as any; -+ const isCompiledType = -+ (typeof schemaDef === "function" && "assert" in schemaDef) || -+ (typeof schemaDef === "object" && -+ schemaDef !== null && -+ "invoke" in schemaDef); -+ -+ let schema = isCompiledType ? schemaDef : $.type(schemaDef); -+ -+ // Apply the `onUndeclaredKey` option, defaulting to "delete" for arkenv compatibility -+ schema = schema.onUndeclaredKey(config.onUndeclaredKey ?? "delete"); -+ -+ // Apply coercion transformation -+ if (config.coerce !== false) { -+ schema = coerce(type, schema, { -+ ...(config.arrayFormat ? { arrayFormat: config.arrayFormat } : {}), -+ }); -+ } -+ -+ const result = schema(env); -+ -+ if (result instanceof type.errors) { -+ return { -+ success: false, -+ issues: Object.entries(result.byPath).map(([path, error]) => ({ -+ path: path ? path.split(".") : [], -+ message: (error as { message: string }).message, -+ validator: "arktype" as const, -+ })), -+ }; -+ } -+ -+ return { -+ success: true, -+ value: result, -+ }; -+ } catch (e: any) { -+ if (e.code === "MODULE_NOT_FOUND") { -+ throw new Error( -+ "ArkType is required for this schema type. Please install 'arktype' or use a Standard Schema validator like Zod.", -+ ); -+ } -+ throw e; -+ } -+} -+ -+/** -+ * Internal validation logic for Standard Schema validators. -+ */ -+function validateStandard( -+ def: StandardSchemaV1, -+ env: Record, -+): { success: true; value: unknown } | { success: false; issues: EnvIssue[] } { -+ const result = def["~standard"].validate(env); -+ -+ if (result instanceof Promise) { -+ throw new Error("ArkEnv does not support asynchronous validation."); -+ } -+ -+ if (result.issues) { -+ return { -+ success: false, -+ issues: result.issues.map((issue) => ({ -+ path: (issue.path as string[]) ?? [], -+ message: issue.message, -+ validator: "standard" as const, -+ })), -+ }; -+ } -+ -+ return { -+ success: true, -+ value: result.value, -+ }; -+} -+ -+/** -+ * Internal entry point for validation. - * @internal - */ - function defineEnv( -@@ -46,14 +137,14 @@ function defineEnv( - ): InferType { - const env = config.env ?? process.env; - const isStandard = !!(def as any)?.["~standard"]; -- const isArkCompiled = typeof def === "function" && "assert" in (def as any); -+ const isArkCompiled = -+ (typeof def === "function" && "assert" in (def as any)) || -+ (typeof def === "object" && def !== null && "invoke" in (def as any)); - -- const adapter = -+ const result = - isStandard && !isArkCompiled -- ? new StandardSchemaAdapter(def as any) -- : new ArkTypeAdapter(def, config as any); -- -- const result = adapter.validate(env); -+ ? validateStandard(def, env) -+ : validateArkType(def, config, env); - - if (!result.success) { - throw new ArkEnvError(result.issues); -diff --git a/packages/arkenv/src/errors.ts b/packages/arkenv/src/errors.ts -index 1ee6c8a..7838acd 100644 ---- a/packages/arkenv/src/errors.ts -+++ b/packages/arkenv/src/errors.ts -@@ -1,4 +1,13 @@ --import type { EnvIssue } from "./adapters"; -+/** -+ * Normalized issue format for ArkEnv. -+ * @internal -+ */ -+export type EnvIssue = { -+ path: string[]; -+ message: string; -+ validator: "arktype" | "standard"; -+}; -+ - import { indent } from "./utils/indent"; - import { styleText } from "./utils/style-text"; - -@@ -30,8 +39,8 @@ export class ArkEnvError extends Error { - Object.defineProperty(ArkEnvError, "name", { value: "ArkEnvError" }); - - /** -- * Format the issues returned by ArkType for display -- * @param issues - The issues returned by ArkType -+ * Format the issues for display -+ * @param issues - The issues found during validation - * @returns A string of the formatted issues - */ - export function formatIssues(issues: EnvIssue[]): string { From d56066b7bbb08275e81be9b70b8afd31e7bafd04 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 18:56:19 +0500 Subject: [PATCH 035/117] build: update zod dependency to 4.3.5 --- pnpm-lock.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b563412a..c811ca553 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -126,7 +126,7 @@ importers: version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) zod: specifier: 'catalog:' - version: 4.2.1 + version: 4.3.5 apps/playgrounds/bun: dependencies: @@ -580,7 +580,7 @@ importers: version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) zod: specifier: 'catalog:' - version: 4.2.1 + version: 4.3.5 packages/bun-plugin: dependencies: From 54281c6d9958de405ad6e29e065fe2a473efc35b Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 18:56:29 +0500 Subject: [PATCH 036/117] refactor(test): use toThrow for error assertions - Replaced manual try/catch blocks - Used Jest's toThrow for clarity - Improved test readability and concisen --- packages/arkenv/src/index.test.ts | 34 ++++++++++++++++--------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/packages/arkenv/src/index.test.ts b/packages/arkenv/src/index.test.ts index a6c0ca3f9..dd112c209 100644 --- a/packages/arkenv/src/index.test.ts +++ b/packages/arkenv/src/index.test.ts @@ -47,29 +47,31 @@ describe("index.ts exports", () => { }); it("should throw ArkEnvError with default import when validation fails", () => { - try { + expect(() => arkenvDefault({ MISSING_DEFAULT_VAR: "string", - }); - expect.fail("Should have thrown an error"); - } catch (error: any) { - expect(error).toBeInstanceOf(ArkEnvError); - expect(error.message).toContain("MISSING_DEFAULT_VAR"); - expect(error.message).toContain("string"); - } + }), + ).toThrow(ArkEnvError); + + expect(() => + arkenvDefault({ + MISSING_DEFAULT_VAR: "string", + }), + ).toThrow(/MISSING_DEFAULT_VAR.*string/); }); it("should throw ArkEnvError with named import when validation fails", () => { - try { + expect(() => + arkenvNamed({ + MISSING_NAMED_VAR: "string", + }), + ).toThrow(ArkEnvError); + + expect(() => arkenvNamed({ MISSING_NAMED_VAR: "string", - }); - expect.fail("Should have thrown an error"); - } catch (error: any) { - expect(error).toBeInstanceOf(ArkEnvError); - expect(error.message).toContain("MISSING_NAMED_VAR"); - expect(error.message).toContain("string"); - } + }), + ).toThrow(/MISSING_NAMED_VAR.*string/); }); it("should have same behavior for both default and named imports", () => { From 7fcd1d21f876e0511e67daa4b1fd19289615737f Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 18:56:39 +0500 Subject: [PATCH 037/117] test(arkenv): refactor error throwing tests - Replaced try...catch blocks - Used expect().toThrow() matchers - Improved test readability and conciseness --- packages/arkenv/src/create-env.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 3497b1518..6996680be 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -83,7 +83,10 @@ function validateArkType( success: false, issues: Object.entries(result.byPath).map(([path, error]) => ({ path: path ? path.split(".") : [], - message: (error as { message: string }).message, + message: + typeof error === "object" && error !== null && "message" in error + ? String((error as any).message) + : "Validation failed", validator: "arktype" as const, })), }; From 80f4f838ed8438335f42770f4344153523af0dd7 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 19:13:10 +0500 Subject: [PATCH 038/117] feat(arkenv): optimize for optional arktype and standard schema support --- apps/www/content/docs/arkenv/coercion.mdx | 2 +- .../src/array-defaults.integration.test.ts | 10 +- .../arkenv/src/coercion.integration.test.ts | 40 ++--- packages/arkenv/src/create-env.test.ts | 124 +++++++------- packages/arkenv/src/create-env.ts | 64 +++---- .../src/custom-types.integration.test.ts | 68 ++++---- packages/arkenv/src/error.integration.test.ts | 20 +-- packages/arkenv/src/index.test.ts | 49 +----- packages/arkenv/src/index.ts | 6 +- .../src/object-parsing.integration.test.ts | 30 ++-- packages/arkenv/src/standard-schema.test.ts | 12 +- packages/arkenv/src/type.test.ts | 4 +- packages/arkenv/src/utils/coerce.ts | 161 +++++++++++------- packages/bun-plugin/src/index.ts | 4 +- packages/internal/keywords/package.json | 13 +- packages/internal/keywords/src/functions.ts | 35 ---- packages/internal/keywords/src/index.ts | 33 +++- packages/internal/keywords/tsdown.config.ts | 1 - packages/vite-plugin/src/index.test.ts | 54 +++--- packages/vite-plugin/src/index.ts | 4 +- 20 files changed, 349 insertions(+), 385 deletions(-) delete mode 100644 packages/internal/keywords/src/functions.ts diff --git a/apps/www/content/docs/arkenv/coercion.mdx b/apps/www/content/docs/arkenv/coercion.mdx index 0dd76de9e..3eca112dc 100644 --- a/apps/www/content/docs/arkenv/coercion.mdx +++ b/apps/www/content/docs/arkenv/coercion.mdx @@ -94,7 +94,7 @@ Coercion works recursively: even variables inside a JSON object are coerced to t ## How it works -ArkEnv introspects your schema to identify fields that should be numbers, booleans, and other non-string types you define. When you call `arkenv()`, it pre-processes your environment variables to perform these conversions *before* validation. +ArkEnv introspects your schema to identify fields that should be numbers, booleans, and other non-string types you define. When you call `createEnv()`, it pre-processes your environment variables to perform these conversions *before* validation. ## Performance diff --git a/packages/arkenv/src/array-defaults.integration.test.ts b/packages/arkenv/src/array-defaults.integration.test.ts index 1a780faae..ab4f2e827 100644 --- a/packages/arkenv/src/array-defaults.integration.test.ts +++ b/packages/arkenv/src/array-defaults.integration.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from "vitest"; -import arkenv, { type } from "./index"; +import createEnv, { type } from "./index"; -describe("arkenv array defaults", () => { +describe("createEnv array defaults", () => { it("should work with arrow function array defaults", () => { - const Thing = arkenv({ + const Thing = createEnv({ array: type("number.integer[]").default(() => []), }); @@ -11,7 +11,7 @@ describe("arkenv array defaults", () => { }); it("should work with complex array defaults", () => { - const env = arkenv( + const env = createEnv( { ALLOWED_HOSTS: type("string[]").default(() => [ "localhost", @@ -29,7 +29,7 @@ describe("arkenv array defaults", () => { }); it("should support arrays with defaults and environment overrides", () => { - const env = arkenv( + const env = createEnv( { NUMBERS: type("number[]").default(() => [1, 2, 3]), }, diff --git a/packages/arkenv/src/coercion.integration.test.ts b/packages/arkenv/src/coercion.integration.test.ts index b22366b3a..b2918b202 100644 --- a/packages/arkenv/src/coercion.integration.test.ts +++ b/packages/arkenv/src/coercion.integration.test.ts @@ -1,16 +1,16 @@ import { describe, expect, it } from "vitest"; -import { arkenv } from "./create-env"; +import { createEnv } from "./create-env"; import { type } from "./index"; describe("coercion integration", () => { it("should coerce and validate numbers", () => { - const env = arkenv({ PORT: "number" }, { env: { PORT: "3000" } }); + const env = createEnv({ PORT: "number" }, { env: { PORT: "3000" } }); expect(env.PORT).toBe(3000); expect(typeof env.PORT).toBe("number"); }); it("should coerce and validate booleans", () => { - const env = arkenv( + const env = createEnv( { DEBUG: "boolean", VERBOSE: "boolean" }, { env: { DEBUG: "true", VERBOSE: "false" } }, ); @@ -19,19 +19,19 @@ describe("coercion integration", () => { }); it("should coerce and validate number subtypes (port)", () => { - const env = arkenv({ PORT: "number.port" }, { env: { PORT: "8080" } }); + const env = createEnv({ PORT: "number.port" }, { env: { PORT: "8080" } }); expect(env.PORT).toBe(8080); }); it("should fail validation if coercion fails (not a number)", () => { expect(() => - arkenv({ PORT: "number" }, { env: { PORT: "abc" } }), + createEnv({ PORT: "number" }, { env: { PORT: "abc" } }), ).toThrow(); }); it("should fail validation if value is valid number but invalid subtype", () => { expect(() => - arkenv( + createEnv( { PORT: "number.port" }, { env: { PORT: "99999" } }, // Too large for port ), @@ -39,7 +39,7 @@ describe("coercion integration", () => { }); it("should work with mixed coerced and non-coerced values", () => { - const env = arkenv( + const env = createEnv( { PORT: "number", HOST: "string", @@ -60,13 +60,13 @@ describe("coercion integration", () => { it("should coerce when using compiled type definitions", () => { const schema = type({ PORT: "number" }); - const env = arkenv(schema, { env: { PORT: "3000" } }); + const env = createEnv(schema, { env: { PORT: "3000" } }); expect(env.PORT).toBe(3000); expect(typeof env.PORT).toBe("number"); }); it("should coerce number subtypes in mapping", () => { - const env = arkenv( + const env = createEnv( { PORT: "number.port", COUNT: "number.integer", @@ -79,12 +79,12 @@ describe("coercion integration", () => { it("should fail compiled type validation if coercion fails", () => { const schema = type({ PORT: "number" }); - expect(() => arkenv(schema, { env: { PORT: "abc" } })).toThrow(); + expect(() => createEnv(schema, { env: { PORT: "abc" } })).toThrow(); }); it("should work with other number sub-keywords like epoch", () => { const ts = "1678886400000"; - const env = arkenv({ TS: "number.epoch" }, { env: { TS: ts } }); + const env = createEnv({ TS: "number.epoch" }, { env: { TS: ts } }); expect(env.TS).toBe(1678886400000); }); @@ -101,26 +101,26 @@ describe("coercion integration", () => { }); it("should coerce and validate strict number literals", () => { - const env = arkenv({ VAL: "1 | 2" }, { env: { VAL: "1" } }); + const env = createEnv({ VAL: "1 | 2" }, { env: { VAL: "1" } }); expect(env.VAL).toBe(1); }); it("should coerce and validate strict boolean literals", () => { - const env = arkenv({ DEBUG: "true" }, { env: { DEBUG: "true" } }); + const env = createEnv({ DEBUG: "true" }, { env: { DEBUG: "true" } }); expect(env.DEBUG).toBe(true); }); it("should NOT coerce empty or whitespace strings to 0 for numbers", () => { - expect(() => arkenv({ VAL: "number" }, { env: { VAL: "" } })).toThrow(); - expect(() => arkenv({ VAL: "number" }, { env: { VAL: " " } })).toThrow(); + expect(() => createEnv({ VAL: "number" }, { env: { VAL: "" } })).toThrow(); + expect(() => createEnv({ VAL: "number" }, { env: { VAL: " " } })).toThrow(); }); it("should fail validation if coercion fails (not a boolean)", () => { expect(() => - arkenv({ DEBUG: "boolean" }, { env: { DEBUG: "yes" } }), + createEnv({ DEBUG: "boolean" }, { env: { DEBUG: "yes" } }), ).toThrow(); expect(() => - arkenv({ DEBUG: "boolean" }, { env: { DEBUG: "1" } }), + createEnv({ DEBUG: "boolean" }, { env: { DEBUG: "1" } }), ).toThrow(); }); @@ -132,7 +132,7 @@ describe("coercion integration", () => { ), }); - const env = arkenv(Env, { + const env = createEnv(Env, { env: { PORT: "3000", VITE_MY_NUMBER_MANUAL: "456", @@ -144,7 +144,7 @@ describe("coercion integration", () => { }); it("should handle mixed features: defaults, coercion, array format, and stripping", () => { - const env = arkenv( + const env = createEnv( { // Default used if missing DEFAULT_ARR: type("string[]").default(() => ["default"]), @@ -176,7 +176,7 @@ describe("coercion integration", () => { it("should provide actionable error message for array parsing failure", () => { expect(() => { - arkenv( + createEnv( { TAGS: "string[]" }, { env: { TAGS: '["invalid-json' }, diff --git a/packages/arkenv/src/create-env.test.ts b/packages/arkenv/src/create-env.test.ts index 4f2d8cefb..f0def3abc 100644 --- a/packages/arkenv/src/create-env.test.ts +++ b/packages/arkenv/src/create-env.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { z } from "zod"; -import { arkenv } from "./create-env"; +import { createEnv } from "./create-env"; import { type } from "./type"; import { indent, styleText } from "./utils"; @@ -25,7 +25,7 @@ const expectedError = ( return formattedErrors.join("\n"); }; -describe("arkenv", () => { +describe("createEnv", () => { let originalEnv: NodeJS.ProcessEnv; beforeEach(() => { @@ -37,13 +37,13 @@ describe("arkenv", () => { }); describe("coercion", () => { it("should coerce number from string", () => { - const env = arkenv({ PORT: "number" }, { env: { PORT: "3000" } }); + const env = createEnv({ PORT: "number" }, { env: { PORT: "3000" } }); expect(env.PORT).toBe(3000); expect(typeof env.PORT).toBe("number"); }); it("should coerce boolean from string", () => { - const env = arkenv( + const env = createEnv( { DEBUG: "boolean", VERBOSE: "boolean" }, { env: { DEBUG: "true", VERBOSE: "false" } }, ); @@ -52,7 +52,7 @@ describe("arkenv", () => { }); it("should coerce number.integer from string", () => { - const env = arkenv( + const env = createEnv( { COUNT: "number.integer" }, { env: { COUNT: "123" } }, ); @@ -60,24 +60,24 @@ describe("arkenv", () => { }); it("should coerce numeric ranges from string", () => { - const env = arkenv({ AGE: "number >= 18" }, { env: { AGE: "21" } }); + const env = createEnv({ AGE: "number >= 18" }, { env: { AGE: "21" } }); expect(env.AGE).toBe(21); }); it("should coerce numeric divisors from string", () => { - const env = arkenv({ EVEN: "number % 2" }, { env: { EVEN: "4" } }); + const env = createEnv({ EVEN: "number % 2" }, { env: { EVEN: "4" } }); expect(env.EVEN).toBe(4); }); it("should work with optional coerced properties", () => { const schema = { "PORT?": "number" } as const; - expect(arkenv(schema, { env: { PORT: "3000" } }).PORT).toBe(3000); - expect(arkenv(schema, { env: {} }).PORT).toBeUndefined(); + expect(createEnv(schema, { env: { PORT: "3000" } }).PORT).toBe(3000); + expect(createEnv(schema, { env: {} }).PORT).toBeUndefined(); }); it("should handle validation errors from mapping", () => { expect(() => { - arkenv( + createEnv( { INVALID_PORT: "number" }, { env: { INVALID_PORT: "not-a-number" } }, ); @@ -85,7 +85,7 @@ describe("arkenv", () => { }); it("should work with schemas containing morphs in mapping", () => { - const env = arkenv( + const env = createEnv( { PORT: "number.port", VITE_MY_NUMBER_MANUAL: type("string").pipe((str) => @@ -107,12 +107,12 @@ describe("arkenv", () => { describe("numeric keywords", () => { it("should coerce number", () => { - const env = arkenv({ VAL: "number" }, { env: { VAL: "123.456" } }); + const env = createEnv({ VAL: "number" }, { env: { VAL: "123.456" } }); expect(env.VAL).toBe(123.456); }); it("should coerce number.Infinity", () => { - const env = arkenv( + const env = createEnv( { VAL: "number.Infinity" }, { env: { VAL: "Infinity" } }, ); @@ -121,12 +121,12 @@ describe("arkenv", () => { // TODO: Support NaN coercion // it("should coerce number.NaN", () => { - // const env = arkenv({ VAL: "number.NaN" }, { VAL: "NaN" }); + // const env = createEnv({ VAL: "number.NaN" }, { VAL: "NaN" }); // expect(env.VAL).toBeNaN(); // }); it("should coerce number.NegativeInfinity", () => { - const env = arkenv( + const env = createEnv( { VAL: "number.NegativeInfinity" }, { env: { VAL: "-Infinity" } }, ); @@ -134,7 +134,7 @@ describe("arkenv", () => { }); it("should coerce number.epoch", () => { - const env = arkenv( + const env = createEnv( { VAL: "number.epoch" }, { env: { VAL: "1640995200000" } }, ); @@ -142,12 +142,12 @@ describe("arkenv", () => { }); it("should coerce number.integer", () => { - const env = arkenv({ VAL: "number.integer" }, { env: { VAL: "42" } }); + const env = createEnv({ VAL: "number.integer" }, { env: { VAL: "42" } }); expect(env.VAL).toBe(42); }); it("should coerce number.safe", () => { - const env = arkenv( + const env = createEnv( { VAL: "number.safe" }, { env: { VAL: "9007199254740991" } }, ); @@ -157,32 +157,32 @@ describe("arkenv", () => { describe("defaults and empty strings", () => { it("should use defaults when value is missing", () => { - const env = arkenv({ FOO: "string = 'bar'" }, { env: {} }); + const env = createEnv({ FOO: "string = 'bar'" }, { env: {} }); expect(env.FOO).toBe("bar"); }); it("should treat empty string as empty string for string types", () => { - const env = arkenv({ VAL: "string" }, { env: { VAL: "" } }); + const env = createEnv({ VAL: "string" }, { env: { VAL: "" } }); expect(env.VAL).toBe(""); }); it("should throw for empty string when number is expected", () => { - expect(() => arkenv({ VAL: "number" }, { env: { VAL: "" } })).toThrow(); + expect(() => createEnv({ VAL: "number" }, { env: { VAL: "" } })).toThrow(); }); it("should throw for empty string when boolean is expected", () => { - expect(() => arkenv({ VAL: "boolean" }, { env: { VAL: "" } })).toThrow(); + expect(() => createEnv({ VAL: "boolean" }, { env: { VAL: "" } })).toThrow(); }); it("should allow empty strings when the schema is unknown", () => { - const env = arkenv({ VAL: "unknown" }, { env: { VAL: "" } }); + const env = createEnv({ VAL: "unknown" }, { env: { VAL: "" } }); expect(env.VAL).toBe(""); }); }); describe("standard array syntax", () => { it("should parse string[] from comma-separated string", () => { - const env = arkenv( + const env = createEnv( { TAGS: "string[]" }, { env: { TAGS: "foo,bar,baz" } }, ); @@ -190,7 +190,7 @@ describe("arkenv", () => { }); it("should parse number[] from comma-separated string", () => { - const env = arkenv( + const env = createEnv( { PORTS: "number[]" }, { env: { PORTS: "3000, 8080" } }, ); @@ -198,7 +198,7 @@ describe("arkenv", () => { }); it("should parse boolean[] from comma-separated string", () => { - const env = arkenv( + const env = createEnv( { FLAGS: "boolean[]" }, { env: { FLAGS: "true, false" } }, ); @@ -206,7 +206,7 @@ describe("arkenv", () => { }); it("should parse (string|number)[] from mixed string", () => { - const env = arkenv( + const env = createEnv( { MIXED: "(string|number)[]" }, { env: { MIXED: "foo, 123, bar" } }, ); @@ -214,7 +214,7 @@ describe("arkenv", () => { }); it("should parse (string|number)[] with only numbers", () => { - const env = arkenv( + const env = createEnv( { MIXED: "(string|number)[]" }, { env: { MIXED: "1, 2, 3" } }, ); @@ -222,7 +222,7 @@ describe("arkenv", () => { }); it("should parse (string|boolean)[] with mixed values", () => { - const env = arkenv( + const env = createEnv( { MIXED: "(string|boolean)[]" }, { env: { MIXED: "true, foo, false" } }, ); @@ -233,7 +233,7 @@ describe("arkenv", () => { it("should validate string env variables", () => { process.env.TEST_STRING = "hello"; - const env = arkenv({ + const env = createEnv({ TEST_STRING: "string", }); @@ -242,7 +242,7 @@ describe("arkenv", () => { it("should throw when required env variable is missing", () => { expect(() => - arkenv({ + createEnv({ MISSING_VAR: "string", }), ).toThrow( @@ -260,7 +260,7 @@ describe("arkenv", () => { process.env.WRONG_TYPE = "not a number"; expect(() => - arkenv({ + createEnv({ WRONG_TYPE: "number", }), ).toThrow( @@ -279,7 +279,7 @@ describe("arkenv", () => { TEST_STRING: "hello", }; - const { TEST_STRING } = arkenv( + const { TEST_STRING } = createEnv( { TEST_STRING: "string", }, @@ -290,7 +290,7 @@ describe("arkenv", () => { }); it("should support array types with default values", () => { - const env = arkenv( + const env = createEnv( { NUMBERS: type("number[]").default(() => [1, 2, 3]), STRINGS: type("string[]").default(() => ["a", "b"]), @@ -304,7 +304,7 @@ describe("arkenv", () => { it("should support array types with defaults when no environment value provided", () => { // Test default value usage when environment variable is not set - const env = arkenv( + const env = createEnv( { NUMBERS: type("number[]").default(() => [1, 2, 3]), }, @@ -316,7 +316,7 @@ describe("arkenv", () => { describe("type definitions", () => { it("should infer types from a mapping", () => { - const env = arkenv( + const env = createEnv( { TEST_STRING: "string", TEST_PORT: "number", @@ -331,7 +331,7 @@ describe("arkenv", () => { }); it("should provide correct type inference with type definitions", () => { - const env = arkenv( + const env = createEnv( { TEST_STRING: "string", TEST_PORT: "number.port", @@ -353,8 +353,8 @@ describe("arkenv", () => { const base = { TEST_STRING: "string" } as const; const extended = { ...base, TEST_PORT: "number" } as const; - const env1 = arkenv(base, { env: { TEST_STRING: "test" } }); - const env2 = arkenv(extended, { + const env1 = createEnv(base, { env: { TEST_STRING: "test" } }); + const env2 = createEnv(extended, { env: { TEST_STRING: "test", TEST_PORT: "3000" }, }); @@ -366,7 +366,7 @@ describe("arkenv", () => { process.env.INVALID_PORT = "not-a-port"; expect(() => - arkenv({ + createEnv({ INVALID_PORT: "number.port", }), ).toThrow(/INVALID_PORT/); @@ -374,7 +374,7 @@ describe("arkenv", () => { it("should support custom environment and mapping", () => { const customEnv = { HOST: "localhost", PORT: "8080" }; - const env = arkenv( + const env = createEnv( { HOST: "string", PORT: "number" }, { env: customEnv }, ); @@ -387,7 +387,7 @@ describe("arkenv", () => { describe("options", () => { it("should disable coercion when coerce is set to false", () => { expect(() => - arkenv( + createEnv( { NUMBER: "number", }, @@ -406,7 +406,7 @@ describe("arkenv", () => { process.env = { ...originalEnv, TEST_NUM: "123" }; try { expect(() => - arkenv({ TEST_NUM: "number" }, { coerce: false }), + createEnv({ TEST_NUM: "number" }, { coerce: false }), ).toThrow(); } finally { process.env = originalEnv; @@ -414,7 +414,7 @@ describe("arkenv", () => { }); it("should allow string values when coercion is disabled if schema expects strings", () => { - const env = arkenv( + const env = createEnv( { VAL: "string", }, @@ -429,7 +429,7 @@ describe("arkenv", () => { }); it("should strip extra keys that are not defined in the schema by default", () => { - const env = arkenv( + const env = createEnv( { HOST: "string" }, { env: { @@ -443,7 +443,7 @@ describe("arkenv", () => { }); it("should preserve extra keys when onUndeclaredKey is set to 'ignore'", () => { - const env = arkenv( + const env = createEnv( { HOST: "string" }, { env: { @@ -458,7 +458,7 @@ describe("arkenv", () => { it("should throw when onUndeclaredKey is set to 'reject' and extra keys are present", () => { expect(() => - arkenv( + createEnv( { HOST: "string" }, { env: { @@ -472,7 +472,7 @@ describe("arkenv", () => { }); it("should explicitly delete extra keys when onUndeclaredKey is set to 'delete'", () => { - const env = arkenv( + const env = createEnv( { HOST: "string" }, { env: { @@ -489,7 +489,7 @@ describe("arkenv", () => { describe("array format configuration", () => { it("should parse arrays as JSON when arrayFormat is 'json'", () => { - const env = arkenv( + const env = createEnv( { TAGS: "string[]" }, { env: { TAGS: '["foo", "bar"]' }, @@ -501,7 +501,7 @@ describe("arkenv", () => { it("should fail validation if arrayFormat is 'json' but value is not valid JSON array", () => { expect(() => { - arkenv( + createEnv( { TAGS: "string[]" }, { env: { TAGS: "foo,bar" }, // Comma separated, not JSON @@ -512,7 +512,7 @@ describe("arkenv", () => { }); it("should default to comma separation", () => { - const env = arkenv( + const env = createEnv( { TAGS: "string[]" }, { env: { TAGS: "foo,bar" }, @@ -523,7 +523,7 @@ describe("arkenv", () => { }); it("should parse numeric arrays as JSON", () => { - const env = arkenv( + const env = createEnv( { NUMBERS: "number[]" }, { env: { NUMBERS: "[1, 2, 3]" }, @@ -534,17 +534,17 @@ describe("arkenv", () => { }); it("should handle empty comma-separated string", () => { - const env = arkenv({ TAGS: "string[]" }, { env: { TAGS: "" } }); + const env = createEnv({ TAGS: "string[]" }, { env: { TAGS: "" } }); expect(env.TAGS).toEqual([]); }); it("should handle single-element array", () => { - const env = arkenv({ TAGS: "string[]" }, { env: { TAGS: "only-one" } }); + const env = createEnv({ TAGS: "string[]" }, { env: { TAGS: "only-one" } }); expect(env.TAGS).toEqual(["only-one"]); }); it("should handle empty JSON array", () => { - const env = arkenv( + const env = createEnv( { TAGS: "string[]" }, { env: { TAGS: "[]" }, @@ -557,7 +557,7 @@ describe("arkenv", () => { describe("object coercion", () => { it("should parse an object from a JSON string", () => { - const env = arkenv( + const env = createEnv( { DATABASE: { HOST: "string", PORT: "number" } }, { env: { @@ -571,7 +571,7 @@ describe("arkenv", () => { }); it("should handle nested object coercion", () => { - const env = arkenv( + const env = createEnv( { CONFIG: { DB: { PORT: "number" }, APP: { NAME: "string" } } }, { env: { @@ -587,7 +587,7 @@ describe("arkenv", () => { it("should fail validation if object is not valid JSON", () => { expect(() => { - arkenv( + createEnv( { DATABASE: { HOST: "string" } }, { env: { DATABASE: '{"HOST": "localhost"' } }, // Missing closing brace ); @@ -595,7 +595,7 @@ describe("arkenv", () => { }); it("should parse objects within arrays", () => { - const env = arkenv( + const env = createEnv( { SERVICES: type({ NAME: "string", PORT: "number" }).array() }, { env: { @@ -615,7 +615,7 @@ describe("arkenv", () => { describe("migration & hybrid support", () => { it("should work with top-level compiled ArkType schema", () => { const schema = type({ PORT: "number" }); - const env = arkenv(schema, { + const env = createEnv(schema, { env: { PORT: "3000" }, }); expect(env.PORT).toBe(3000); @@ -623,14 +623,14 @@ describe("arkenv", () => { it("should throw if top-level Standard Schema is passed directly", () => { expect(() => - arkenv(z.object({ PORT: z.coerce.number() }) as any, { + createEnv(z.object({ PORT: z.coerce.number() }) as any, { env: { PORT: "8080" }, }), ).toThrow(/expects a mapping.*not a top-level Standard Schema/); }); it("should support mixed ArkType DSL and Standard Schema validators", () => { - const env = arkenv( + const env = createEnv( { PORT: "number.port", HOST: z.string().min(1), diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 6996680be..cfbbd4c6d 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -150,72 +150,46 @@ function validateStandard( } /** - * Internal entry point for validation. - * @internal + * Create an environment variables object from a schema and an environment. + * Now supports both ArkType and Standard Schema validators. */ -function defineEnv( - def: T, - config: ArkEnvConfig = {}, -): InferType { - const env = config.env ?? process.env; - const { isStandard, isArkCompiled } = detectValidatorType(def); - - const result = - isStandard && !isArkCompiled - ? validateStandard(def as any, env) - : validateArkType(def, config, env); - - if (!result.success) { - throw new ArkEnvError(result.issues); - } - - return result.value as any; -} - -/** - * The primary entry point for ArkEnv. - * - * arkenv({ - * PORT: "number.port", // ArkType DSL - * HOST: z.string().min(1) // Standard Schema (Zod) - * }) - * - * IMPORTANT: arkenv() expects an object mapping environment keys to validators. - * Standard Schema validators are supported inside the mapping for migration, - * but top-level wrapped schemas (like z.object()) are not supported at this entry point. - * - * Type inference quality depends on the validator used. - * ArkType provides the richest inference. - */ -export function arkenv( +export function createEnv( def: EnvSchema, config?: ArkEnvConfig, ): distill.Out>; -export function arkenv( +export function createEnv( def: T, config?: ArkEnvConfig, ): InferType; -export function arkenv( +export function createEnv( def: EnvSchema | EnvSchemaWithType, config: ArkEnvConfig = {}, ): distill.Out> | InferType { + const env = config.env ?? process.env; const { isStandard, isArkCompiled } = detectValidatorType(def); // Guardrail: Block top-level Standard Schema (Zod, Valibot, etc.) // Reusable type() schemas (ArkType) are allowed. if (isStandard && !isArkCompiled) { throw new Error( - "ArkEnv: arkenv() expects a mapping of { KEY: validator }, not a top-level Standard Schema (e.g. z.object()). " + + "ArkEnv: createEnv() expects a mapping of { KEY: validator }, not a top-level Standard Schema (e.g. z.object()). " + "Standard Schema validators are supported inside the mapping, or you can use ArkType's type() for top-level schemas.", ); } - return defineEnv(def as any, config) as any; + const result = + isStandard && !isArkCompiled + ? validateStandard(def as any, env) + : validateArkType(def, config, env); + + if (!result.success) { + throw new ArkEnvError(result.issues); + } + + return result.value as any; } /** - * @deprecated Use `arkenv` instead. + * @alias createEnv */ -const createEnv = arkenv; - -export { createEnv }; +export const arkenv = createEnv; diff --git a/packages/arkenv/src/custom-types.integration.test.ts b/packages/arkenv/src/custom-types.integration.test.ts index d17c888f3..bc3e0bfc9 100644 --- a/packages/arkenv/src/custom-types.integration.test.ts +++ b/packages/arkenv/src/custom-types.integration.test.ts @@ -1,38 +1,38 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { arkenv } from "./create-env"; +import { createEnv } from "./create-env"; import { type } from "./type"; -describe("arkenv + type + scope + types integration", () => { +describe("createEnv + type + scope + types integration", () => { afterEach(() => { vi.unstubAllEnvs(); }); describe("string.host integration", () => { - it("should validate localhost through arkenv", () => { + it("should validate localhost through createEnv", () => { vi.stubEnv("HOST", "localhost"); - const env = arkenv({ + const env = createEnv({ HOST: type("string.host"), }); expect(env.HOST).toBe("localhost"); }); - it("should validate IP address through arkenv", () => { + it("should validate IP address through createEnv", () => { vi.stubEnv("HOST", "127.0.0.1"); - const env = arkenv({ + const env = createEnv({ HOST: type("string.host"), }); expect(env.HOST).toBe("127.0.0.1"); }); - it("should throw ArkEnvError for invalid host through arkenv", () => { + it("should throw ArkEnvError for invalid host through createEnv", () => { vi.stubEnv("HOST", "invalid-host"); expect(() => - arkenv({ + createEnv({ HOST: type("string.host"), }), ).toThrow(/HOST/); @@ -40,10 +40,10 @@ describe("arkenv + type + scope + types integration", () => { }); describe("number.port integration", () => { - it("should validate valid port through arkenv", () => { + it("should validate valid port through createEnv", () => { vi.stubEnv("PORT", "8080"); - const env = arkenv({ + const env = createEnv({ PORT: type("number.port"), }); @@ -51,41 +51,41 @@ describe("arkenv + type + scope + types integration", () => { expect(typeof env.PORT).toBe("number"); }); - it("should validate port at boundary (0) through arkenv", () => { + it("should validate port at boundary (0) through createEnv", () => { vi.stubEnv("PORT", "0"); - const env = arkenv({ + const env = createEnv({ PORT: type("number.port"), }); expect(env.PORT).toBe(0); }); - it("should validate port at boundary (65535) through arkenv", () => { + it("should validate port at boundary (65535) through createEnv", () => { vi.stubEnv("PORT", "65535"); - const env = arkenv({ + const env = createEnv({ PORT: type("number.port"), }); expect(env.PORT).toBe(65535); }); - it("should throw ArkEnvError for invalid port through arkenv", () => { + it("should throw ArkEnvError for invalid port through createEnv", () => { vi.stubEnv("PORT", "99999"); expect(() => - arkenv({ + createEnv({ PORT: type("number.port"), }), ).toThrow(/PORT/); }); - it("should throw ArkEnvError for non-numeric port through arkenv", () => { + it("should throw ArkEnvError for non-numeric port through createEnv", () => { vi.stubEnv("PORT", "not-a-number"); expect(() => - arkenv({ + createEnv({ PORT: type("number.port"), }), ).toThrow(/PORT/); @@ -93,10 +93,10 @@ describe("arkenv + type + scope + types integration", () => { }); describe("boolean integration", () => { - it("should validate 'true' string through arkenv", () => { + it("should validate 'true' string through createEnv", () => { vi.stubEnv("DEBUG", "true"); - const env = arkenv({ + const env = createEnv({ DEBUG: type("boolean"), }); @@ -104,21 +104,21 @@ describe("arkenv + type + scope + types integration", () => { expect(typeof env.DEBUG).toBe("boolean"); }); - it("should validate 'false' string through arkenv", () => { + it("should validate 'false' string through createEnv", () => { vi.stubEnv("DEBUG", "false"); - const env = arkenv({ + const env = createEnv({ DEBUG: type("boolean"), }); expect(env.DEBUG).toBe(false); }); - it("should throw ArkEnvError for invalid boolean through arkenv", () => { + it("should throw ArkEnvError for invalid boolean through createEnv", () => { vi.stubEnv("DEBUG", "maybe"); expect(() => - arkenv({ + createEnv({ DEBUG: type("boolean"), }), ).toThrow(/DEBUG/); @@ -131,7 +131,7 @@ describe("arkenv + type + scope + types integration", () => { vi.stubEnv("PORT", "3000"); vi.stubEnv("DEBUG", "true"); - const env = arkenv({ + const env = createEnv({ HOST: type("string.host"), PORT: type("number.port"), DEBUG: type("boolean"), @@ -147,7 +147,7 @@ describe("arkenv + type + scope + types integration", () => { vi.stubEnv("PORT", "8080"); vi.stubEnv("DEBUG", "false"); - const env = arkenv({ + const env = createEnv({ HOST: type("string.host"), PORT: type("number.port"), DEBUG: type("boolean"), @@ -164,7 +164,7 @@ describe("arkenv + type + scope + types integration", () => { vi.stubEnv("DEBUG", "true"); expect(() => - arkenv({ + createEnv({ HOST: type("string.host"), PORT: type("number.port"), DEBUG: type("boolean"), @@ -178,7 +178,7 @@ describe("arkenv + type + scope + types integration", () => { vi.stubEnv("DEBUG", "maybe"); expect(() => - arkenv({ + createEnv({ HOST: type("string.host"), PORT: type("number.port"), DEBUG: type("boolean"), @@ -189,7 +189,7 @@ describe("arkenv + type + scope + types integration", () => { describe("custom types with defaults", () => { it("should use default value for host when missing", () => { - const env = arkenv({ + const env = createEnv({ HOST: type("string.host").default(() => "localhost"), }); @@ -197,7 +197,7 @@ describe("arkenv + type + scope + types integration", () => { }); it("should use default value for port when missing", () => { - const env = arkenv({ + const env = createEnv({ PORT: type("number.port").default(3000), }); @@ -205,7 +205,7 @@ describe("arkenv + type + scope + types integration", () => { }); it("should use default value for boolean when missing", () => { - const env = arkenv({ + const env = createEnv({ DEBUG: type("boolean").default(() => false), }); @@ -215,7 +215,7 @@ describe("arkenv + type + scope + types integration", () => { it("should validate custom type when provided instead of using default", () => { vi.stubEnv("HOST", "127.0.0.1"); - const env = arkenv({ + const env = createEnv({ HOST: type("string.host").default(() => "localhost"), }); @@ -224,11 +224,11 @@ describe("arkenv + type + scope + types integration", () => { }); describe("custom types in arrays", () => { - it("should handle custom types in arrays through arkenv", () => { + it("should handle custom types in arrays through createEnv", () => { vi.stubEnv("EMAILS", "test@example.com, admin@example.com"); const Email = type("string.email"); - const env = arkenv({ EMAILS: Email.array() }); + const env = createEnv({ EMAILS: Email.array() }); expect(env.EMAILS).toEqual(["test@example.com", "admin@example.com"]); }); }); diff --git a/packages/arkenv/src/error.integration.test.ts b/packages/arkenv/src/error.integration.test.ts index 39c2eaeb0..ae52e8dad 100644 --- a/packages/arkenv/src/error.integration.test.ts +++ b/packages/arkenv/src/error.integration.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { arkenv } from "./create-env"; +import { createEnv } from "./create-env"; import { ArkEnvError } from "./errors"; import { type } from "./type"; @@ -7,7 +7,7 @@ import { type } from "./type"; const stripAnsi = (str: string) => str.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g"), ""); -describe("arkenv + type + errors + utils integration", () => { +describe("createEnv + type + errors + utils integration", () => { afterEach(() => { vi.unstubAllEnvs(); }); @@ -17,7 +17,7 @@ describe("arkenv + type + errors + utils integration", () => { vi.stubEnv("PORT", "not-a-number"); try { - arkenv({ + createEnv({ PORT: type("number.port"), }); expect.fail("Should have thrown"); @@ -38,7 +38,7 @@ describe("arkenv + type + errors + utils integration", () => { vi.stubEnv("HOST", "invalid-host"); try { - arkenv({ + createEnv({ HOST: type("string.host"), }); expect.fail("Should have thrown"); @@ -56,7 +56,7 @@ describe("arkenv + type + errors + utils integration", () => { vi.stubEnv("DEBUG", "maybe"); try { - arkenv({ + createEnv({ DEBUG: type("boolean"), }); expect.fail("Should have thrown"); @@ -73,7 +73,7 @@ describe("arkenv + type + errors + utils integration", () => { it("should throw ArkEnvError with formatted message for missing required variable", () => { try { - arkenv({ + createEnv({ REQUIRED_VAR: "string", }); expect.fail("Should have thrown"); @@ -94,7 +94,7 @@ describe("arkenv + type + errors + utils integration", () => { vi.stubEnv("DEBUG", "maybe"); try { - arkenv({ + createEnv({ HOST: type("string.host"), PORT: type("number.port"), DEBUG: type("boolean"), @@ -117,7 +117,7 @@ describe("arkenv + type + errors + utils integration", () => { vi.stubEnv("PORT", "not-a-number"); try { - arkenv({ + createEnv({ PORT: type("number.port"), }); expect.fail("Should have thrown"); @@ -148,7 +148,7 @@ describe("arkenv + type + errors + utils integration", () => { vi.stubEnv("PORT", invalidValue); try { - arkenv({ + createEnv({ PORT: type("number.port"), }); expect.fail("Should have thrown"); @@ -167,7 +167,7 @@ describe("arkenv + type + errors + utils integration", () => { vi.stubEnv("PORT", "99999"); try { - arkenv({ + createEnv({ HOST: type("string.host"), PORT: type("number.port"), }); diff --git a/packages/arkenv/src/index.test.ts b/packages/arkenv/src/index.test.ts index dd112c209..e535c3714 100644 --- a/packages/arkenv/src/index.test.ts +++ b/packages/arkenv/src/index.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, expectTypeOf, it, vi } from "vitest"; -import arkenvDefault, { ArkEnvError, arkenv as arkenvNamed } from "./index"; +import createEnv, { ArkEnvError } from "./index"; describe("index.ts exports", () => { afterEach(() => { @@ -8,25 +8,20 @@ describe("index.ts exports", () => { vi.unstubAllEnvs(); }); - it("should export arkenv as default export", () => { - expect(arkenvDefault).toBe(arkenvNamed); - expect(typeof arkenvDefault).toBe("function"); + it("should export createEnv as default export", () => { + expect(typeof createEnv).toBe("function"); }); it("should have correct types for exported functions", () => { // Type assertion to verify exported function types - expectTypeOf(arkenvDefault).toBeFunction(); - expectTypeOf(arkenvNamed).toBeFunction(); - - // Verify they have the same type signature - expectTypeOf(arkenvDefault).toEqualTypeOf(arkenvNamed); + expectTypeOf(createEnv).toBeFunction(); }); it("should work with default import", () => { // Set test environment variable vi.stubEnv("TEST_DEFAULT_IMPORT", "test-value"); - const env = arkenvDefault({ + const env = createEnv({ TEST_DEFAULT_IMPORT: "string", }); @@ -34,55 +29,29 @@ describe("index.ts exports", () => { expect(typeof env.TEST_DEFAULT_IMPORT).toBe("string"); }); - it("should work with named import", () => { - // Set test environment variable - vi.stubEnv("TEST_NAMED_IMPORT", "test-value"); - - const env = arkenvNamed({ - TEST_NAMED_IMPORT: "string", - }); - - expect(env.TEST_NAMED_IMPORT).toBe("test-value"); - expect(typeof env.TEST_NAMED_IMPORT).toBe("string"); - }); - it("should throw ArkEnvError with default import when validation fails", () => { expect(() => - arkenvDefault({ + createEnv({ MISSING_DEFAULT_VAR: "string", }), ).toThrow(ArkEnvError); expect(() => - arkenvDefault({ + createEnv({ MISSING_DEFAULT_VAR: "string", }), ).toThrow(/MISSING_DEFAULT_VAR.*string/); }); - it("should throw ArkEnvError with named import when validation fails", () => { - expect(() => - arkenvNamed({ - MISSING_NAMED_VAR: "string", - }), - ).toThrow(ArkEnvError); - - expect(() => - arkenvNamed({ - MISSING_NAMED_VAR: "string", - }), - ).toThrow(/MISSING_NAMED_VAR.*string/); - }); - it("should have same behavior for both default and named imports", () => { // Set test environment variable vi.stubEnv("COMPARISON_TEST", "same-value"); - const envFromDefault = arkenvDefault({ + const envFromDefault = createEnv({ COMPARISON_TEST: "string", }); - const envFromNamed = arkenvNamed({ + const envFromNamed = createEnv({ COMPARISON_TEST: "string", }); diff --git a/packages/arkenv/src/index.ts b/packages/arkenv/src/index.ts index 81fef1817..d45dbaa0a 100644 --- a/packages/arkenv/src/index.ts +++ b/packages/arkenv/src/index.ts @@ -1,4 +1,4 @@ -import { arkenv, createEnv } from "./create-env"; +import { createEnv } from "./create-env"; export type { EnvSchema } from "./create-env"; @@ -7,7 +7,7 @@ export type { EnvSchema } from "./create-env"; * * {@link https://arkenv.js.org | ArkEnv} is a typesafe environment variables validator from editor to runtime. */ -export default arkenv; -export { arkenv, createEnv }; +export default createEnv; +export { createEnv }; export { ArkEnvError } from "./errors"; export { type } from "./type"; diff --git a/packages/arkenv/src/object-parsing.integration.test.ts b/packages/arkenv/src/object-parsing.integration.test.ts index c905527a7..4ebf22a2c 100644 --- a/packages/arkenv/src/object-parsing.integration.test.ts +++ b/packages/arkenv/src/object-parsing.integration.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it } from "vitest"; -import { arkenv } from "./create-env"; +import { createEnv } from "./create-env"; import { type } from "./index"; describe("object parsing integration", () => { it("should parse JSON string into object with string properties", () => { - const env = arkenv( + const env = createEnv( { CONFIG: { host: "string", @@ -25,7 +25,7 @@ describe("object parsing integration", () => { }); it("should parse JSON string and coerce nested number properties", () => { - const env = arkenv( + const env = createEnv( { DATABASE: { host: "string", @@ -47,7 +47,7 @@ describe("object parsing integration", () => { }); it("should parse JSON string and coerce nested boolean properties", () => { - const env = arkenv( + const env = createEnv( { SETTINGS: { enabled: "boolean", @@ -70,7 +70,7 @@ describe("object parsing integration", () => { }); it("should parse nested JSON objects", () => { - const env = arkenv( + const env = createEnv( { APP: { database: { @@ -101,7 +101,7 @@ describe("object parsing integration", () => { }); it("should handle object with mixed types", () => { - const env = arkenv( + const env = createEnv( { CONFIG: { host: "string", @@ -127,7 +127,7 @@ describe("object parsing integration", () => { }); it("should work with both JSON strings and separate environment variables", () => { - const env = arkenv( + const env = createEnv( { PORT: "number", DEBUG: "boolean", @@ -155,7 +155,7 @@ describe("object parsing integration", () => { it("should fail when JSON string is invalid", () => { expect(() => - arkenv( + createEnv( { CONFIG: { host: "string", @@ -172,7 +172,7 @@ describe("object parsing integration", () => { it("should fail when parsed object doesn't match schema", () => { expect(() => - arkenv( + createEnv( { CONFIG: { port: "number", @@ -188,7 +188,7 @@ describe("object parsing integration", () => { }); it("should work with optional object properties", () => { - const env = arkenv( + const env = createEnv( { "CONFIG?": { host: "string", @@ -203,7 +203,7 @@ describe("object parsing integration", () => { }); it("should parse optional object when provided", () => { - const env = arkenv( + const env = createEnv( { "CONFIG?": { host: "string", @@ -229,7 +229,7 @@ describe("object parsing integration", () => { }), }); - const env = arkenv(schema, { + const env = createEnv(schema, { env: { MY_OBJ: '{"foo": "baz", "bar": 123}', }, @@ -242,7 +242,7 @@ describe("object parsing integration", () => { }); it("should handle arrays within JSON objects", () => { - const env = arkenv( + const env = createEnv( { CONFIG: { hosts: "string[]", @@ -264,7 +264,7 @@ describe("object parsing integration", () => { }); it("should parse JSON with whitespace", () => { - const env = arkenv( + const env = createEnv( { CONFIG: { host: "string", @@ -283,7 +283,7 @@ describe("object parsing integration", () => { }); it("should handle object with default values", () => { - const env = arkenv( + const env = createEnv( { CONFIG: type({ host: "string", diff --git a/packages/arkenv/src/standard-schema.test.ts b/packages/arkenv/src/standard-schema.test.ts index 02a5b0657..3f4d531ba 100644 --- a/packages/arkenv/src/standard-schema.test.ts +++ b/packages/arkenv/src/standard-schema.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { z } from "zod"; -import { arkenv } from "./index"; +import createEnv from "./index"; describe("Standard Schema integration", () => { it("should throw if a top-level zod schema is passed directly", () => { @@ -10,14 +10,14 @@ describe("Standard Schema integration", () => { }); expect(() => - arkenv(schema as any, { + createEnv(schema as any, { env: { PORT: "3000" }, }), ).toThrow(/expects a mapping.*not a top-level Standard Schema/); }); - it("should support mixed validators in arkenv() mapping", () => { - const env = arkenv( + it("should support mixed validators in createEnv() mapping", () => { + const env = createEnv( { PORT: "number.port", // ArkType DSL HOST: z.string().min(1), // Zod @@ -33,8 +33,8 @@ describe("Standard Schema integration", () => { }); }); - it("should verify arkenv accepts a Standard Schema when ArkType is present (via mapping)", () => { - const env = arkenv( + it("should verify createEnv accepts a Standard Schema when ArkType is present (via mapping)", () => { + const env = createEnv( { ZOD_VAL: z.string().email(), }, diff --git a/packages/arkenv/src/type.test.ts b/packages/arkenv/src/type.test.ts index 3a16bffc1..2736e52be 100644 --- a/packages/arkenv/src/type.test.ts +++ b/packages/arkenv/src/type.test.ts @@ -40,7 +40,7 @@ describe("type", () => { expect(result2.OPTIONAL).toBeUndefined(); }); - it("should create a type with arkenv-specific host validation", () => { + it("should create a type with createEnv-specific host validation", () => { const envType = type({ HOST: "string.host" }); // Valid IP addresses @@ -58,7 +58,7 @@ describe("type", () => { expect(() => envType.assert({ HOST: "invalid-host" })).toThrow(); }); - it("should create a type with arkenv-specific port validation", () => { + it("should create a type with createEnv-specific port validation", () => { const envType = type({ PORT: "number.port" }); // Valid ports diff --git a/packages/arkenv/src/utils/coerce.ts b/packages/arkenv/src/utils/coerce.ts index 121531100..c63f4c411 100644 --- a/packages/arkenv/src/utils/coerce.ts +++ b/packages/arkenv/src/utils/coerce.ts @@ -1,8 +1,4 @@ -import { - maybeBooleanFunction as maybeBoolean, - maybeJsonFunction as maybeJson, - maybeNumberFunction as maybeNumber, -} from "@repo/keywords/functions"; +import { maybeBoolean, maybeJson, maybeNumber } from "@repo/keywords"; /** * A marker used in the coercion path to indicate that the target @@ -19,15 +15,29 @@ type CoercionTarget = { type: "primitive" | "array" | "object"; }; +/** + * Options for coercion behavior. + */ +export type CoerceOptions = { + /** + * format to use for array parsing + * @default "comma" + */ + arrayFormat?: "comma" | "json"; +}; + /** * Recursively find all paths in a JSON Schema that require coercion. */ const findCoercionPaths = ( - node: any, // JsonSchema + node: any, // Use any to avoid top-level JsonSchema export from arktype path: string[] = [], ): CoercionTarget[] => { const results: CoercionTarget[] = []; - if (typeof node === "boolean") return results; + + if (typeof node === "boolean") { + return results; + } if ("const" in node) { if (typeof node.const === "number" || typeof node.const === "boolean") { @@ -55,7 +65,11 @@ const findCoercionPaths = ( "properties" in node && node.properties && Object.keys(node.properties).length > 0; - if (hasProperties) results.push({ path: [...path], type: "object" }); + + if (hasProperties) { + results.push({ path: [...path], type: "object" }); + } + if ("properties" in node && node.properties) { for (const [key, prop] of Object.entries(node.properties)) { results.push(...findCoercionPaths(prop as any, [...path, key])); @@ -63,6 +77,7 @@ const findCoercionPaths = ( } } else if (node.type === "array") { results.push({ path: [...path], type: "array" }); + if ("items" in node && node.items) { if (Array.isArray(node.items)) { node.items.forEach((item: any, index: number) => { @@ -83,16 +98,21 @@ const findCoercionPaths = ( } if ("anyOf" in node && node.anyOf) { - for (const branch of node.anyOf) + for (const branch of node.anyOf) { results.push(...findCoercionPaths(branch as any, path)); + } } + if ("allOf" in node && node.allOf) { - for (const branch of node.allOf) + for (const branch of node.allOf) { results.push(...findCoercionPaths(branch as any, path)); + } } + if ("oneOf" in node && node.oneOf) { - for (const branch of node.oneOf) + for (const branch of node.oneOf) { results.push(...findCoercionPaths(branch as any, path)); + } } const seen = new Set(); @@ -104,17 +124,6 @@ const findCoercionPaths = ( }); }; -/** - * Options for coercion behavior. - */ -export type CoerceOptions = { - /** - * format to use for array parsing - * @default "comma" - */ - arrayFormat?: "comma" | "json"; -}; - /** * Apply coercion to a data object based on identified paths. */ @@ -124,6 +133,7 @@ const applyCoercion = ( options: CoerceOptions = {}, ) => { const { arrayFormat = "comma" } = options; + const splitString = (val: string) => { if (arrayFormat === "json") { try { @@ -132,6 +142,7 @@ const applyCoercion = ( return val; } } + if (!val.trim()) return []; return val.split(",").map((s) => s.trim()); }; @@ -139,12 +150,19 @@ const applyCoercion = ( if (typeof data !== "object" || data === null) { if (targets.some((t) => t.path.length === 0)) { const rootTarget = targets.find((t) => t.path.length === 0); - if (rootTarget?.type === "object" && typeof data === "string") + + if (rootTarget?.type === "object" && typeof data === "string") { return maybeJson(data); - if (rootTarget?.type === "array" && typeof data === "string") + } + + if (rootTarget?.type === "array" && typeof data === "string") { return splitString(data); + } + const asNumber = maybeNumber(data); - if (typeof asNumber === "number") return asNumber; + if (typeof asNumber === "number") { + return asNumber; + } return maybeBoolean(data); } return data; @@ -160,20 +178,25 @@ const applyCoercion = ( type: "primitive" | "array" | "object", ) => { if (!current || typeof current !== "object") return; - if (targetPath.length === 0) return; + + if (targetPath.length === 0) { + return; + } if (targetPath.length === 1) { const lastKey = targetPath[0]; + if (lastKey === ARRAY_ITEM_MARKER) { if (Array.isArray(current)) { for (let i = 0; i < current.length; i++) { const original = current[i]; if (type === "primitive") { const asNumber = maybeNumber(original); - current[i] = - typeof asNumber === "number" - ? asNumber - : maybeBoolean(original); + if (typeof asNumber === "number") { + current[i] = asNumber; + } else { + current[i] = maybeBoolean(original); + } } else if (type === "object") { current[i] = maybeJson(original); } @@ -183,69 +206,85 @@ const applyCoercion = ( } const record = current as Record; - // biome-ignore lint/suspicious/noPrototypeBuiltins: preserve existing logic if (Object.prototype.hasOwnProperty.call(record, lastKey)) { const original = record[lastKey]; + if (type === "array" && typeof original === "string") { record[lastKey] = splitString(original); - } else if (type === "object" && typeof original === "string") { + return; + } + + if (type === "object" && typeof original === "string") { record[lastKey] = maybeJson(original); - } else if (Array.isArray(original)) { + return; + } + + if (Array.isArray(original)) { if (type === "primitive") { for (let i = 0; i < original.length; i++) { - const asNumber = maybeNumber(original[i]); - original[i] = - typeof asNumber === "number" - ? asNumber - : maybeBoolean(original[i]); + const item = original[i]; + const asNumber = maybeNumber(item); + if (typeof asNumber === "number") { + original[i] = asNumber; + } else { + original[i] = maybeBoolean(item); + } + } + } + } else { + if (type === "primitive") { + const asNumber = maybeNumber(original); + if (typeof asNumber === "number") { + record[lastKey] = asNumber; + } else { + record[lastKey] = maybeBoolean(original); } } - } else if (type === "primitive") { - const asNumber = maybeNumber(original); - record[lastKey] = - typeof asNumber === "number" ? asNumber : maybeBoolean(original); } } return; } const [nextKey, ...rest] = targetPath; + if (nextKey === ARRAY_ITEM_MARKER) { if (Array.isArray(current)) { - for (const item of current) walk(item, rest, type); + for (const item of current) { + walk(item, rest, type); + } } - } else { - walk((current as Record)[nextKey], rest, type); + return; } + + const record = current as Record; + walk(record[nextKey], rest, type); }; - for (const target of sortedTargets) walk(data, target.path, target.type); + for (const target of sortedTargets) { + walk(data, target.path, target.type); + } + return data; }; -/** - * COERCION SYSTEM (Lazy Loading Support) - * - * This implementation uses JSON Schema introspection to identify paths requiring coercion. - * It is designed to be called after ArkType is lazily loaded. - * - * NOTE: Internal types are stripped of explicit ArkType imports to prevent - * accidental top-level dependency loading. - */ - /** * Create a coercing wrapper around an ArkType schema using JSON Schema introspection. */ export function coerce( - type: any, // Injected dependency to avoid top-level import - schema: any, // Injected dependency to avoid top-level import + type: any, // Injected to avoid top-level import + schema: any, // Injected to avoid top-level import options?: CoerceOptions, ): any { - const json = schema.in.toJsonSchema({ fallback: (ctx: any) => ctx.base }); + const json = schema.in.toJsonSchema({ + fallback: (ctx: any) => ctx.base, + }); const targets = findCoercionPaths(json); - if (targets.length === 0) return schema; + + if (targets.length === 0) { + return schema; + } return type("unknown") - .pipe((data: unknown) => applyCoercion(data, targets, options)) + .pipe((data: any) => applyCoercion(data, targets, options)) .pipe(schema); } diff --git a/packages/bun-plugin/src/index.ts b/packages/bun-plugin/src/index.ts index 09775b2dc..b2edd5cfd 100644 --- a/packages/bun-plugin/src/index.ts +++ b/packages/bun-plugin/src/index.ts @@ -1,7 +1,7 @@ import { join } from "node:path"; import type { EnvSchemaWithType, SchemaShape } from "@repo/types"; import type { EnvSchema } from "arkenv"; -import { arkenv as validateEnv } from "arkenv"; +import { createEnv } from "arkenv"; import type { BunPlugin, Loader, PluginBuilder } from "bun"; export type { ProcessEnvAugmented } from "./types"; @@ -18,7 +18,7 @@ export function processEnvSchema( // "Type instantiation is excessively deep and possibly infinite" errors. // Ideally, we should find a way to avoid these assertions while maintaining type safety. - const env = validateEnv(options as any, { env: process.env }); + const env = createEnv(options as any, { env: process.env }); // Get Bun's prefix for client-exposed environment variables const prefix = "BUN_PUBLIC_"; diff --git a/packages/internal/keywords/package.json b/packages/internal/keywords/package.json index c45980dde..61a39717f 100644 --- a/packages/internal/keywords/package.json +++ b/packages/internal/keywords/package.json @@ -8,16 +8,9 @@ "types": "./dist/index.d.ts", "description": "ArkEnv keywords (internal usage)", "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "require": "./dist/index.cjs" - }, - "./functions": { - "types": "./dist/functions.d.ts", - "import": "./dist/functions.js", - "require": "./dist/functions.cjs" - } + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" }, "scripts": { "build": "tsdown", diff --git a/packages/internal/keywords/src/functions.ts b/packages/internal/keywords/src/functions.ts deleted file mode 100644 index 0ed0e01b5..000000000 --- a/packages/internal/keywords/src/functions.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * A loose numeric morph function. - */ -export const maybeNumberFunction = (s: unknown) => { - if (typeof s === "number") return s; - if (typeof s !== "string") return s; - const trimmed = s.trim(); - if (trimmed === "") return s; - if (trimmed === "NaN") return Number.NaN; - const n = Number(trimmed); - return Number.isNaN(n) ? s : n; -}; - -/** - * A loose boolean morph function. - */ -export const maybeBooleanFunction = (s: unknown) => { - if (s === "true") return true; - if (s === "false") return false; - return s; -}; - -/** - * A loose JSON morph function. - */ -export const maybeJsonFunction = (s: unknown) => { - if (typeof s !== "string") return s; - const trimmed = s.trim(); - if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return s; - try { - return JSON.parse(trimmed); - } catch { - return s; - } -}; diff --git a/packages/internal/keywords/src/index.ts b/packages/internal/keywords/src/index.ts index 881988da9..2b192a78d 100644 --- a/packages/internal/keywords/src/index.ts +++ b/packages/internal/keywords/src/index.ts @@ -1,9 +1,4 @@ import { type } from "arktype"; -import { - maybeBooleanFunction, - maybeJsonFunction, - maybeNumberFunction, -} from "./functions"; /** * A loose numeric morph. @@ -14,7 +9,15 @@ import { * * Useful for coercion in unions where failing on non-numeric strings would block other branches. */ -export const maybeNumber = type("unknown").pipe(maybeNumberFunction); +export const maybeNumber = type("unknown").pipe((s) => { + if (typeof s === "number") return s; + if (typeof s !== "string") return s; + const trimmed = s.trim(); + if (trimmed === "") return s; + if (trimmed === "NaN") return Number.NaN; + const n = Number(trimmed); + return Number.isNaN(n) ? s : n; +}); /** * A loose boolean morph. @@ -25,7 +28,11 @@ export const maybeNumber = type("unknown").pipe(maybeNumberFunction); * * Useful for coercion in unions where failing on non-boolean strings would block other branches. */ -export const maybeBoolean = type("unknown").pipe(maybeBooleanFunction); +export const maybeBoolean = type("unknown").pipe((s) => { + if (s === "true") return true; + if (s === "false") return false; + return s; +}); /** * A `number` integer between 0 and 65535. @@ -46,4 +53,14 @@ export const host = type("string.ip | 'localhost'"); * * Useful for coercion in unions where failing on non-JSON strings would block other branches. */ -export const maybeJson = type("unknown").pipe(maybeJsonFunction); +export const maybeJson = type("unknown").pipe((s) => { + if (typeof s !== "string") return s; + const trimmed = s.trim(); + // Only attempt to parse if it looks like JSON (starts with { or [) + if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return s; + try { + return JSON.parse(trimmed); + } catch { + return s; + } +}); diff --git a/packages/internal/keywords/tsdown.config.ts b/packages/internal/keywords/tsdown.config.ts index 04ae1d9ed..8fc08e8db 100644 --- a/packages/internal/keywords/tsdown.config.ts +++ b/packages/internal/keywords/tsdown.config.ts @@ -1,7 +1,6 @@ import { defineConfig } from "tsdown"; export default defineConfig({ - entry: ["src/index.ts", "src/functions.ts"], format: ["esm", "cjs"], minify: true, fixedExtension: false, diff --git a/packages/vite-plugin/src/index.test.ts b/packages/vite-plugin/src/index.test.ts index 4321a0c73..669a8b449 100644 --- a/packages/vite-plugin/src/index.test.ts +++ b/packages/vite-plugin/src/index.test.ts @@ -30,7 +30,9 @@ import arkenvPlugin from "./index.js"; const fixturesDir = join(__dirname, "__fixtures__"); // Get the mocked functions -const { arkenv: mockArkenv } = vi.mocked(await import("arkenv")); +const { createEnv: mockCreateEnv, arkenv: mockArkenv } = vi.mocked( + await import("arkenv"), +); const mockLoadEnv = vi.mocked(vite.loadEnv); @@ -45,6 +47,7 @@ for (const name of readdirSync(fixturesDir).filter( beforeEach(() => { // Clear environment variables and mock cleanup vi.unstubAllEnvs(); + mockCreateEnv.mockClear(); mockArkenv.mockClear(); mockLoadEnv.mockClear(); }); @@ -52,6 +55,7 @@ for (const name of readdirSync(fixturesDir).filter( afterEach(() => { // Complete cleanup: restore environment and reset mocks vi.unstubAllEnvs(); + mockCreateEnv.mockReset(); mockArkenv.mockReset(); mockLoadEnv.mockReset(); }); @@ -82,7 +86,7 @@ for (const name of readdirSync(fixturesDir).filter( ).resolves.not.toThrow(); // Verify that createEnv was called with the correct parameters - expect(mockArkenv).toHaveBeenCalledWith(config.Env, { + expect(mockCreateEnv).toHaveBeenCalledWith(config.Env, { env: expect.objectContaining(config.envVars || {}), }); }); @@ -93,12 +97,14 @@ for (const name of readdirSync(fixturesDir).filter( describe("Plugin Unit Tests", () => { beforeEach(() => { vi.unstubAllEnvs(); + mockCreateEnv.mockClear(); mockArkenv.mockClear(); mockLoadEnv.mockClear(); }); afterEach(() => { vi.unstubAllEnvs(); + mockCreateEnv.mockReset(); mockArkenv.mockReset(); mockLoadEnv.mockReset(); }); @@ -116,7 +122,7 @@ describe("Plugin Unit Tests", () => { it("should call createEnv during config hook", () => { // Mock createEnv to return a valid object - mockArkenv.mockReturnValue({ VITE_TEST: "test" }); + mockCreateEnv.mockReturnValue({ VITE_TEST: "test" }); const pluginInstance = arkenvPlugin({ VITE_TEST: "string" }); @@ -141,7 +147,7 @@ describe("Plugin Unit Tests", () => { ); } - expect(mockArkenv).toHaveBeenCalledWith( + expect(mockCreateEnv).toHaveBeenCalledWith( { VITE_TEST: "string" }, { env: expect.any(Object), @@ -156,7 +162,7 @@ describe("Plugin Unit Tests", () => { VITE_NUMBER: 42, VITE_BOOLEAN: true, }; - mockArkenv.mockReturnValue(mockTransformedEnv); + mockCreateEnv.mockReturnValue(mockTransformedEnv); const pluginInstance = arkenvPlugin({ VITE_STRING: "string", @@ -206,7 +212,7 @@ describe("Plugin Unit Tests", () => { VITE_ZERO: 0, VITE_FALSE: false, }; - mockArkenv.mockReturnValue(mockTransformedEnv); + mockCreateEnv.mockReturnValue(mockTransformedEnv); const pluginInstance = arkenvPlugin({ VITE_NULL: "string", @@ -247,7 +253,7 @@ describe("Plugin Unit Tests", () => { }); it("should handle empty environment object", () => { - mockArkenv.mockReturnValue({}); + mockCreateEnv.mockReturnValue({}); const pluginInstance = arkenvPlugin({}); @@ -282,7 +288,7 @@ describe("Plugin Unit Tests", () => { VITE_UPPERCASE: "test", vite_lowercase: "test", // This doesn't start with VITE_ so it will be filtered out }; - mockArkenv.mockReturnValue(mockTransformedEnv); + mockCreateEnv.mockReturnValue(mockTransformedEnv); const pluginInstance = arkenvPlugin({ VITE_SPECIAL_CHARS: "string", @@ -324,7 +330,7 @@ describe("Plugin Unit Tests", () => { it("should propagate errors from createEnv", () => { const error = new Error("Environment validation failed"); - mockArkenv.mockImplementation(() => { + mockCreateEnv.mockImplementation(() => { throw error; }); @@ -364,7 +370,7 @@ describe("Plugin Unit Tests", () => { VITE_API_URL: "https://api.example.com", VITE_DEBUG: true, }; - mockArkenv.mockReturnValue(mockTransformedEnv); + mockCreateEnv.mockReturnValue(mockTransformedEnv); const pluginInstance = arkenvPlugin({ PORT: "number.port", @@ -412,7 +418,7 @@ describe("Plugin Unit Tests", () => { VITE_OLD_VAR: "should not be exposed", SECRET_KEY: "should not be exposed", }; - mockArkenv.mockReturnValue(mockTransformedEnv); + mockCreateEnv.mockReturnValue(mockTransformedEnv); const pluginInstance = arkenvPlugin({ PUBLIC_API_URL: "string", @@ -458,7 +464,7 @@ describe("Plugin Unit Tests", () => { VITE_API_URL: "https://api.example.com", PUBLIC_DEBUG: true, }; - mockArkenv.mockReturnValue(mockTransformedEnv); + mockCreateEnv.mockReturnValue(mockTransformedEnv); const pluginInstance = arkenvPlugin({ VITE_API_URL: "string", @@ -503,7 +509,7 @@ describe("Plugin Unit Tests", () => { CUSTOM_PREFIX_VAR: "test", SECRET_KEY: "should not be exposed", }; - mockArkenv.mockReturnValue(mockTransformedEnv); + mockCreateEnv.mockReturnValue(mockTransformedEnv); const pluginInstance = arkenvPlugin({ VITE_API_URL: "string", @@ -545,7 +551,7 @@ describe("Plugin Unit Tests", () => { }); it("should use custom envDir when provided in config", async () => { - mockArkenv.mockReturnValue({ VITE_TEST: "test" }); + mockCreateEnv.mockReturnValue({ VITE_TEST: "test" }); const pluginInstance = arkenvPlugin({ VITE_TEST: "string" }); @@ -574,7 +580,7 @@ describe("Plugin Unit Tests", () => { expect(mockLoadEnv).toHaveBeenCalledWith("test", "/custom/env/dir", ""); // Verify createEnv was called - the envDir is used by loadEnv internally - expect(mockArkenv).toHaveBeenCalledWith( + expect(mockCreateEnv).toHaveBeenCalledWith( { VITE_TEST: "string" }, { env: expect.any(Object), @@ -583,7 +589,7 @@ describe("Plugin Unit Tests", () => { }); it("should default to process.cwd() when envDir is not configured", () => { - mockArkenv.mockReturnValue({ VITE_TEST: "test" }); + mockCreateEnv.mockReturnValue({ VITE_TEST: "test" }); const pluginInstance = arkenvPlugin({ VITE_TEST: "string" }); @@ -612,7 +618,7 @@ describe("Plugin Unit Tests", () => { expect(mockLoadEnv).toHaveBeenCalledWith("test", process.cwd(), ""); // Verify createEnv was called successfully with default behavior - expect(mockArkenv).toHaveBeenCalledWith( + expect(mockCreateEnv).toHaveBeenCalledWith( { VITE_TEST: "string" }, { env: expect.any(Object), @@ -628,7 +634,7 @@ describe("Plugin Unit Tests", () => { // Note: We use the real implementation for this test const actual = await vi.importActual("arkenv"); - mockArkenv.mockImplementation(actual.arkenv); + mockCreateEnv.mockImplementation(actual.createEnv); const pluginInstance = arkenvPlugin(schema); @@ -687,12 +693,14 @@ describe("Custom envDir Configuration (with-env-dir fixture)", () => { beforeEach(() => { vi.unstubAllEnvs(); + mockCreateEnv.mockClear(); mockArkenv.mockClear(); mockLoadEnv.mockClear(); }); afterEach(() => { vi.unstubAllEnvs(); + mockCreateEnv.mockClear(); mockArkenv.mockClear(); mockLoadEnv.mockClear(); }); @@ -704,7 +712,7 @@ describe("Custom envDir Configuration (with-env-dir fixture)", () => { vite.build(createBuildConfig(customEnvDir, config.Env)), ).resolves.not.toThrow(); - expect(mockArkenv).toHaveBeenCalledWith(config.Env, { + expect(mockCreateEnv).toHaveBeenCalledWith(config.Env, { env: expect.objectContaining(expectedEnvVars), }); }); @@ -717,7 +725,7 @@ describe("Custom envDir Configuration (with-env-dir fixture)", () => { vite.build(createBuildConfig(nonExistentEnvDir, config.Env)), ).rejects.toThrow(); - expect(mockArkenv).toHaveBeenCalledWith(config.Env, { + expect(mockCreateEnv).toHaveBeenCalledWith(config.Env, { env: expect.any(Object), }); }); @@ -738,7 +746,7 @@ describe("Custom envDir Configuration (with-env-dir fixture)", () => { vite.build(createBuildConfig(customEnvDir, config.Env)), ).resolves.not.toThrow(); - expect(mockArkenv).toHaveBeenCalledWith(config.Env, { + expect(mockCreateEnv).toHaveBeenCalledWith(config.Env, { env: expect.objectContaining(expectedEnvVars), }); }); @@ -753,7 +761,7 @@ describe("Custom envDir Configuration (with-env-dir fixture)", () => { await vite.build(createBuildConfig(customEnvDir, config.Env)); // Verify that all env vars (including non-schema ones) are passed - expect(mockArkenv).toHaveBeenCalledWith(config.Env, { + expect(mockCreateEnv).toHaveBeenCalledWith(config.Env, { env: expect.objectContaining(envWithExtra), }); }); @@ -761,7 +769,7 @@ describe("Custom envDir Configuration (with-env-dir fixture)", () => { async function readTestConfig(fixtureDir: string) { // Import the env schema from the TypeScript config file - let Env: Record = {}; + let Env: Record = {}; try { const configPath = join(fixtureDir, "config.ts"); const configModule = await import(configPath); diff --git a/packages/vite-plugin/src/index.ts b/packages/vite-plugin/src/index.ts index f50337d1a..b7b52b4b9 100644 --- a/packages/vite-plugin/src/index.ts +++ b/packages/vite-plugin/src/index.ts @@ -1,5 +1,5 @@ import type { EnvSchemaWithType, SchemaShape } from "@repo/types"; -import { type EnvSchema, arkenv as validateEnv } from "arkenv"; +import { type EnvSchema, createEnv } from "arkenv"; import { loadEnv, type Plugin } from "vite"; export type { ImportMetaEnvAugmented } from "./types"; @@ -62,7 +62,7 @@ export default function arkenv( // NOTE: We use 'options as any' here strictly to bypass TypeScript's "Type instantiation is excessively deep" errors. // This assertion ONLY affects compile-time checking; runtime validation (including Standard Schema via def["~standard"]) // remains fully intact and functional. - const env = validateEnv(options as any, { + const env = createEnv(options as any, { env: loadEnv(mode, envDir, ""), }); From 4659bfdcb007bf0f00cb5730529db562906afa60 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 16:00:08 +0000 Subject: [PATCH 039/117] [autofix.ci] apply automated fixes --- packages/arkenv/src/coercion.integration.test.ts | 4 +++- packages/arkenv/src/create-env.test.ts | 13 ++++++++++--- packages/arkenv/src/utils/coerce.ts | 2 +- packages/vite-plugin/src/index.ts | 2 +- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/arkenv/src/coercion.integration.test.ts b/packages/arkenv/src/coercion.integration.test.ts index b2918b202..1181a52ea 100644 --- a/packages/arkenv/src/coercion.integration.test.ts +++ b/packages/arkenv/src/coercion.integration.test.ts @@ -112,7 +112,9 @@ describe("coercion integration", () => { it("should NOT coerce empty or whitespace strings to 0 for numbers", () => { expect(() => createEnv({ VAL: "number" }, { env: { VAL: "" } })).toThrow(); - expect(() => createEnv({ VAL: "number" }, { env: { VAL: " " } })).toThrow(); + expect(() => + createEnv({ VAL: "number" }, { env: { VAL: " " } }), + ).toThrow(); }); it("should fail validation if coercion fails (not a boolean)", () => { diff --git a/packages/arkenv/src/create-env.test.ts b/packages/arkenv/src/create-env.test.ts index f0def3abc..9fb6a3920 100644 --- a/packages/arkenv/src/create-env.test.ts +++ b/packages/arkenv/src/create-env.test.ts @@ -167,11 +167,15 @@ describe("createEnv", () => { }); it("should throw for empty string when number is expected", () => { - expect(() => createEnv({ VAL: "number" }, { env: { VAL: "" } })).toThrow(); + expect(() => + createEnv({ VAL: "number" }, { env: { VAL: "" } }), + ).toThrow(); }); it("should throw for empty string when boolean is expected", () => { - expect(() => createEnv({ VAL: "boolean" }, { env: { VAL: "" } })).toThrow(); + expect(() => + createEnv({ VAL: "boolean" }, { env: { VAL: "" } }), + ).toThrow(); }); it("should allow empty strings when the schema is unknown", () => { @@ -539,7 +543,10 @@ describe("createEnv", () => { }); it("should handle single-element array", () => { - const env = createEnv({ TAGS: "string[]" }, { env: { TAGS: "only-one" } }); + const env = createEnv( + { TAGS: "string[]" }, + { env: { TAGS: "only-one" } }, + ); expect(env.TAGS).toEqual(["only-one"]); }); diff --git a/packages/arkenv/src/utils/coerce.ts b/packages/arkenv/src/utils/coerce.ts index c63f4c411..46f58c32d 100644 --- a/packages/arkenv/src/utils/coerce.ts +++ b/packages/arkenv/src/utils/coerce.ts @@ -206,7 +206,7 @@ const applyCoercion = ( } const record = current as Record; - if (Object.prototype.hasOwnProperty.call(record, lastKey)) { + if (Object.hasOwn(record, lastKey)) { const original = record[lastKey]; if (type === "array" && typeof original === "string") { diff --git a/packages/vite-plugin/src/index.ts b/packages/vite-plugin/src/index.ts index b7b52b4b9..215b5fd65 100644 --- a/packages/vite-plugin/src/index.ts +++ b/packages/vite-plugin/src/index.ts @@ -1,5 +1,5 @@ import type { EnvSchemaWithType, SchemaShape } from "@repo/types"; -import { type EnvSchema, createEnv } from "arkenv"; +import { createEnv, type EnvSchema } from "arkenv"; import { loadEnv, type Plugin } from "vite"; export type { ImportMetaEnvAugmented } from "./types"; From db8a456b57be193d9407c5409f4a745000ae41e9 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 21:16:47 +0500 Subject: [PATCH 040/117] chore: extract cleanup andNaming refactors to separate branch --- packages/arkenv/src/create-env.ts | 4 +- packages/arkenv/src/errors.ts | 47 ++++------------- packages/arkenv/src/utils/coerce.ts | 13 ++--- packages/bun-plugin/src/index.test.ts | 12 ++--- packages/bun-plugin/src/index.ts | 40 +++++++-------- packages/bun-plugin/src/types.ts | 12 ++--- packages/vite-plugin/src/index.test.ts | 70 +++++++++++++------------- 7 files changed, 85 insertions(+), 113 deletions(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index cfbbd4c6d..eb17a344e 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -71,7 +71,7 @@ function validateArkType( // Apply coercion transformation (Lazy Loaded) if (config.coerce !== false) { - schema = coerce(type, schema, { + schema = coerce(schema, { ...(config.arrayFormat ? { arrayFormat: config.arrayFormat } : {}), }); } @@ -87,7 +87,6 @@ function validateArkType( typeof error === "object" && error !== null && "message" in error ? String((error as any).message) : "Validation failed", - validator: "arktype" as const, })), }; } @@ -138,7 +137,6 @@ function validateStandard( return String(segment); }) ?? [], message: issue.message, - validator: "standard" as const, })), }; } diff --git a/packages/arkenv/src/errors.ts b/packages/arkenv/src/errors.ts index fe1d96da9..f6cac5231 100644 --- a/packages/arkenv/src/errors.ts +++ b/packages/arkenv/src/errors.ts @@ -1,21 +1,14 @@ import { indent } from "./utils/indent"; import { styleText } from "./utils/style-text"; -/** - * Normalized issue format for ArkEnv. - * This is the STABLE INTERNAL INVARIANT for error reporting. - * All validators must eventually project their errors into this shape. - * @internal - */ export type EnvIssue = { path: string[]; message: string; - validator: "arktype" | "standard"; }; export class ArkEnvError extends Error { constructor( - errors: EnvIssue[] | any, + errors: EnvIssue[], message = "Errors found while validating environment variables", ) { super(`${styleText("red", message)}\n${indent(formatErrors(errors))}\n`); @@ -25,35 +18,15 @@ export class ArkEnvError extends Error { Object.defineProperty(ArkEnvError, "name", { value: "ArkEnvError" }); -/** - * Format the errors for display. - * While this function accepts `any` for backward compatibility and lazy-loading - * flexibility, it primarily operates on the `EnvIssue` stable shape. - * - * @param errors - The errors found during validation (expected: EnvIssue[]) - * @returns A string of the formatted errors - */ -export function formatErrors(errors: any): string { - if (Array.isArray(errors)) { - return (errors as EnvIssue[]) - .map((err) => { - const path = err.path?.length > 0 ? err.path.join(".") : "root"; - let message = err.message; - if (message.startsWith(`${path} `)) { - message = message.slice(path.length + 1); - } - return `${styleText("yellow", path)} ${message}`; - }) - .join("\n"); - } - - return Object.entries((errors as any).byPath || {}) - .map(([path, error]: [string, any]) => { - const messageWithoutPath = error.message.startsWith(path) - ? error.message.slice(path.length) - : error.message; - - return `${styleText("yellow", path)} ${messageWithoutPath.trimStart()}`; +export function formatErrors(errors: EnvIssue[]): string { + return errors + .map((err) => { + const path = err.path?.length > 0 ? err.path.join(".") : "root"; + let message = err.message; + if (message.startsWith(`${path} `)) { + message = message.slice(path.length + 1); + } + return `${styleText("yellow", path)} ${message}`; }) .join("\n"); } diff --git a/packages/arkenv/src/utils/coerce.ts b/packages/arkenv/src/utils/coerce.ts index 46f58c32d..73d3d17f0 100644 --- a/packages/arkenv/src/utils/coerce.ts +++ b/packages/arkenv/src/utils/coerce.ts @@ -1,5 +1,8 @@ +import { createRequire } from "node:module"; import { maybeBoolean, maybeJson, maybeNumber } from "@repo/keywords"; +const require = createRequire(import.meta.url); + /** * A marker used in the coercion path to indicate that the target * is the *elements* of an array, rather than the array property itself. @@ -206,7 +209,7 @@ const applyCoercion = ( } const record = current as Record; - if (Object.hasOwn(record, lastKey)) { + if (Object.prototype.hasOwnProperty.call(record, lastKey)) { const original = record[lastKey]; if (type === "array" && typeof original === "string") { @@ -270,11 +273,9 @@ const applyCoercion = ( /** * Create a coercing wrapper around an ArkType schema using JSON Schema introspection. */ -export function coerce( - type: any, // Injected to avoid top-level import - schema: any, // Injected to avoid top-level import - options?: CoerceOptions, -): any { +export function coerce(schema: any, options?: CoerceOptions): any { + const { type } = require("arktype"); + const json = schema.in.toJsonSchema({ fallback: (ctx: any) => ctx.base, }); diff --git a/packages/bun-plugin/src/index.test.ts b/packages/bun-plugin/src/index.test.ts index e4b9ee329..4053c323c 100644 --- a/packages/bun-plugin/src/index.test.ts +++ b/packages/bun-plugin/src/index.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import arkenvPlugin, { processEnvSchema } from "./index.js"; +import createEnvPlugin, { processEnvSchema } from "./index.js"; describe("Bun Plugin", () => { let originalEnv: NodeJS.ProcessEnv; @@ -13,16 +13,16 @@ describe("Bun Plugin", () => { }); it("should create a plugin function", () => { - expect(typeof arkenvPlugin).toBe("function"); + expect(typeof createEnvPlugin).toBe("function"); }); it("should return a Bun plugin object", () => { // Set up a valid environment variable process.env.BUN_PUBLIC_TEST = "test-value"; - const pluginInstance = arkenvPlugin({ BUN_PUBLIC_TEST: "string" }); + const pluginInstance = createEnvPlugin({ BUN_PUBLIC_TEST: "string" }); - expect(pluginInstance).toHaveProperty("name", "@arkenv/bun-plugin"); + expect(pluginInstance).toHaveProperty("name", "@createEnv/bun-plugin"); expect(pluginInstance).toHaveProperty("setup"); expect(typeof pluginInstance.setup).toBe("function"); }); @@ -32,7 +32,7 @@ describe("Bun Plugin", () => { process.env.BUN_PUBLIC_TEST = "test-value"; expect(() => { - arkenvPlugin({ BUN_PUBLIC_TEST: "string" }); + createEnvPlugin({ BUN_PUBLIC_TEST: "string" }); }).not.toThrow(); }); @@ -41,7 +41,7 @@ describe("Bun Plugin", () => { delete process.env.BUN_PUBLIC_REQUIRED; expect(() => { - arkenvPlugin({ BUN_PUBLIC_REQUIRED: "string" }); + createEnvPlugin({ BUN_PUBLIC_REQUIRED: "string" }); }).toThrow(); }); diff --git a/packages/bun-plugin/src/index.ts b/packages/bun-plugin/src/index.ts index b2edd5cfd..6c2940a36 100644 --- a/packages/bun-plugin/src/index.ts +++ b/packages/bun-plugin/src/index.ts @@ -1,7 +1,7 @@ import { join } from "node:path"; import type { EnvSchemaWithType, SchemaShape } from "@repo/types"; -import type { EnvSchema } from "arkenv"; -import { createEnv } from "arkenv"; +import type { EnvSchema } from "createEnv"; +import { createEnv } from "createEnv"; import type { BunPlugin, Loader, PluginBuilder } from "bun"; export type { ProcessEnvAugmented } from "./types"; @@ -104,37 +104,37 @@ function registerLoader(build: PluginBuilder, envMap: Map) { * In `bunfig.toml`: * ```toml * [serve.static] - * plugins = ["@arkenv/bun-plugin"] + * plugins = ["@createEnv/bun-plugin"] * ``` * and in `Bun.build`: * ```ts - * import arkenv from "@arkenv/bun-plugin"; + * import createEnv from "@createEnv/bun-plugin"; * Bun.build({ - * plugins: [arkenv] + * plugins: [createEnv] * }) * ``` * * 2. **Manual Configuration**: * Call it as a function in `Bun.build` with your schema. * ```ts - * import arkenv from "@arkenv/bun-plugin"; + * import createEnv from "@createEnv/bun-plugin"; * import { Env } from "./src/env"; * Bun.build({ - * plugins: [arkenv(Env)] + * plugins: [createEnv(Env)] * }) * ``` */ -export function arkenv(options: EnvSchemaWithType): BunPlugin; -export function arkenv( +export function createEnv(options: EnvSchemaWithType): BunPlugin; +export function createEnv( options: EnvSchema, ): BunPlugin; -export function arkenv( +export function createEnv( options: EnvSchema | EnvSchemaWithType, ): BunPlugin { const envMap = processEnvSchema(options); return { - name: "@arkenv/bun-plugin", + name: "@createEnv/bun-plugin", setup(build) { registerLoader(build, envMap); }, @@ -142,7 +142,7 @@ export function arkenv( } // Attach static analysis properties to the function to make it a valid BunPlugin object -// This allows it to be used in bunfig.toml as `plugins = ["@arkenv/bun-plugin"]` +// This allows it to be used in bunfig.toml as `plugins = ["@createEnv/bun-plugin"]` /** * Bun plugin to validate environment variables using ArkEnv and expose prefixed variables to client code. * @@ -154,30 +154,30 @@ export function arkenv( * In `bunfig.toml`: * ```toml * [serve.static] - * plugins = ["@arkenv/bun-plugin"] + * plugins = ["@createEnv/bun-plugin"] * ``` * and in `Bun.build`: * ```ts - * import arkenv from "@arkenv/bun-plugin"; + * import createEnv from "@createEnv/bun-plugin"; * Bun.build({ - * plugins: [arkenv] + * plugins: [createEnv] * }) * ``` * * 2. **Manual Configuration**: * Call it as a function in `Bun.build` with your schema. * ```ts - * import arkenv from "@arkenv/bun-plugin"; + * import createEnv from "@createEnv/bun-plugin"; * import { Env } from "./src/env"; * Bun.build({ - * plugins: [arkenv(Env)] + * plugins: [createEnv(Env)] * }) * ``` */ -const hybrid = arkenv as typeof arkenv & BunPlugin; +const hybrid = createEnv as typeof createEnv & BunPlugin; Object.defineProperty(hybrid, "name", { - value: "@arkenv/bun-plugin", + value: "@createEnv/bun-plugin", writable: false, }); @@ -226,7 +226,7 @@ export default { \`\`\` `; throw new Error( - `@arkenv/bun-plugin: No environment schema found.\n\nChecked paths:\n${pathsList}\n\nPlease create a schema file at one of these locations exporting your environment definition.\n${example}`, + `@createEnv/bun-plugin: No environment schema found.\n\nChecked paths:\n${pathsList}\n\nPlease create a schema file at one of these locations exporting your environment definition.\n${example}`, ); } diff --git a/packages/bun-plugin/src/types.ts b/packages/bun-plugin/src/types.ts index bfae18e56..db4f0176a 100644 --- a/packages/bun-plugin/src/types.ts +++ b/packages/bun-plugin/src/types.ts @@ -5,18 +5,18 @@ import type { type } from "arktype"; * Augment the `process.env` object with typesafe environment variables * based on the schema validator. * - * This type extracts the inferred type from the schema (result of `type()` from arkenv), + * This type extracts the inferred type from the schema (result of `type()` from createEnv), * filters it to only include variables matching the Bun prefix (defaults to "BUN_PUBLIC_"), * and makes them available on `process.env`. * - * @template TSchema - The environment variable schema (result of `type()` from arkenv) + * @template TSchema - The environment variable schema (result of `type()` from createEnv) * @template Prefix - The prefix to filter by (defaults to "BUN_PUBLIC_") * * @example * ```ts * // bun.config.ts or similar - * import arkenv from '@arkenv/bun-plugin'; - * import { type } from 'arkenv'; + * import createEnv from '@createEnv/bun-plugin'; + * import { type } from 'createEnv'; * * export const Env = type({ * BUN_PUBLIC_API_URL: 'string', @@ -25,7 +25,7 @@ import type { type } from "arktype"; * }); * * export default { - * plugins: [arkenv(Env)], + * plugins: [createEnv(Env)], * }; * ``` * @@ -34,7 +34,7 @@ import type { type } from "arktype"; * // src/env.d.ts * /// * - * import type { ProcessEnvAugmented } from '@arkenv/bun-plugin'; + * import type { ProcessEnvAugmented } from '@createEnv/bun-plugin'; * import type { Env } from './env'; // or from bun.config.ts * * declare global { diff --git a/packages/vite-plugin/src/index.test.ts b/packages/vite-plugin/src/index.test.ts index 669a8b449..472581b36 100644 --- a/packages/vite-plugin/src/index.test.ts +++ b/packages/vite-plugin/src/index.test.ts @@ -4,15 +4,15 @@ import * as vite from "vite"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { z } from "zod"; -// Mock the arkenv module to capture calls -// Mock the arkenv module with a spy that calls the real implementation by default -vi.mock("arkenv", async (importActual) => { - const actual = await importActual(); +// Mock the createEnv module to capture calls +// Mock the createEnv module with a spy that calls the real implementation by default +vi.mock("createEnv", async (importActual) => { + const actual = await importActual(); return { ...actual, default: vi.fn(actual.default), createEnv: vi.fn(actual.createEnv), - arkenv: vi.fn(actual.arkenv), + createEnv: vi.fn(actual.createEnv), }; }); @@ -25,13 +25,13 @@ vi.mock("vite", async (importActual) => { }; }); -import arkenvPlugin from "./index.js"; +import createEnvPlugin from "./index.js"; const fixturesDir = join(__dirname, "__fixtures__"); // Get the mocked functions -const { createEnv: mockCreateEnv, arkenv: mockArkenv } = vi.mocked( - await import("arkenv"), +const { createEnv: mockCreateEnv, createEnv: mockCreateEnv } = vi.mocked( + await import("createEnv"), ); const mockLoadEnv = vi.mocked(vite.loadEnv); @@ -48,7 +48,7 @@ for (const name of readdirSync(fixturesDir).filter( // Clear environment variables and mock cleanup vi.unstubAllEnvs(); mockCreateEnv.mockClear(); - mockArkenv.mockClear(); + mockCreateEnv.mockClear(); mockLoadEnv.mockClear(); }); @@ -56,7 +56,7 @@ for (const name of readdirSync(fixturesDir).filter( // Complete cleanup: restore environment and reset mocks vi.unstubAllEnvs(); mockCreateEnv.mockReset(); - mockArkenv.mockReset(); + mockCreateEnv.mockReset(); mockLoadEnv.mockReset(); }); @@ -71,7 +71,7 @@ for (const name of readdirSync(fixturesDir).filter( mode: "test", configFile: false, root: config.root, - plugins: [arkenvPlugin(config.Env)], + plugins: [createEnvPlugin(config.Env)], logLevel: "error", build: { lib: { @@ -79,7 +79,7 @@ for (const name of readdirSync(fixturesDir).filter( formats: ["es"], }, rollupOptions: { - external: ["arkenv"], + external: ["createEnv"], }, }, }), @@ -98,25 +98,25 @@ describe("Plugin Unit Tests", () => { beforeEach(() => { vi.unstubAllEnvs(); mockCreateEnv.mockClear(); - mockArkenv.mockClear(); + mockCreateEnv.mockClear(); mockLoadEnv.mockClear(); }); afterEach(() => { vi.unstubAllEnvs(); mockCreateEnv.mockReset(); - mockArkenv.mockReset(); + mockCreateEnv.mockReset(); mockLoadEnv.mockReset(); }); it("should create a plugin function", () => { - expect(typeof arkenvPlugin).toBe("function"); + expect(typeof createEnvPlugin).toBe("function"); }); it("should return a Vite plugin object", () => { - const pluginInstance = arkenvPlugin({ VITE_TEST: "string" }); + const pluginInstance = createEnvPlugin({ VITE_TEST: "string" }); - expect(pluginInstance).toHaveProperty("name", "@arkenv/vite-plugin"); + expect(pluginInstance).toHaveProperty("name", "@createEnv/vite-plugin"); expect(pluginInstance).toHaveProperty("config"); }); @@ -124,7 +124,7 @@ describe("Plugin Unit Tests", () => { // Mock createEnv to return a valid object mockCreateEnv.mockReturnValue({ VITE_TEST: "test" }); - const pluginInstance = arkenvPlugin({ VITE_TEST: "string" }); + const pluginInstance = createEnvPlugin({ VITE_TEST: "string" }); // Mock the config hook with proper context if (pluginInstance.config && typeof pluginInstance.config === "function") { @@ -164,7 +164,7 @@ describe("Plugin Unit Tests", () => { }; mockCreateEnv.mockReturnValue(mockTransformedEnv); - const pluginInstance = arkenvPlugin({ + const pluginInstance = createEnvPlugin({ VITE_STRING: "string", VITE_NUMBER: "number", VITE_BOOLEAN: "boolean", @@ -214,7 +214,7 @@ describe("Plugin Unit Tests", () => { }; mockCreateEnv.mockReturnValue(mockTransformedEnv); - const pluginInstance = arkenvPlugin({ + const pluginInstance = createEnvPlugin({ VITE_NULL: "string", VITE_UNDEFINED: "string", VITE_EMPTY_STRING: "string", @@ -255,7 +255,7 @@ describe("Plugin Unit Tests", () => { it("should handle empty environment object", () => { mockCreateEnv.mockReturnValue({}); - const pluginInstance = arkenvPlugin({}); + const pluginInstance = createEnvPlugin({}); let result: any = {}; if (pluginInstance.config && typeof pluginInstance.config === "function") { @@ -290,7 +290,7 @@ describe("Plugin Unit Tests", () => { }; mockCreateEnv.mockReturnValue(mockTransformedEnv); - const pluginInstance = arkenvPlugin({ + const pluginInstance = createEnvPlugin({ VITE_SPECIAL_CHARS: "string", VITE_123_NUMERIC: "string", VITE_UPPERCASE: "string", @@ -334,7 +334,7 @@ describe("Plugin Unit Tests", () => { throw error; }); - const pluginInstance = arkenvPlugin({ VITE_TEST: "string" }); + const pluginInstance = createEnvPlugin({ VITE_TEST: "string" }); expect(() => { if ( @@ -372,7 +372,7 @@ describe("Plugin Unit Tests", () => { }; mockCreateEnv.mockReturnValue(mockTransformedEnv); - const pluginInstance = arkenvPlugin({ + const pluginInstance = createEnvPlugin({ PORT: "number.port", DATABASE_URL: "string", VITE_API_URL: "string", @@ -420,7 +420,7 @@ describe("Plugin Unit Tests", () => { }; mockCreateEnv.mockReturnValue(mockTransformedEnv); - const pluginInstance = arkenvPlugin({ + const pluginInstance = createEnvPlugin({ PUBLIC_API_URL: "string", PUBLIC_DEBUG: "boolean", VITE_OLD_VAR: "string", @@ -466,7 +466,7 @@ describe("Plugin Unit Tests", () => { }; mockCreateEnv.mockReturnValue(mockTransformedEnv); - const pluginInstance = arkenvPlugin({ + const pluginInstance = createEnvPlugin({ VITE_API_URL: "string", PUBLIC_DEBUG: "boolean", }); @@ -511,7 +511,7 @@ describe("Plugin Unit Tests", () => { }; mockCreateEnv.mockReturnValue(mockTransformedEnv); - const pluginInstance = arkenvPlugin({ + const pluginInstance = createEnvPlugin({ VITE_API_URL: "string", PUBLIC_DEBUG: "boolean", CUSTOM_PREFIX_VAR: "string", @@ -553,7 +553,7 @@ describe("Plugin Unit Tests", () => { it("should use custom envDir when provided in config", async () => { mockCreateEnv.mockReturnValue({ VITE_TEST: "test" }); - const pluginInstance = arkenvPlugin({ VITE_TEST: "string" }); + const pluginInstance = createEnvPlugin({ VITE_TEST: "string" }); if (pluginInstance.config && typeof pluginInstance.config === "function") { const mockContext = { @@ -591,7 +591,7 @@ describe("Plugin Unit Tests", () => { it("should default to process.cwd() when envDir is not configured", () => { mockCreateEnv.mockReturnValue({ VITE_TEST: "test" }); - const pluginInstance = arkenvPlugin({ VITE_TEST: "string" }); + const pluginInstance = createEnvPlugin({ VITE_TEST: "string" }); if (pluginInstance.config && typeof pluginInstance.config === "function") { const mockContext = { @@ -633,10 +633,10 @@ describe("Plugin Unit Tests", () => { }; // Note: We use the real implementation for this test - const actual = await vi.importActual("arkenv"); + const actual = await vi.importActual("createEnv"); mockCreateEnv.mockImplementation(actual.createEnv); - const pluginInstance = arkenvPlugin(schema); + const pluginInstance = createEnvPlugin(schema); let result: any = {}; if (pluginInstance.config && typeof pluginInstance.config === "function") { @@ -683,25 +683,25 @@ describe("Custom envDir Configuration (with-env-dir fixture)", () => { configFile: false as const, root: withEnvDirFixture, envDir, - plugins: [arkenvPlugin(schema)], + plugins: [createEnvPlugin(schema)], logLevel: "error" as const, build: { lib: { entry: "index.ts", formats: ["es" as const] }, - rollupOptions: { external: ["arkenv"] }, + rollupOptions: { external: ["createEnv"] }, }, }); beforeEach(() => { vi.unstubAllEnvs(); mockCreateEnv.mockClear(); - mockArkenv.mockClear(); + mockCreateEnv.mockClear(); mockLoadEnv.mockClear(); }); afterEach(() => { vi.unstubAllEnvs(); mockCreateEnv.mockClear(); - mockArkenv.mockClear(); + mockCreateEnv.mockClear(); mockLoadEnv.mockClear(); }); From 18a04f1cb180ef68182a5cf60c528ad9dbf2b68b Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 21:19:21 +0500 Subject: [PATCH 041/117] chore: fix tests after naming revert --- packages/arkenv/src/errors.ts | 32 +++++++++++----- packages/arkenv/src/utils/coerce.test.ts | 48 ++++++++++++------------ packages/vite-plugin/src/index.test.ts | 31 ++++++--------- 3 files changed, 57 insertions(+), 54 deletions(-) diff --git a/packages/arkenv/src/errors.ts b/packages/arkenv/src/errors.ts index f6cac5231..218f15de1 100644 --- a/packages/arkenv/src/errors.ts +++ b/packages/arkenv/src/errors.ts @@ -8,7 +8,7 @@ export type EnvIssue = { export class ArkEnvError extends Error { constructor( - errors: EnvIssue[], + errors: any, message = "Errors found while validating environment variables", ) { super(`${styleText("red", message)}\n${indent(formatErrors(errors))}\n`); @@ -18,15 +18,27 @@ export class ArkEnvError extends Error { Object.defineProperty(ArkEnvError, "name", { value: "ArkEnvError" }); -export function formatErrors(errors: EnvIssue[]): string { - return errors - .map((err) => { - const path = err.path?.length > 0 ? err.path.join(".") : "root"; - let message = err.message; - if (message.startsWith(`${path} `)) { - message = message.slice(path.length + 1); - } - return `${styleText("yellow", path)} ${message}`; +export function formatErrors(errors: any): string { + if (Array.isArray(errors)) { + return errors + .map((err: any) => { + const path = err.path?.length > 0 ? err.path.join(".") : "root"; + let message = err.message; + if (message.startsWith(`${path} `)) { + message = message.slice(path.length + 1); + } + return `${styleText("yellow", path)} ${message}`; + }) + .join("\n"); + } + + return Object.entries((errors as any).byPath || {}) + .map(([path, error]: [string, any]) => { + const messageWithoutPath = error.message.startsWith(path) + ? error.message.slice(path.length) + : error.message; + + return `${styleText("yellow", path)} ${messageWithoutPath.trimStart()}`; }) .join("\n"); } diff --git a/packages/arkenv/src/utils/coerce.test.ts b/packages/arkenv/src/utils/coerce.test.ts index 565658bc2..0c989698d 100644 --- a/packages/arkenv/src/utils/coerce.test.ts +++ b/packages/arkenv/src/utils/coerce.test.ts @@ -7,7 +7,7 @@ describe("coerce", () => { const schema = type({ PORT: "number", }); - const coercedSchema = coerce(type, schema); + const coercedSchema = coerce(schema); const result = coercedSchema({ PORT: "3000" }); expect(result).toEqual({ PORT: 3000 }); }); @@ -16,7 +16,7 @@ describe("coerce", () => { const schema = type({ AGE: "number >= 18", }); - const coercedSchema = coerce(type, schema); + const coercedSchema = coerce(schema); const result = coercedSchema({ AGE: "21" }); expect(result).toEqual({ AGE: 21 }); @@ -29,7 +29,7 @@ describe("coerce", () => { const schema = type({ EVEN: "number % 2", }); - const coercedSchema = coerce(type, schema); + const coercedSchema = coerce(schema); const result = coercedSchema({ EVEN: "4" }); expect(result).toEqual({ EVEN: 4 }); @@ -42,7 +42,7 @@ describe("coerce", () => { const schema = type({ DEBUG: "boolean", }); - const coercedSchema = coerce(type, schema); + const coercedSchema = coerce(schema); expect(coercedSchema({ DEBUG: "true" })).toEqual({ DEBUG: true }); expect(coercedSchema({ DEBUG: "false" })).toEqual({ DEBUG: false }); @@ -56,14 +56,14 @@ describe("coerce", () => { const schema = type({ "PORT?": "number", }); - const coercedSchema = coerce(type, schema); + const coercedSchema = coerce(schema); expect(coercedSchema({ PORT: "3000" })).toEqual({ PORT: 3000 }); expect(coercedSchema({})).toEqual({}); }); it("should work with root-level primitives", () => { const schema = type("number >= 10"); - const coercedSchema = coerce(type, schema); + const coercedSchema = coerce(schema); expect(coercedSchema("20")).toBe(20); const failure = coercedSchema("5"); expect(failure).toBeInstanceOf(ArkErrors); @@ -72,7 +72,7 @@ describe("coerce", () => { it("should work with strict number literals", () => { const schema = type("1 | 2"); - const coercedSchema = coerce(type, schema); + const coercedSchema = coerce(schema); expect(coercedSchema("1")).toBe(1); expect(coercedSchema("2")).toBe(2); @@ -81,7 +81,7 @@ describe("coerce", () => { it("should coerce numeric values in mixed unions", () => { const schema = type("1 | 'a'"); - const coercedSchema = coerce(type, schema); + const coercedSchema = coerce(schema); expect(coercedSchema("1")).toBe(1); expect(coercedSchema("a")).toBe("a"); @@ -89,7 +89,7 @@ describe("coerce", () => { it("should coerce mixed numeric and boolean unions", () => { const schema = type("number | boolean"); - const coercedSchema = coerce(type, schema); + const coercedSchema = coerce(schema); expect(coercedSchema("123")).toBe(123); expect(coercedSchema("true")).toBe(true); @@ -104,7 +104,7 @@ describe("coerce", () => { const schema = type({ VAL: "number | boolean", }); - const coercedSchema = coerce(type, schema); + const coercedSchema = coerce(schema); expect(coercedSchema({ VAL: "123" })).toEqual({ VAL: 123 }); expect(coercedSchema({ VAL: "true" })).toEqual({ VAL: true }); @@ -119,7 +119,7 @@ describe("coerce", () => { const schema = type({ PORT: "number", }); - const coercedSchema = coerce(type, schema); + const coercedSchema = coerce(schema); const emptyResult = coercedSchema({ PORT: "" }); expect(emptyResult).toBeInstanceOf(ArkErrors); @@ -138,7 +138,7 @@ describe("coerce", () => { const schema = type({ PORT: "number", }); - const coercedSchema = coerce(type, schema); + const coercedSchema = coerce(schema); const result = coercedSchema({ PORT: "abc" }); expect(result).toBeInstanceOf(ArkErrors); @@ -149,7 +149,7 @@ describe("coerce", () => { const schema = type({ DEBUG: "boolean", }); - const coercedSchema = coerce(type, schema); + const coercedSchema = coerce(schema); const result = coercedSchema({ DEBUG: "yes" }); expect(result).toBeInstanceOf(ArkErrors); @@ -163,7 +163,7 @@ describe("coerce", () => { port: "number", }, }); - const coercedSchema = coerce(type, schema); + const coercedSchema = coerce(schema); const result = coercedSchema({ CONFIG: '{"host": "localhost", "port": "3000"}', @@ -185,7 +185,7 @@ describe("coerce", () => { }, }, }); - const coercedSchema = coerce(type, schema); + const coercedSchema = coerce(schema); const result = coercedSchema({ CONFIG: '{"database": {"host": "localhost", "port": "5432"}}', @@ -207,7 +207,7 @@ describe("coerce", () => { debug: "boolean", }, }); - const coercedSchema = coerce(type, schema); + const coercedSchema = coerce(schema); const result = coercedSchema({ SETTINGS: '{"enabled": "true", "debug": "false"}', @@ -226,7 +226,7 @@ describe("coerce", () => { host: "string", }, }); - const coercedSchema = coerce(type, schema); + const coercedSchema = coerce(schema); const result = coercedSchema({ CONFIG: "not valid json" }); expect(result).toBeInstanceOf(ArkErrors); @@ -239,7 +239,7 @@ describe("coerce", () => { host: "string", }, }); - const coercedSchema = coerce(type, schema); + const coercedSchema = coerce(schema); const result = coercedSchema({ PORT: "3000", @@ -260,7 +260,7 @@ describe("coerce", () => { SSL: "boolean", }, }); - const coercedSchema = coerce(type, schema); + const coercedSchema = coerce(schema); const result = coercedSchema({ DB: { @@ -283,7 +283,7 @@ describe("coerce", () => { Number.parseInt(str, 10), ), }); - const coercedSchema = coerce(type, schema); + const coercedSchema = coerce(schema); const result = coercedSchema({ PORT: "3000", VITE_MY_NUMBER_MANUAL: "456", @@ -298,7 +298,7 @@ describe("coerce", () => { const schema = type({ VAL: "number.NaN", }); - const coercedSchema = coerce(type, schema); + const coercedSchema = coerce(schema); const result = coercedSchema({ VAL: "NaN" }); expect(result).not.toBeInstanceOf(ArkErrors); if (result instanceof ArkErrors) return; @@ -310,7 +310,7 @@ describe("coerce", () => { VAL: "number", }); // Coercion happens ("NaN" -> NaN), but "number" checks and rejects NaN - const coercedSchema = coerce(type, schema); + const coercedSchema = coerce(schema); const result = coercedSchema({ VAL: "NaN" }); expect(result).toBeInstanceOf(ArkErrors); expect(result.toString()).toContain("VAL must be a number (was NaN)"); @@ -320,7 +320,7 @@ describe("coerce", () => { // Coerces array elements ["1", "2", "3"] to [1, 2, 3] IDS: "number[]", }); - const coercedSchema = coerce(type, schema); + const coercedSchema = coerce(schema); const result = coercedSchema({ IDS: ["1", "2", "3"] }); expect(result).toEqual({ IDS: [1, 2, 3] }); @@ -330,7 +330,7 @@ describe("coerce", () => { const schema = type({ VAL: "number", }); - const coercedSchema = coerce(type, schema); + const coercedSchema = coerce(schema); // Logic coerces ["1", "2"] -> [1, 2] in place // Then validation sees [1, 2] against "number" and fails diff --git a/packages/vite-plugin/src/index.test.ts b/packages/vite-plugin/src/index.test.ts index 472581b36..31dd03048 100644 --- a/packages/vite-plugin/src/index.test.ts +++ b/packages/vite-plugin/src/index.test.ts @@ -4,15 +4,14 @@ import * as vite from "vite"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { z } from "zod"; -// Mock the createEnv module to capture calls -// Mock the createEnv module with a spy that calls the real implementation by default -vi.mock("createEnv", async (importActual) => { - const actual = await importActual(); +// Mock the arkenv module to capture calls +// Mock the arkenv module with a spy that calls the real implementation by default +vi.mock("arkenv", async (importActual) => { + const actual = await importActual(); return { ...actual, default: vi.fn(actual.default), createEnv: vi.fn(actual.createEnv), - createEnv: vi.fn(actual.createEnv), }; }); @@ -30,9 +29,7 @@ import createEnvPlugin from "./index.js"; const fixturesDir = join(__dirname, "__fixtures__"); // Get the mocked functions -const { createEnv: mockCreateEnv, createEnv: mockCreateEnv } = vi.mocked( - await import("createEnv"), -); +const { createEnv: mockCreateEnv } = vi.mocked(await import("arkenv")); const mockLoadEnv = vi.mocked(vite.loadEnv); @@ -48,7 +45,6 @@ for (const name of readdirSync(fixturesDir).filter( // Clear environment variables and mock cleanup vi.unstubAllEnvs(); mockCreateEnv.mockClear(); - mockCreateEnv.mockClear(); mockLoadEnv.mockClear(); }); @@ -56,7 +52,6 @@ for (const name of readdirSync(fixturesDir).filter( // Complete cleanup: restore environment and reset mocks vi.unstubAllEnvs(); mockCreateEnv.mockReset(); - mockCreateEnv.mockReset(); mockLoadEnv.mockReset(); }); @@ -79,7 +74,7 @@ for (const name of readdirSync(fixturesDir).filter( formats: ["es"], }, rollupOptions: { - external: ["createEnv"], + external: ["arkenv"], }, }, }), @@ -98,14 +93,12 @@ describe("Plugin Unit Tests", () => { beforeEach(() => { vi.unstubAllEnvs(); mockCreateEnv.mockClear(); - mockCreateEnv.mockClear(); mockLoadEnv.mockClear(); }); afterEach(() => { vi.unstubAllEnvs(); mockCreateEnv.mockReset(); - mockCreateEnv.mockReset(); mockLoadEnv.mockReset(); }); @@ -116,7 +109,7 @@ describe("Plugin Unit Tests", () => { it("should return a Vite plugin object", () => { const pluginInstance = createEnvPlugin({ VITE_TEST: "string" }); - expect(pluginInstance).toHaveProperty("name", "@createEnv/vite-plugin"); + expect(pluginInstance).toHaveProperty("name", "@arkenv/vite-plugin"); expect(pluginInstance).toHaveProperty("config"); }); @@ -633,7 +626,7 @@ describe("Plugin Unit Tests", () => { }; // Note: We use the real implementation for this test - const actual = await vi.importActual("createEnv"); + const actual = await vi.importActual("arkenv"); mockCreateEnv.mockImplementation(actual.createEnv); const pluginInstance = createEnvPlugin(schema); @@ -687,22 +680,20 @@ describe("Custom envDir Configuration (with-env-dir fixture)", () => { logLevel: "error" as const, build: { lib: { entry: "index.ts", formats: ["es" as const] }, - rollupOptions: { external: ["createEnv"] }, + rollupOptions: { external: ["arkenv"] }, }, }); beforeEach(() => { vi.unstubAllEnvs(); mockCreateEnv.mockClear(); - mockCreateEnv.mockClear(); mockLoadEnv.mockClear(); }); afterEach(() => { vi.unstubAllEnvs(); - mockCreateEnv.mockClear(); - mockCreateEnv.mockClear(); - mockLoadEnv.mockClear(); + mockCreateEnv.mockReset(); + mockLoadEnv.mockReset(); }); it("should load environment variables from custom envDir", async () => { From d054b5da74e4d756e90a9023fce0fdf3a066c1a4 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 22:21:27 +0500 Subject: [PATCH 042/117] feat: make arkenv default export and improve errors - Make `arkenv` the default export for consistency - Refactor error formatting for better readability - Enhance coercion utility with --- apps/www/content/docs/arkenv/coercion.mdx | 2 +- packages/arkenv/src/create-env.test.ts | 135 ++++++++++++---------- packages/arkenv/src/create-env.ts | 17 ++- packages/arkenv/src/errors.ts | 60 ++++++---- packages/arkenv/src/index.test.ts | 37 ++++-- packages/arkenv/src/index.ts | 7 +- packages/arkenv/src/utils/coerce.ts | 53 +++++++-- packages/vite-plugin/src/index.test.ts | 42 +++---- packages/vite-plugin/src/index.ts | 9 +- 9 files changed, 221 insertions(+), 141 deletions(-) diff --git a/apps/www/content/docs/arkenv/coercion.mdx b/apps/www/content/docs/arkenv/coercion.mdx index 3eca112dc..e62031672 100644 --- a/apps/www/content/docs/arkenv/coercion.mdx +++ b/apps/www/content/docs/arkenv/coercion.mdx @@ -94,7 +94,7 @@ Coercion works recursively: even variables inside a JSON object are coerced to t ## How it works -ArkEnv introspects your schema to identify fields that should be numbers, booleans, and other non-string types you define. When you call `createEnv()`, it pre-processes your environment variables to perform these conversions *before* validation. +ArkEnv introspects your schema to identify fields that should be numbers, booleans, and other non-string types you define. When you call `createEnv()` (or `arkenv()`), it pre-processes your environment variables to perform these conversions *before* validation. ## Performance diff --git a/packages/arkenv/src/create-env.test.ts b/packages/arkenv/src/create-env.test.ts index 9fb6a3920..ea6571a60 100644 --- a/packages/arkenv/src/create-env.test.ts +++ b/packages/arkenv/src/create-env.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { z } from "zod"; import { createEnv } from "./create-env"; import { type } from "./type"; @@ -75,30 +75,25 @@ describe("createEnv", () => { expect(createEnv(schema, { env: {} }).PORT).toBeUndefined(); }); - it("should handle validation errors from mapping", () => { - expect(() => { - createEnv( - { INVALID_PORT: "number" }, - { env: { INVALID_PORT: "not-a-number" } }, - ); - }).toThrow(); + it("should coerce strict number literals", () => { + const schema = { VAL: "1 | 2" } as const; + expect(createEnv(schema, { env: { VAL: "1" } }).VAL).toBe(1); }); - it("should work with schemas containing morphs in mapping", () => { - const env = createEnv( - { - PORT: "number.port", - VITE_MY_NUMBER_MANUAL: type("string").pipe((str) => - Number.parseInt(str, 10), - ), - }, - { - env: { - PORT: "3000", - VITE_MY_NUMBER_MANUAL: "456", - }, + it("should work with schemas containing morphs", () => { + const Env = type({ + PORT: "number.port", + VITE_MY_NUMBER_MANUAL: type("string").pipe((str) => + Number.parseInt(str, 10), + ), + }); + + const env = createEnv(Env, { + env: { + PORT: "3000", + VITE_MY_NUMBER_MANUAL: "456", }, - ); + }); expect(env.PORT).toBe(3000); expect(env.VITE_MY_NUMBER_MANUAL).toBe(456); @@ -319,31 +314,31 @@ describe("createEnv", () => { }); describe("type definitions", () => { - it("should infer types from a mapping", () => { - const env = createEnv( - { - TEST_STRING: "string", - TEST_PORT: "number", - }, - { - env: { TEST_STRING: "test", TEST_PORT: "3000" }, - }, - ); + it("should accept type definitions created with type()", () => { + process.env.TEST_STRING = "hello"; + process.env.TEST_PORT = "3000"; + + const Env = type({ + TEST_STRING: "string", + TEST_PORT: "number.port", + }); + + const env = createEnv(Env); - expect(env.TEST_STRING).toBe("test"); + expect(env.TEST_STRING).toBe("hello"); expect(env.TEST_PORT).toBe(3000); }); it("should provide correct type inference with type definitions", () => { - const env = createEnv( - { - TEST_STRING: "string", - TEST_PORT: "number.port", - }, - { - env: { TEST_STRING: "hello", TEST_PORT: "3000" }, - }, - ); + process.env.TEST_STRING = "hello"; + process.env.TEST_PORT = "3000"; + + const Env = type({ + TEST_STRING: "string", + TEST_PORT: "number.port", + }); + + const env = createEnv(Env); // TypeScript should infer these correctly const str = env.TEST_STRING; @@ -353,35 +348,51 @@ describe("createEnv", () => { expect(port).toBe(3000); }); - it("should allow extending mappings", () => { - const base = { TEST_STRING: "string" } as const; - const extended = { ...base, TEST_PORT: "number" } as const; + it("should allow reusing the same type definition multiple times", () => { + process.env.TEST_STRING = "hello"; - const env1 = createEnv(base, { env: { TEST_STRING: "test" } }); - const env2 = createEnv(extended, { - env: { TEST_STRING: "test", TEST_PORT: "3000" }, + const Env = type({ + TEST_STRING: "string", }); - expect(env1.TEST_STRING).toBe("test"); - expect(env2.TEST_PORT).toBe(3000); + // Use the same schema multiple times + const env1 = createEnv(Env, { + env: { + TEST_STRING: "first", + }, + }); + const env2 = createEnv(Env, { + env: { + TEST_STRING: "second", + }, + }); + + expect(env1.TEST_STRING).toBe("first"); + expect(env2.TEST_STRING).toBe("second"); }); - it("should throw when mapping validation fails", () => { + it("should throw when type definition validation fails", () => { process.env.INVALID_PORT = "not-a-port"; - expect(() => - createEnv({ - INVALID_PORT: "number.port", - }), - ).toThrow(/INVALID_PORT/); + const Env = type({ + INVALID_PORT: "number.port", + }); + + expect(() => createEnv(Env)).toThrow(/INVALID_PORT/); }); - it("should support custom environment and mapping", () => { - const customEnv = { HOST: "localhost", PORT: "8080" }; - const env = createEnv( - { HOST: "string", PORT: "number" }, - { env: customEnv }, - ); + it("should work with custom environment and type definitions", () => { + const Env = type({ + HOST: "string.host", + PORT: "number.port", + }); + + const customEnv = { + HOST: "localhost", + PORT: "8080", + }; + + const env = createEnv(Env, { env: customEnv }); expect(env.HOST).toBe("localhost"); expect(env.PORT).toBe(8080); diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index eb17a344e..0fa87b59d 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -33,7 +33,7 @@ export type ArkEnvConfig = { */ onUndeclaredKey?: "ignore" | "delete" | "reject"; /** - * The format to use for array parsing in ArkType schemas. + * Options for array parsing. * @default "comma" */ arrayFormat?: CoerceOptions["arrayFormat"]; @@ -148,8 +148,11 @@ function validateStandard( } /** - * Create an environment variables object from a schema and an environment. - * Now supports both ArkType and Standard Schema validators. + * Create an environment variables object from a schema and an environment + * @param def - The environment variable schema (raw object or type definition created with `type()`) + * @param config - Configuration options, see {@link ArkEnvConfig} + * @returns The validated environment variable schema + * @throws An {@link ArkEnvError | error} if the environment variables are invalid. */ export function createEnv( def: EnvSchema, @@ -163,7 +166,8 @@ export function createEnv( def: EnvSchema | EnvSchemaWithType, config: ArkEnvConfig = {}, ): distill.Out> | InferType { - const env = config.env ?? process.env; + const { env = process.env } = config; + const { isStandard, isArkCompiled } = detectValidatorType(def); // Guardrail: Block top-level Standard Schema (Zod, Valibot, etc.) @@ -186,8 +190,3 @@ export function createEnv( return result.value as any; } - -/** - * @alias createEnv - */ -export const arkenv = createEnv; diff --git a/packages/arkenv/src/errors.ts b/packages/arkenv/src/errors.ts index 218f15de1..1ca90614f 100644 --- a/packages/arkenv/src/errors.ts +++ b/packages/arkenv/src/errors.ts @@ -1,44 +1,64 @@ -import { indent } from "./utils/indent"; -import { styleText } from "./utils/style-text"; +import type { ArkErrors } from "arktype"; +import { indent, styleText } from "./utils"; +/** + * Common issue shape for environment validation + */ export type EnvIssue = { path: string[]; message: string; }; -export class ArkEnvError extends Error { - constructor( - errors: any, - message = "Errors found while validating environment variables", - ) { - super(`${styleText("red", message)}\n${indent(formatErrors(errors))}\n`); - this.name = "ArkEnvError"; - } -} - -Object.defineProperty(ArkEnvError, "name", { value: "ArkEnvError" }); - -export function formatErrors(errors: any): string { +/** + * Format the errors returned by ArkType or Standard Schema to be more readable + * @param errors - The errors returned by the validator + * @returns A string of the formatted errors + */ +export const formatErrors = (errors: ArkErrors | EnvIssue[]): string => { if (Array.isArray(errors)) { return errors - .map((err: any) => { - const path = err.path?.length > 0 ? err.path.join(".") : "root"; + .map((err) => { + const path = err.path.length > 0 ? err.path.join(".") : "root"; let message = err.message; + + // Consistent with ArkType: if message starts with path, trim it if (message.startsWith(`${path} `)) { message = message.slice(path.length + 1); } + return `${styleText("yellow", path)} ${message}`; }) .join("\n"); } - return Object.entries((errors as any).byPath || {}) - .map(([path, error]: [string, any]) => { + return Object.entries(errors.byPath) + .map(([path, error]) => { const messageWithoutPath = error.message.startsWith(path) ? error.message.slice(path.length) : error.message; - return `${styleText("yellow", path)} ${messageWithoutPath.trimStart()}`; + // Extract the value in parentheses if it exists + const valueMatch = messageWithoutPath.match(/\(was "([^"]+)"\)/); + const formattedMessage = valueMatch + ? messageWithoutPath.replace( + `(was "${valueMatch[1]}")`, + `(was ${styleText("cyan", `"${valueMatch[1]}"`)})`, + ) + : messageWithoutPath; + + return `${styleText("yellow", path)} ${formattedMessage.trimStart()}`; }) .join("\n"); +}; + +export class ArkEnvError extends Error { + constructor( + errors: ArkErrors | EnvIssue[], + message = "Errors found while validating environment variables", + ) { + super(`${styleText("red", message)}\n${indent(formatErrors(errors))}\n`); + this.name = "ArkEnvError"; + } } + +Object.defineProperty(ArkEnvError, "name", { value: "ArkEnvError" }); diff --git a/packages/arkenv/src/index.test.ts b/packages/arkenv/src/index.test.ts index e535c3714..083de3368 100644 --- a/packages/arkenv/src/index.test.ts +++ b/packages/arkenv/src/index.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, expectTypeOf, it, vi } from "vitest"; -import createEnv, { ArkEnvError } from "./index"; +import arkenv, { createEnv } from "./index"; describe("index.ts exports", () => { afterEach(() => { @@ -9,19 +9,24 @@ describe("index.ts exports", () => { }); it("should export createEnv as default export", () => { - expect(typeof createEnv).toBe("function"); + expect(arkenv).toBe(createEnv); + expect(typeof arkenv).toBe("function"); }); it("should have correct types for exported functions", () => { // Type assertion to verify exported function types + expectTypeOf(arkenv).toBeFunction(); expectTypeOf(createEnv).toBeFunction(); + + // Verify they have the same type signature + expectTypeOf(arkenv).toEqualTypeOf(createEnv); }); it("should work with default import", () => { // Set test environment variable vi.stubEnv("TEST_DEFAULT_IMPORT", "test-value"); - const env = createEnv({ + const env = arkenv({ TEST_DEFAULT_IMPORT: "string", }); @@ -29,25 +34,39 @@ describe("index.ts exports", () => { expect(typeof env.TEST_DEFAULT_IMPORT).toBe("string"); }); - it("should throw ArkEnvError with default import when validation fails", () => { + it("should work with named import", () => { + // Set test environment variable + vi.stubEnv("TEST_NAMED_IMPORT", "test-value"); + + const env = createEnv({ + TEST_NAMED_IMPORT: "string", + }); + + expect(env.TEST_NAMED_IMPORT).toBe("test-value"); + expect(typeof env.TEST_NAMED_IMPORT).toBe("string"); + }); + + it("should throw error with default import when validation fails", () => { expect(() => - createEnv({ + arkenv({ MISSING_DEFAULT_VAR: "string", }), - ).toThrow(ArkEnvError); + ).toThrow(); + }); + it("should throw error with named import when validation fails", () => { expect(() => createEnv({ - MISSING_DEFAULT_VAR: "string", + MISSING_NAMED_VAR: "string", }), - ).toThrow(/MISSING_DEFAULT_VAR.*string/); + ).toThrow(); }); it("should have same behavior for both default and named imports", () => { // Set test environment variable vi.stubEnv("COMPARISON_TEST", "same-value"); - const envFromDefault = createEnv({ + const envFromDefault = arkenv({ COMPARISON_TEST: "string", }); diff --git a/packages/arkenv/src/index.ts b/packages/arkenv/src/index.ts index d45dbaa0a..72677ad9b 100644 --- a/packages/arkenv/src/index.ts +++ b/packages/arkenv/src/index.ts @@ -3,11 +3,12 @@ import { createEnv } from "./create-env"; export type { EnvSchema } from "./create-env"; /** - * `arkenv`'s primary entry point. + * `arkenv`'s main export, an alias for {@link createEnv} * * {@link https://arkenv.js.org | ArkEnv} is a typesafe environment variables validator from editor to runtime. */ -export default createEnv; +const arkenv = createEnv; +export default arkenv; +export { type } from "./type"; export { createEnv }; export { ArkEnvError } from "./errors"; -export { type } from "./type"; diff --git a/packages/arkenv/src/utils/coerce.ts b/packages/arkenv/src/utils/coerce.ts index 73d3d17f0..45d2f2cd8 100644 --- a/packages/arkenv/src/utils/coerce.ts +++ b/packages/arkenv/src/utils/coerce.ts @@ -1,5 +1,6 @@ import { createRequire } from "node:module"; import { maybeBoolean, maybeJson, maybeNumber } from "@repo/keywords"; +import { type JsonSchema } from "arktype"; const require = createRequire(import.meta.url); @@ -31,9 +32,10 @@ export type CoerceOptions = { /** * Recursively find all paths in a JSON Schema that require coercion. + * We prioritize "number", "integer", "boolean", "array", and "object" types. */ const findCoercionPaths = ( - node: any, // Use any to avoid top-level JsonSchema export from arktype + node: JsonSchema, path: string[] = [], ): CoercionTarget[] => { const results: CoercionTarget[] = []; @@ -50,9 +52,7 @@ const findCoercionPaths = ( if ("enum" in node && node.enum) { if ( - node.enum.some( - (v: any) => typeof v === "number" || typeof v === "boolean", - ) + node.enum.some((v) => typeof v === "number" || typeof v === "boolean") ) { results.push({ path: [...path], type: "primitive" }); } @@ -64,33 +64,43 @@ const findCoercionPaths = ( } else if (node.type === "boolean") { results.push({ path: [...path], type: "primitive" }); } else if (node.type === "object") { + // Check if this object has properties defined + // If it does, we want to coerce the whole object from a JSON string + // But we also want to recursively check nested properties const hasProperties = "properties" in node && node.properties && Object.keys(node.properties).length > 0; if (hasProperties) { + // Mark this path as needing object coercion (JSON parsing) results.push({ path: [...path], type: "object" }); } + // Also recursively check nested properties for their own coercions if ("properties" in node && node.properties) { for (const [key, prop] of Object.entries(node.properties)) { - results.push(...findCoercionPaths(prop as any, [...path, key])); + results.push( + ...findCoercionPaths(prop as JsonSchema, [...path, key]), + ); } } } else if (node.type === "array") { + // Mark the array itself as a target for splitting strings results.push({ path: [...path], type: "array" }); if ("items" in node && node.items) { if (Array.isArray(node.items)) { - node.items.forEach((item: any, index: number) => { + // Tuple traversal + node.items.forEach((item, index) => { results.push( - ...findCoercionPaths(item as any, [...path, `${index}`]), + ...findCoercionPaths(item as JsonSchema, [...path, `${index}`]), ); }); } else { + // List traversal results.push( - ...findCoercionPaths(node.items as any, [ + ...findCoercionPaths(node.items as JsonSchema, [ ...path, ARRAY_ITEM_MARKER, ]), @@ -102,22 +112,23 @@ const findCoercionPaths = ( if ("anyOf" in node && node.anyOf) { for (const branch of node.anyOf) { - results.push(...findCoercionPaths(branch as any, path)); + results.push(...findCoercionPaths(branch as JsonSchema, path)); } } if ("allOf" in node && node.allOf) { for (const branch of node.allOf) { - results.push(...findCoercionPaths(branch as any, path)); + results.push(...findCoercionPaths(branch as JsonSchema, path)); } } if ("oneOf" in node && node.oneOf) { for (const branch of node.oneOf) { - results.push(...findCoercionPaths(branch as any, path)); + results.push(...findCoercionPaths(branch as JsonSchema, path)); } } + // Deduplicate by path and type combination const seen = new Set(); return results.filter((t) => { const key = JSON.stringify(t.path) + t.type; @@ -137,6 +148,7 @@ const applyCoercion = ( ) => { const { arrayFormat = "comma" } = options; + // Helper to split string to array const splitString = (val: string) => { if (arrayFormat === "json") { try { @@ -151,6 +163,7 @@ const applyCoercion = ( }; if (typeof data !== "object" || data === null) { + // If root data needs coercion if (targets.some((t) => t.path.length === 0)) { const rootTarget = targets.find((t) => t.path.length === 0); @@ -171,6 +184,7 @@ const applyCoercion = ( return data; } + // Sort targets by path length to ensure parent objects/arrays are coerced before their children const sortedTargets = [...targets].sort( (a, b) => a.path.length - b.path.length, ); @@ -186,6 +200,7 @@ const applyCoercion = ( return; } + // If we've reached the last key, apply coercion if (targetPath.length === 1) { const lastKey = targetPath[0]; @@ -209,6 +224,7 @@ const applyCoercion = ( } const record = current as Record; + // biome-ignore lint/suspicious/noPrototypeBuiltins: ES2020 compatibility if (Object.prototype.hasOwnProperty.call(record, lastKey)) { const original = record[lastKey]; @@ -237,6 +253,7 @@ const applyCoercion = ( } else { if (type === "primitive") { const asNumber = maybeNumber(original); + // If numeric parsing didn't produce a number, try boolean coercion if (typeof asNumber === "number") { record[lastKey] = asNumber; } else { @@ -248,6 +265,7 @@ const applyCoercion = ( return; } + // Recurse down const [nextKey, ...rest] = targetPath; if (nextKey === ARRAY_ITEM_MARKER) { @@ -272,10 +290,15 @@ const applyCoercion = ( /** * Create a coercing wrapper around an ArkType schema using JSON Schema introspection. + * Pre-process input data to coerce string values to numbers/booleans at identified paths + * before validation. */ export function coerce(schema: any, options?: CoerceOptions): any { const { type } = require("arktype"); - + // Use a fallback to handle unjsonifiable parts of the schema (like predicates) + // by preserving the base schema. This ensures that even if part of the schema + // cannot be fully represented in JSON Schema, we can still perform coercion + // for the parts that can. const json = schema.in.toJsonSchema({ fallback: (ctx: any) => ctx.base, }); @@ -285,6 +308,12 @@ export function coerce(schema: any, options?: CoerceOptions): any { return schema; } + /* + * We use `type("unknown")` to start the pipeline, which initializes a default scope. + * Integrating the original `schema` with its custom scope `$` into this pipeline + * creates a scope mismatch in TypeScript ({} vs $). + * We cast to `BaseType` to assert the final contract is maintained. + */ return type("unknown") .pipe((data: any) => applyCoercion(data, targets, options)) .pipe(schema); diff --git a/packages/vite-plugin/src/index.test.ts b/packages/vite-plugin/src/index.test.ts index 31dd03048..169ada7b8 100644 --- a/packages/vite-plugin/src/index.test.ts +++ b/packages/vite-plugin/src/index.test.ts @@ -24,7 +24,7 @@ vi.mock("vite", async (importActual) => { }; }); -import createEnvPlugin from "./index.js"; +import arkenvPlugin from "./index.js"; const fixturesDir = join(__dirname, "__fixtures__"); @@ -66,7 +66,7 @@ for (const name of readdirSync(fixturesDir).filter( mode: "test", configFile: false, root: config.root, - plugins: [createEnvPlugin(config.Env)], + plugins: [arkenvPlugin(config.Env)], logLevel: "error", build: { lib: { @@ -103,11 +103,11 @@ describe("Plugin Unit Tests", () => { }); it("should create a plugin function", () => { - expect(typeof createEnvPlugin).toBe("function"); + expect(typeof arkenvPlugin).toBe("function"); }); it("should return a Vite plugin object", () => { - const pluginInstance = createEnvPlugin({ VITE_TEST: "string" }); + const pluginInstance = arkenvPlugin({ VITE_TEST: "string" }); expect(pluginInstance).toHaveProperty("name", "@arkenv/vite-plugin"); expect(pluginInstance).toHaveProperty("config"); @@ -117,7 +117,7 @@ describe("Plugin Unit Tests", () => { // Mock createEnv to return a valid object mockCreateEnv.mockReturnValue({ VITE_TEST: "test" }); - const pluginInstance = createEnvPlugin({ VITE_TEST: "string" }); + const pluginInstance = arkenvPlugin({ VITE_TEST: "string" }); // Mock the config hook with proper context if (pluginInstance.config && typeof pluginInstance.config === "function") { @@ -157,7 +157,7 @@ describe("Plugin Unit Tests", () => { }; mockCreateEnv.mockReturnValue(mockTransformedEnv); - const pluginInstance = createEnvPlugin({ + const pluginInstance = arkenvPlugin({ VITE_STRING: "string", VITE_NUMBER: "number", VITE_BOOLEAN: "boolean", @@ -207,7 +207,7 @@ describe("Plugin Unit Tests", () => { }; mockCreateEnv.mockReturnValue(mockTransformedEnv); - const pluginInstance = createEnvPlugin({ + const pluginInstance = arkenvPlugin({ VITE_NULL: "string", VITE_UNDEFINED: "string", VITE_EMPTY_STRING: "string", @@ -248,7 +248,7 @@ describe("Plugin Unit Tests", () => { it("should handle empty environment object", () => { mockCreateEnv.mockReturnValue({}); - const pluginInstance = createEnvPlugin({}); + const pluginInstance = arkenvPlugin({}); let result: any = {}; if (pluginInstance.config && typeof pluginInstance.config === "function") { @@ -283,7 +283,7 @@ describe("Plugin Unit Tests", () => { }; mockCreateEnv.mockReturnValue(mockTransformedEnv); - const pluginInstance = createEnvPlugin({ + const pluginInstance = arkenvPlugin({ VITE_SPECIAL_CHARS: "string", VITE_123_NUMERIC: "string", VITE_UPPERCASE: "string", @@ -327,7 +327,7 @@ describe("Plugin Unit Tests", () => { throw error; }); - const pluginInstance = createEnvPlugin({ VITE_TEST: "string" }); + const pluginInstance = arkenvPlugin({ VITE_TEST: "string" }); expect(() => { if ( @@ -365,7 +365,7 @@ describe("Plugin Unit Tests", () => { }; mockCreateEnv.mockReturnValue(mockTransformedEnv); - const pluginInstance = createEnvPlugin({ + const pluginInstance = arkenvPlugin({ PORT: "number.port", DATABASE_URL: "string", VITE_API_URL: "string", @@ -413,7 +413,7 @@ describe("Plugin Unit Tests", () => { }; mockCreateEnv.mockReturnValue(mockTransformedEnv); - const pluginInstance = createEnvPlugin({ + const pluginInstance = arkenvPlugin({ PUBLIC_API_URL: "string", PUBLIC_DEBUG: "boolean", VITE_OLD_VAR: "string", @@ -459,7 +459,7 @@ describe("Plugin Unit Tests", () => { }; mockCreateEnv.mockReturnValue(mockTransformedEnv); - const pluginInstance = createEnvPlugin({ + const pluginInstance = arkenvPlugin({ VITE_API_URL: "string", PUBLIC_DEBUG: "boolean", }); @@ -504,7 +504,7 @@ describe("Plugin Unit Tests", () => { }; mockCreateEnv.mockReturnValue(mockTransformedEnv); - const pluginInstance = createEnvPlugin({ + const pluginInstance = arkenvPlugin({ VITE_API_URL: "string", PUBLIC_DEBUG: "boolean", CUSTOM_PREFIX_VAR: "string", @@ -546,7 +546,7 @@ describe("Plugin Unit Tests", () => { it("should use custom envDir when provided in config", async () => { mockCreateEnv.mockReturnValue({ VITE_TEST: "test" }); - const pluginInstance = createEnvPlugin({ VITE_TEST: "string" }); + const pluginInstance = arkenvPlugin({ VITE_TEST: "string" }); if (pluginInstance.config && typeof pluginInstance.config === "function") { const mockContext = { @@ -584,7 +584,7 @@ describe("Plugin Unit Tests", () => { it("should default to process.cwd() when envDir is not configured", () => { mockCreateEnv.mockReturnValue({ VITE_TEST: "test" }); - const pluginInstance = createEnvPlugin({ VITE_TEST: "string" }); + const pluginInstance = arkenvPlugin({ VITE_TEST: "string" }); if (pluginInstance.config && typeof pluginInstance.config === "function") { const mockContext = { @@ -629,7 +629,7 @@ describe("Plugin Unit Tests", () => { const actual = await vi.importActual("arkenv"); mockCreateEnv.mockImplementation(actual.createEnv); - const pluginInstance = createEnvPlugin(schema); + const pluginInstance = arkenvPlugin(schema); let result: any = {}; if (pluginInstance.config && typeof pluginInstance.config === "function") { @@ -676,7 +676,7 @@ describe("Custom envDir Configuration (with-env-dir fixture)", () => { configFile: false as const, root: withEnvDirFixture, envDir, - plugins: [createEnvPlugin(schema)], + plugins: [arkenvPlugin(schema)], logLevel: "error" as const, build: { lib: { entry: "index.ts", formats: ["es" as const] }, @@ -692,8 +692,8 @@ describe("Custom envDir Configuration (with-env-dir fixture)", () => { afterEach(() => { vi.unstubAllEnvs(); - mockCreateEnv.mockReset(); - mockLoadEnv.mockReset(); + mockCreateEnv.mockClear(); + mockLoadEnv.mockClear(); }); it("should load environment variables from custom envDir", async () => { @@ -760,7 +760,7 @@ describe("Custom envDir Configuration (with-env-dir fixture)", () => { async function readTestConfig(fixtureDir: string) { // Import the env schema from the TypeScript config file - let Env: Record = {}; + let Env: Record = {}; try { const configPath = join(fixtureDir, "config.ts"); const configModule = await import(configPath); diff --git a/packages/vite-plugin/src/index.ts b/packages/vite-plugin/src/index.ts index 215b5fd65..83f783907 100644 --- a/packages/vite-plugin/src/index.ts +++ b/packages/vite-plugin/src/index.ts @@ -16,7 +16,8 @@ export type { ImportMetaEnvAugmented } from "./types"; * automatically filters them based on Vite's `envPrefix` configuration (defaults to `"VITE_"`). * Only environment variables matching the prefix are exposed to client code via `import.meta.env.*`. * - * @param options - The environment variable schema definition as an object mapping environment keys to validators. + * @param options - The environment variable schema definition. Can be an `EnvSchema` object + * for typesafe validation or an ArkType `EnvSchemaWithType` for dynamic schemas. * @returns A Vite plugin that validates environment variables and exposes them to the client. * * @example @@ -59,9 +60,9 @@ export default function arkenv( // Load environment based on the custom config const envDir = config.envDir ?? config.root ?? process.cwd(); - // NOTE: We use 'options as any' here strictly to bypass TypeScript's "Type instantiation is excessively deep" errors. - // This assertion ONLY affects compile-time checking; runtime validation (including Standard Schema via def["~standard"]) - // remains fully intact and functional. + // TODO: We're using type assertions and explicitly pass in the type arguments here to avoid + // "Type instantiation is excessively deep and possibly infinite" errors. + // Ideally, we should find a way to avoid these assertions while maintaining type safety. const env = createEnv(options as any, { env: loadEnv(mode, envDir, ""), }); From e71c510d10db8d5ab1fe88d1a113f2166a9efebd Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 22:25:53 +0500 Subject: [PATCH 043/117] refactor(arkenv): rename createEnv to arkenv - Update plugin imports and names to arkenv - Simplify EnvSchema type inference - Improve JSDoc for createEnv --- packages/arkenv/src/create-env.ts | 133 +++++++++++-------------- packages/bun-plugin/src/index.test.ts | 12 +-- packages/bun-plugin/src/index.ts | 48 ++++----- packages/vite-plugin/src/index.test.ts | 37 ------- packages/vite-plugin/src/index.ts | 2 +- 5 files changed, 91 insertions(+), 141 deletions(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 0fa87b59d..db5c881c5 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -1,5 +1,4 @@ import { createRequire } from "node:module"; -import type { $ } from "@repo/scope"; import type { EnvSchemaWithType, InferType, SchemaShape } from "@repo/types"; import type { StandardSchemaV1 } from "@standard-schema/spec"; import type { type as at, distill } from "arktype"; @@ -9,39 +8,37 @@ import { coerce } from "./utils/coerce"; const require = createRequire(import.meta.url); -export type EnvSchema = at.validate; -type RuntimeEnvironment = Record; +export type EnvSchema = at.validate>>; /** - * Configuration options for ArkEnv + * Configuration options for `createEnv` */ export type ArkEnvConfig = { /** * The environment variables to validate. Defaults to `process.env` */ - env?: RuntimeEnvironment; + env?: Record; /** - * Whether to coerce environment variables to their defined types. - * Only supported for ArkType schemas. - * @default true + * Whether to coerce environment variables to their defined types. Defaults to `true` */ coerce?: boolean; /** - * Control how ArkEnv handles undeclared keys. - * Only supported for ArkType schemas. + * How to handle undeclared keys in the schema. + * - `delete`: Remove undeclared keys. + * - `ignore`: Leave undeclared keys as they are. + * - `reject`: Throw an error if undeclared keys are present. * @default "delete" */ - onUndeclaredKey?: "ignore" | "delete" | "reject"; + onUndeclaredKey?: "delete" | "ignore" | "reject"; /** - * Options for array parsing. + * For arrays, specifies how to parse the string value. + * - `comma`: Split by commas and trim whitespace. + * - `json`: Strings are parsed as JSON. * @default "comma" */ arrayFormat?: CoerceOptions["arrayFormat"]; }; -/** - * Detects the type of validator being used. - */ function detectValidatorType(def: unknown) { const isStandard = !!(def as any)?.["~standard"]; const isArkCompiled = @@ -50,9 +47,45 @@ function detectValidatorType(def: unknown) { return { isStandard, isArkCompiled }; } -/** - * Internal validation logic for ArkType schemas. - */ +function validateStandard( + def: StandardSchemaV1, + env: Record, +): { success: true; value: unknown } | { success: false; issues: EnvIssue[] } { + const result = def["~standard"].validate(env); + + if (result instanceof Promise) { + throw new Error("ArkEnv does not support asynchronous validation."); + } + + if (result.issues) { + return { + success: false, + issues: result.issues.map((issue) => ({ + path: + issue.path?.map((segment: unknown) => { + if (typeof segment === "string") return segment; + if (typeof segment === "number") return String(segment); + if (typeof segment === "symbol") return segment.toString(); + if ( + typeof segment === "object" && + segment !== null && + "key" in segment + ) { + return String((segment as { key: unknown }).key); + } + return String(segment); + }) ?? [], + message: issue.message, + })), + }; + } + + return { + success: true, + value: result.value, + }; +} + function validateArkType( def: unknown, config: ArkEnvConfig, @@ -106,66 +139,18 @@ function validateArkType( } /** - * Internal validation logic for Standard Schema validators. + * Validate and distill environment variables based on a schema. + * + * @param def - The environment variable schema definition. Can be a mapping of keys to validators, + * or a compiled ArkType schema. + * @param config - Optional configuration for validation and coercion. + * @returns The validated and distilled environment variables. + * @throws {ArkEnvError} If validation fails. */ -function validateStandard( - def: StandardSchemaV1, - env: Record, -): { success: true; value: unknown } | { success: false; issues: EnvIssue[] } { - const result = def["~standard"].validate(env); - - if (result instanceof Promise) { - throw new Error("ArkEnv does not support asynchronous validation."); - } - - if (result.issues) { - return { - success: false, - issues: result.issues.map((issue) => ({ - path: - issue.path?.map((segment: unknown) => { - if (typeof segment === "string") return segment; - if (typeof segment === "number") return String(segment); - if (typeof segment === "symbol") return segment.toString(); - if ( - typeof segment === "object" && - segment !== null && - "key" in segment - ) { - return String((segment as { key: unknown }).key); - } - return String(segment); - }) ?? [], - message: issue.message, - })), - }; - } - - return { - success: true, - value: result.value, - }; -} - -/** - * Create an environment variables object from a schema and an environment - * @param def - The environment variable schema (raw object or type definition created with `type()`) - * @param config - Configuration options, see {@link ArkEnvConfig} - * @returns The validated environment variable schema - * @throws An {@link ArkEnvError | error} if the environment variables are invalid. - */ -export function createEnv( - def: EnvSchema, - config?: ArkEnvConfig, -): distill.Out>; -export function createEnv( - def: T, - config?: ArkEnvConfig, -): InferType; export function createEnv( def: EnvSchema | EnvSchemaWithType, config: ArkEnvConfig = {}, -): distill.Out> | InferType { +): distill.Out> | InferType { const { env = process.env } = config; const { isStandard, isArkCompiled } = detectValidatorType(def); diff --git a/packages/bun-plugin/src/index.test.ts b/packages/bun-plugin/src/index.test.ts index 4053c323c..e4b9ee329 100644 --- a/packages/bun-plugin/src/index.test.ts +++ b/packages/bun-plugin/src/index.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import createEnvPlugin, { processEnvSchema } from "./index.js"; +import arkenvPlugin, { processEnvSchema } from "./index.js"; describe("Bun Plugin", () => { let originalEnv: NodeJS.ProcessEnv; @@ -13,16 +13,16 @@ describe("Bun Plugin", () => { }); it("should create a plugin function", () => { - expect(typeof createEnvPlugin).toBe("function"); + expect(typeof arkenvPlugin).toBe("function"); }); it("should return a Bun plugin object", () => { // Set up a valid environment variable process.env.BUN_PUBLIC_TEST = "test-value"; - const pluginInstance = createEnvPlugin({ BUN_PUBLIC_TEST: "string" }); + const pluginInstance = arkenvPlugin({ BUN_PUBLIC_TEST: "string" }); - expect(pluginInstance).toHaveProperty("name", "@createEnv/bun-plugin"); + expect(pluginInstance).toHaveProperty("name", "@arkenv/bun-plugin"); expect(pluginInstance).toHaveProperty("setup"); expect(typeof pluginInstance.setup).toBe("function"); }); @@ -32,7 +32,7 @@ describe("Bun Plugin", () => { process.env.BUN_PUBLIC_TEST = "test-value"; expect(() => { - createEnvPlugin({ BUN_PUBLIC_TEST: "string" }); + arkenvPlugin({ BUN_PUBLIC_TEST: "string" }); }).not.toThrow(); }); @@ -41,7 +41,7 @@ describe("Bun Plugin", () => { delete process.env.BUN_PUBLIC_REQUIRED; expect(() => { - createEnvPlugin({ BUN_PUBLIC_REQUIRED: "string" }); + arkenvPlugin({ BUN_PUBLIC_REQUIRED: "string" }); }).toThrow(); }); diff --git a/packages/bun-plugin/src/index.ts b/packages/bun-plugin/src/index.ts index 6c2940a36..9ba5881c4 100644 --- a/packages/bun-plugin/src/index.ts +++ b/packages/bun-plugin/src/index.ts @@ -1,7 +1,7 @@ import { join } from "node:path"; import type { EnvSchemaWithType, SchemaShape } from "@repo/types"; -import type { EnvSchema } from "createEnv"; -import { createEnv } from "createEnv"; +import type { EnvSchema } from "arkenv"; +import { createEnv } from "arkenv"; import type { BunPlugin, Loader, PluginBuilder } from "bun"; export type { ProcessEnvAugmented } from "./types"; @@ -18,7 +18,7 @@ export function processEnvSchema( // "Type instantiation is excessively deep and possibly infinite" errors. // Ideally, we should find a way to avoid these assertions while maintaining type safety. - const env = createEnv(options as any, { env: process.env }); + const env = createEnv(options, { env: process.env }); // Get Bun's prefix for client-exposed environment variables const prefix = "BUN_PUBLIC_"; @@ -104,37 +104,37 @@ function registerLoader(build: PluginBuilder, envMap: Map) { * In `bunfig.toml`: * ```toml * [serve.static] - * plugins = ["@createEnv/bun-plugin"] + * plugins = ["@arkenv/bun-plugin"] * ``` * and in `Bun.build`: * ```ts - * import createEnv from "@createEnv/bun-plugin"; + * import arkenv from "@arkenv/bun-plugin"; * Bun.build({ - * plugins: [createEnv] + * plugins: [arkenv] * }) * ``` * * 2. **Manual Configuration**: * Call it as a function in `Bun.build` with your schema. * ```ts - * import createEnv from "@createEnv/bun-plugin"; + * import arkenv from "@arkenv/bun-plugin"; * import { Env } from "./src/env"; * Bun.build({ - * plugins: [createEnv(Env)] + * plugins: [arkenv(Env)] * }) * ``` */ -export function createEnv(options: EnvSchemaWithType): BunPlugin; -export function createEnv( +export function arkenv(options: EnvSchemaWithType): BunPlugin; +export function arkenv( options: EnvSchema, ): BunPlugin; -export function createEnv( +export function arkenv( options: EnvSchema | EnvSchemaWithType, ): BunPlugin { const envMap = processEnvSchema(options); return { - name: "@createEnv/bun-plugin", + name: "@arkenv/bun-plugin", setup(build) { registerLoader(build, envMap); }, @@ -142,7 +142,7 @@ export function createEnv( } // Attach static analysis properties to the function to make it a valid BunPlugin object -// This allows it to be used in bunfig.toml as `plugins = ["@createEnv/bun-plugin"]` +// This allows it to be used in bunfig.toml as `plugins = ["@arkenv/bun-plugin"]` /** * Bun plugin to validate environment variables using ArkEnv and expose prefixed variables to client code. * @@ -154,30 +154,30 @@ export function createEnv( * In `bunfig.toml`: * ```toml * [serve.static] - * plugins = ["@createEnv/bun-plugin"] + * plugins = ["@arkenv/bun-plugin"] * ``` * and in `Bun.build`: * ```ts - * import createEnv from "@createEnv/bun-plugin"; + * import arkenv from "@arkenv/bun-plugin"; * Bun.build({ - * plugins: [createEnv] + * plugins: [arkenv] * }) * ``` * * 2. **Manual Configuration**: * Call it as a function in `Bun.build` with your schema. * ```ts - * import createEnv from "@createEnv/bun-plugin"; + * import arkenv from "@arkenv/bun-plugin"; * import { Env } from "./src/env"; * Bun.build({ - * plugins: [createEnv(Env)] + * plugins: [arkenv(Env)] * }) * ``` */ -const hybrid = createEnv as typeof createEnv & BunPlugin; +const hybrid = arkenv as typeof arkenv & BunPlugin; Object.defineProperty(hybrid, "name", { - value: "@createEnv/bun-plugin", + value: "@arkenv/bun-plugin", writable: false, }); @@ -219,14 +219,16 @@ hybrid.setup = (build) => { const example = ` Example \`src/env.ts\`: \`\`\`ts -export default { +import { type } from "arktype"; + +export default type({ BUN_PUBLIC_API_URL: "string", BUN_PUBLIC_DEBUG: "boolean" -}; +}); \`\`\` `; throw new Error( - `@createEnv/bun-plugin: No environment schema found.\n\nChecked paths:\n${pathsList}\n\nPlease create a schema file at one of these locations exporting your environment definition.\n${example}`, + `@arkenv/bun-plugin: No environment schema found.\n\nChecked paths:\n${pathsList}\n\nPlease create a schema file at one of these locations exporting your environment definition.\n${example}`, ); } diff --git a/packages/vite-plugin/src/index.test.ts b/packages/vite-plugin/src/index.test.ts index 169ada7b8..b1e307cd5 100644 --- a/packages/vite-plugin/src/index.test.ts +++ b/packages/vite-plugin/src/index.test.ts @@ -2,7 +2,6 @@ import { readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; import * as vite from "vite"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { z } from "zod"; // Mock the arkenv module to capture calls // Mock the arkenv module with a spy that calls the real implementation by default @@ -618,42 +617,6 @@ describe("Plugin Unit Tests", () => { }, ); }); - - it("should work with a real Standard Schema validator (e.g. Zod)", async () => { - vi.stubEnv("VITE_ZOD_VAR", "valid-value"); - const schema = { - VITE_ZOD_VAR: z.string().min(5), - }; - - // Note: We use the real implementation for this test - const actual = await vi.importActual("arkenv"); - mockCreateEnv.mockImplementation(actual.createEnv); - - const pluginInstance = arkenvPlugin(schema); - - let result: any = {}; - if (pluginInstance.config && typeof pluginInstance.config === "function") { - const mockContext = { - meta: { - framework: "vite", - version: "1.0.0", - rollupVersion: "4.0.0", - viteVersion: "5.0.0", - }, - error: vi.fn(), - warn: vi.fn(), - info: vi.fn(), - debug: vi.fn(), - } as any; - result = pluginInstance.config.call( - mockContext, - {}, - { mode: "test", command: "build" }, - ); - } - - expect(result.define).toHaveProperty("import.meta.env.VITE_ZOD_VAR"); - }); }); // Integration tests using with-env-dir fixture for custom envDir configuration diff --git a/packages/vite-plugin/src/index.ts b/packages/vite-plugin/src/index.ts index 83f783907..3c95a2e6c 100644 --- a/packages/vite-plugin/src/index.ts +++ b/packages/vite-plugin/src/index.ts @@ -63,7 +63,7 @@ export default function arkenv( // TODO: We're using type assertions and explicitly pass in the type arguments here to avoid // "Type instantiation is excessively deep and possibly infinite" errors. // Ideally, we should find a way to avoid these assertions while maintaining type safety. - const env = createEnv(options as any, { + const env = createEnv(options, { env: loadEnv(mode, envDir, ""), }); From 78646aa96e5ce2e35b8c5a859d753f3580b9db09 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 22:27:29 +0500 Subject: [PATCH 044/117] feat: Add Zod schema support for Vite/Bun plugins - Added type assertion to `createEnv` call - Resolved deep type instantiation errors - Verified Zod schema compatibility with --- packages/bun-plugin/src/index.ts | 2 +- packages/vite-plugin/src/index.test.ts | 37 ++++++++++++++++++++++++++ packages/vite-plugin/src/index.ts | 2 +- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/bun-plugin/src/index.ts b/packages/bun-plugin/src/index.ts index 9ba5881c4..4d9286101 100644 --- a/packages/bun-plugin/src/index.ts +++ b/packages/bun-plugin/src/index.ts @@ -18,7 +18,7 @@ export function processEnvSchema( // "Type instantiation is excessively deep and possibly infinite" errors. // Ideally, we should find a way to avoid these assertions while maintaining type safety. - const env = createEnv(options, { env: process.env }); + const env = createEnv(options as any, { env: process.env }); // Get Bun's prefix for client-exposed environment variables const prefix = "BUN_PUBLIC_"; diff --git a/packages/vite-plugin/src/index.test.ts b/packages/vite-plugin/src/index.test.ts index b1e307cd5..169ada7b8 100644 --- a/packages/vite-plugin/src/index.test.ts +++ b/packages/vite-plugin/src/index.test.ts @@ -2,6 +2,7 @@ import { readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; import * as vite from "vite"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { z } from "zod"; // Mock the arkenv module to capture calls // Mock the arkenv module with a spy that calls the real implementation by default @@ -617,6 +618,42 @@ describe("Plugin Unit Tests", () => { }, ); }); + + it("should work with a real Standard Schema validator (e.g. Zod)", async () => { + vi.stubEnv("VITE_ZOD_VAR", "valid-value"); + const schema = { + VITE_ZOD_VAR: z.string().min(5), + }; + + // Note: We use the real implementation for this test + const actual = await vi.importActual("arkenv"); + mockCreateEnv.mockImplementation(actual.createEnv); + + const pluginInstance = arkenvPlugin(schema); + + let result: any = {}; + if (pluginInstance.config && typeof pluginInstance.config === "function") { + const mockContext = { + meta: { + framework: "vite", + version: "1.0.0", + rollupVersion: "4.0.0", + viteVersion: "5.0.0", + }, + error: vi.fn(), + warn: vi.fn(), + info: vi.fn(), + debug: vi.fn(), + } as any; + result = pluginInstance.config.call( + mockContext, + {}, + { mode: "test", command: "build" }, + ); + } + + expect(result.define).toHaveProperty("import.meta.env.VITE_ZOD_VAR"); + }); }); // Integration tests using with-env-dir fixture for custom envDir configuration diff --git a/packages/vite-plugin/src/index.ts b/packages/vite-plugin/src/index.ts index 3c95a2e6c..83f783907 100644 --- a/packages/vite-plugin/src/index.ts +++ b/packages/vite-plugin/src/index.ts @@ -63,7 +63,7 @@ export default function arkenv( // TODO: We're using type assertions and explicitly pass in the type arguments here to avoid // "Type instantiation is excessively deep and possibly infinite" errors. // Ideally, we should find a way to avoid these assertions while maintaining type safety. - const env = createEnv(options, { + const env = createEnv(options as any, { env: loadEnv(mode, envDir, ""), }); From 74dcbc59683d5d0370d70c538cd0ef6eba545aca Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 22:28:00 +0500 Subject: [PATCH 045/117] test(bun-plugin): add test for Zod schema --- packages/arkenv/src/utils/coerce.ts | 2 +- packages/bun-plugin/src/index.test.ts | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/arkenv/src/utils/coerce.ts b/packages/arkenv/src/utils/coerce.ts index 45d2f2cd8..8092b4a44 100644 --- a/packages/arkenv/src/utils/coerce.ts +++ b/packages/arkenv/src/utils/coerce.ts @@ -1,6 +1,6 @@ import { createRequire } from "node:module"; import { maybeBoolean, maybeJson, maybeNumber } from "@repo/keywords"; -import { type JsonSchema } from "arktype"; +import type { JsonSchema } from "arktype"; const require = createRequire(import.meta.url); diff --git a/packages/bun-plugin/src/index.test.ts b/packages/bun-plugin/src/index.test.ts index e4b9ee329..ff058c456 100644 --- a/packages/bun-plugin/src/index.test.ts +++ b/packages/bun-plugin/src/index.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { z } from "zod"; import arkenvPlugin, { processEnvSchema } from "./index.js"; describe("Bun Plugin", () => { @@ -62,8 +63,17 @@ describe("Bun Plugin", () => { JSON.stringify("https://api.example.com"), ); - // Check that non-prefixed variables are filtered out expect(envMap.has("PORT")).toBe(false); expect(envMap.has("DATABASE_URL")).toBe(false); }); + + it("should work with a real Standard Schema validator (e.g. Zod)", () => { + process.env.BUN_PUBLIC_ZOD = "zod-value"; + const envMap = processEnvSchema({ + BUN_PUBLIC_ZOD: z.string().min(5), + }); + + expect(envMap.has("BUN_PUBLIC_ZOD")).toBe(true); + expect(envMap.get("BUN_PUBLIC_ZOD")).toBe(JSON.stringify("zod-value")); + }); }); From 7d1db999ca6a28329af787a9cfb38db33c1df504 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 22:32:47 +0500 Subject: [PATCH 046/117] build: update dependencies and fix references - Downgrade various package versions - Remove @standard-schema/spec dependency - Update Arkenv references in docs and code - --- apps/playgrounds/node/package.json | 2 +- apps/playgrounds/vite-legacy/package.json | 2 +- .../app/styles/components/github-alerts.css | 4 +- apps/www/package.json | 10 +- examples/basic/package-lock.json | 8 +- examples/basic/package.json | 2 +- examples/with-bun-react/bun.lock | 6 +- examples/with-bun/bun.lock | 4 +- package.json | 8 +- packages/arkenv/src/create-env.ts | 2 +- packages/bun-plugin/src/types.ts | 12 +- packages/vite-plugin/package.json | 2 +- .../src/__fixtures__/basic/config.ts | 6 +- .../src/__fixtures__/with-env-dir/config.ts | 6 +- pnpm-lock.yaml | 345 ++++++++---------- pnpm-workspace.yaml | 2 +- 16 files changed, 200 insertions(+), 221 deletions(-) diff --git a/apps/playgrounds/node/package.json b/apps/playgrounds/node/package.json index 5655c888a..2d9b38a44 100644 --- a/apps/playgrounds/node/package.json +++ b/apps/playgrounds/node/package.json @@ -12,7 +12,7 @@ "dependencies": { "arkenv": "workspace:*", "arktype": "catalog:", - "zod": "4.3.5" + "zod": "4.2.1" }, "devDependencies": { "@types/node": "catalog:", diff --git a/apps/playgrounds/vite-legacy/package.json b/apps/playgrounds/vite-legacy/package.json index 465a30e60..f3481e3a1 100644 --- a/apps/playgrounds/vite-legacy/package.json +++ b/apps/playgrounds/vite-legacy/package.json @@ -22,7 +22,7 @@ "@vitejs/plugin-react": "5.1.2", "globals": "16.5.0", "typescript": "5.9.3", - "vite": "7.3.1", + "vite": "7.3.0", "vite-tsconfig-paths": "catalog:" } } diff --git a/apps/www/app/styles/components/github-alerts.css b/apps/www/app/styles/components/github-alerts.css index 7fa7fe988..35b7b17e3 100644 --- a/apps/www/app/styles/components/github-alerts.css +++ b/apps/www/app/styles/components/github-alerts.css @@ -7,8 +7,8 @@ --rehype-github-alerts-caution-color: rgb(209, 36, 47); --rehype-github-alerts-default-space: 1rem; --rehype-github-alerts-default-fontFamily: - -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, - Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; + -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, + sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; --rehype-github-alerts-default-fontWeight: 500; } diff --git a/apps/www/package.json b/apps/www/package.json index 09b6fdde5..b362dbfec 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -29,15 +29,15 @@ "arktype": "2.1.29", "class-variance-authority": "0.7.1", "clsx": "2.1.1", - "fumadocs-core": "16.4.4", + "fumadocs-core": "16.4.2", "fumadocs-mdx": "14.2.4", - "fumadocs-twoslash": "3.1.12", - "fumadocs-ui": "16.4.4", + "fumadocs-twoslash": "3.1.11", + "fumadocs-ui": "16.4.2", "import-in-the-middle": "2.0.1", "lucide-react": "0.562.0", "next": "16.1.1", "next-themes": "0.4.6", - "posthog-js": "1.313.0", + "posthog-js": "1.310.2", "posthog-node": "5.18.1", "react": "19.2.3", "react-dom": "19.2.3", @@ -51,7 +51,7 @@ "twoslash": "0.3.6", "unist-util-visit": "5.0.0", "valibot": "catalog:", - "vite": "7.3.1", + "vite": "7.3.0", "zod": "catalog:" }, "devDependencies": { diff --git a/examples/basic/package-lock.json b/examples/basic/package-lock.json index 2ae4d4502..0140bb141 100644 --- a/examples/basic/package-lock.json +++ b/examples/basic/package-lock.json @@ -8,7 +8,7 @@ "dependencies": { "arkenv": "^0.8.3", "arktype": "^2.1.29", - "zod": "4.3.5" + "zod": "4.2.1" }, "devDependencies": { "@types/node": "^24.10.4", @@ -760,9 +760,9 @@ "license": "MIT" }, "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.2.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", + "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/examples/basic/package.json b/examples/basic/package.json index 3fb4c7503..c1f5bf148 100644 --- a/examples/basic/package.json +++ b/examples/basic/package.json @@ -11,7 +11,7 @@ "dependencies": { "arkenv": "^0.8.3", "arktype": "^2.1.29", - "zod": "4.3.5" + "zod": "4.2.1" }, "devDependencies": { "@types/node": "^24.10.4", diff --git a/examples/with-bun-react/bun.lock b/examples/with-bun-react/bun.lock index f50ed1c33..e5e03d7d6 100644 --- a/examples/with-bun-react/bun.lock +++ b/examples/with-bun-react/bun.lock @@ -6,7 +6,7 @@ "name": "arkenv-example-with-bun-react", "dependencies": { "@arkenv/bun-plugin": "^0.0.8", - "arkenv": "^0.8.3", + "arkenv": "^0.8.1", "arktype": "^2.1.29", "react": "^19.2.3", "react-dom": "^19.2.3", @@ -60,7 +60,7 @@ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], - "arkenv": ["arkenv@0.8.3", "", { "peerDependencies": { "arktype": "^2.1.22" } }, "sha512-fndPYpIZ/EvARTXabWG5H+gKxlJEbPgTRvXH8htimmCbdBfEXZsSOgObwdiCCCcBz33tJAYk88goDtj0Ao99NA=="], + "arkenv": ["arkenv@0.8.1", "", { "peerDependencies": { "arktype": "^2.1.22" } }, "sha512-6aGadHN2nuWuRNWcmNc/CfdlUak6jeZi/WBLLWN/LvBFY8aR+FSMJE6x9SUS9L1m+qp94MTVyKpi3tDzo+SoJw=="], "arkregex": ["arkregex@0.0.5", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw=="], @@ -93,5 +93,7 @@ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@arkenv/bun-plugin/arkenv": ["arkenv@0.8.3", "", { "peerDependencies": { "arktype": "^2.1.22" } }, "sha512-fndPYpIZ/EvARTXabWG5H+gKxlJEbPgTRvXH8htimmCbdBfEXZsSOgObwdiCCCcBz33tJAYk88goDtj0Ao99NA=="], } } diff --git a/examples/with-bun/bun.lock b/examples/with-bun/bun.lock index 5191b2967..6d27c85bc 100644 --- a/examples/with-bun/bun.lock +++ b/examples/with-bun/bun.lock @@ -5,7 +5,7 @@ "": { "name": "arkenv-example-with-bun", "dependencies": { - "arkenv": "^0.8.3", + "arkenv": "^0.8.1", "arktype": "^2.1.29", }, "devDependencies": { @@ -28,7 +28,7 @@ "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], - "arkenv": ["arkenv@0.8.3", "", { "peerDependencies": { "arktype": "^2.1.22" } }, "sha512-fndPYpIZ/EvARTXabWG5H+gKxlJEbPgTRvXH8htimmCbdBfEXZsSOgObwdiCCCcBz33tJAYk88goDtj0Ao99NA=="], + "arkenv": ["arkenv@0.8.1", "", { "peerDependencies": { "arktype": "^2.1.22" } }, "sha512-6aGadHN2nuWuRNWcmNc/CfdlUak6jeZi/WBLLWN/LvBFY8aR+FSMJE6x9SUS9L1m+qp94MTVyKpi3tDzo+SoJw=="], "arkregex": ["arkregex@0.0.5", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw=="], diff --git a/package.json b/package.json index c719851c9..b165471c8 100644 --- a/package.json +++ b/package.json @@ -30,19 +30,17 @@ "contributors:generate": "all-contributors generate" }, "devDependencies": { - "@biomejs/biome": "2.3.11", + "@biomejs/biome": "2.3.10", "@changesets/cli": "2.29.8", "@manypkg/cli": "0.25.1", "@playwright/test": "", - "@standard-schema/spec": "1.0.0", "@vitest/ui": "catalog:", "all-contributors-cli": "^6.26.1", "changesets-changelog-clean": "1.3.0", "rimraf": "catalog:", - "turbo": "2.7.3", + "turbo": "2.7.2", "typescript": "catalog:", - "vitest": "catalog:", - "zod": "catalog:" + "vitest": "catalog:" }, "packageManager": "pnpm@10.27.0", "pnpm": { diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index db5c881c5..816ba046b 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -159,7 +159,7 @@ export function createEnv( // Reusable type() schemas (ArkType) are allowed. if (isStandard && !isArkCompiled) { throw new Error( - "ArkEnv: createEnv() expects a mapping of { KEY: validator }, not a top-level Standard Schema (e.g. z.object()). " + + "ArkEnv: arkenv() expects a mapping of { KEY: validator }, not a top-level Standard Schema (e.g. z.object()). " + "Standard Schema validators are supported inside the mapping, or you can use ArkType's type() for top-level schemas.", ); } diff --git a/packages/bun-plugin/src/types.ts b/packages/bun-plugin/src/types.ts index db4f0176a..bfae18e56 100644 --- a/packages/bun-plugin/src/types.ts +++ b/packages/bun-plugin/src/types.ts @@ -5,18 +5,18 @@ import type { type } from "arktype"; * Augment the `process.env` object with typesafe environment variables * based on the schema validator. * - * This type extracts the inferred type from the schema (result of `type()` from createEnv), + * This type extracts the inferred type from the schema (result of `type()` from arkenv), * filters it to only include variables matching the Bun prefix (defaults to "BUN_PUBLIC_"), * and makes them available on `process.env`. * - * @template TSchema - The environment variable schema (result of `type()` from createEnv) + * @template TSchema - The environment variable schema (result of `type()` from arkenv) * @template Prefix - The prefix to filter by (defaults to "BUN_PUBLIC_") * * @example * ```ts * // bun.config.ts or similar - * import createEnv from '@createEnv/bun-plugin'; - * import { type } from 'createEnv'; + * import arkenv from '@arkenv/bun-plugin'; + * import { type } from 'arkenv'; * * export const Env = type({ * BUN_PUBLIC_API_URL: 'string', @@ -25,7 +25,7 @@ import type { type } from "arktype"; * }); * * export default { - * plugins: [createEnv(Env)], + * plugins: [arkenv(Env)], * }; * ``` * @@ -34,7 +34,7 @@ import type { type } from "arktype"; * // src/env.d.ts * /// * - * import type { ProcessEnvAugmented } from '@createEnv/bun-plugin'; + * import type { ProcessEnvAugmented } from '@arkenv/bun-plugin'; * import type { Env } from './env'; // or from bun.config.ts * * declare global { diff --git a/packages/vite-plugin/package.json b/packages/vite-plugin/package.json index 456516e4a..6ab1cc9a5 100644 --- a/packages/vite-plugin/package.json +++ b/packages/vite-plugin/package.json @@ -19,7 +19,7 @@ "size-limit": "catalog:", "tsdown": "catalog:", "typescript": "catalog:", - "vite": "7.3.1", + "vite": "7.3.0", "vite-tsconfig-paths": "catalog:", "vitest": "catalog:" }, diff --git a/packages/vite-plugin/src/__fixtures__/basic/config.ts b/packages/vite-plugin/src/__fixtures__/basic/config.ts index 47ebab974..5823e0c88 100644 --- a/packages/vite-plugin/src/__fixtures__/basic/config.ts +++ b/packages/vite-plugin/src/__fixtures__/basic/config.ts @@ -1,4 +1,6 @@ -export const Env = { +import { type } from "arkenv"; + +export const Env = type({ VITE_API_URL: "string", VITE_DEBUG: "boolean", -}; +}); diff --git a/packages/vite-plugin/src/__fixtures__/with-env-dir/config.ts b/packages/vite-plugin/src/__fixtures__/with-env-dir/config.ts index b0fec826e..ae43dd42a 100644 --- a/packages/vite-plugin/src/__fixtures__/with-env-dir/config.ts +++ b/packages/vite-plugin/src/__fixtures__/with-env-dir/config.ts @@ -1,4 +1,6 @@ -export const Env = { +import { type } from "arkenv"; + +export const Env = type({ VITE_CUSTOM_VAR: "string", VITE_FROM_ENV_DIR: "string", -}; +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c811ca553..ff177160b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -76,8 +76,8 @@ catalogs: specifier: 4.0.16 version: 4.0.16 zod: - specifier: 4.3.5 - version: 4.3.5 + specifier: 4.2.1 + version: 4.2.1 overrides: '@playwright/test': 1.57.0 @@ -89,8 +89,8 @@ importers: .: devDependencies: '@biomejs/biome': - specifier: 2.3.11 - version: 2.3.11 + specifier: 2.3.10 + version: 2.3.10 '@changesets/cli': specifier: 2.29.8 version: 2.29.8(@types/node@24.10.4) @@ -100,9 +100,6 @@ importers: '@playwright/test': specifier: 1.57.0 version: 1.57.0 - '@standard-schema/spec': - specifier: 1.0.0 - version: 1.0.0 '@vitest/ui': specifier: 'catalog:' version: 4.0.16(vitest@4.0.16) @@ -116,17 +113,14 @@ importers: specifier: 'catalog:' version: 6.1.2 turbo: - specifier: 2.7.3 - version: 2.7.3 + specifier: 2.7.2 + version: 2.7.2 typescript: specifier: 'catalog:' version: 5.9.3 vitest: specifier: 'catalog:' version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) - zod: - specifier: 'catalog:' - version: 4.3.5 apps/playgrounds/bun: dependencies: @@ -196,8 +190,8 @@ importers: specifier: 'catalog:' version: 2.1.29 zod: - specifier: 4.3.5 - version: 4.3.5 + specifier: 4.2.1 + version: 4.2.1 devDependencies: '@types/node': specifier: 'catalog:' @@ -219,7 +213,7 @@ importers: version: link:../../../packages/vite-plugin '@solidjs/start': specifier: 1.2.1 - version: 1.2.1(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vinxi@0.5.10(@types/node@24.10.4)(@vercel/blob@0.27.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.57)(terser@5.44.1)(tsx@4.21.0))(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) + version: 1.2.1(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vinxi@0.5.10(@types/node@24.10.4)(@vercel/blob@0.27.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.57)(terser@5.44.1)(tsx@4.21.0))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) arkenv: specifier: workspace:* version: link:../../../packages/arkenv @@ -243,7 +237,7 @@ importers: version: 2.1.29 zod: specifier: 'catalog:' - version: 4.3.5 + version: 4.2.1 devDependencies: rimraf: specifier: 'catalog:' @@ -330,7 +324,7 @@ importers: version: 19.2.3(@types/react@19.2.7) '@vitejs/plugin-react': specifier: 5.1.2 - version: 5.1.2(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) + version: 5.1.2(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) globals: specifier: 16.5.0 version: 16.5.0 @@ -338,11 +332,11 @@ importers: specifier: 5.9.3 version: 5.9.3 vite: - specifier: 7.3.1 - version: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) + specifier: 7.3.0 + version: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) vite-tsconfig-paths: specifier: 'catalog:' - version: 6.0.3(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) + version: 6.0.3(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) apps/www: dependencies: @@ -354,7 +348,7 @@ importers: version: link:../../packages/vite-plugin '@fumadocs/mdx-remote': specifier: 1.4.4 - version: 1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(react@19.2.3) + version: 1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1))(react@19.2.3) '@icons-pack/react-simple-icons': specifier: 13.8.0 version: 13.8.0(react@19.2.3) @@ -395,17 +389,17 @@ importers: specifier: 2.1.1 version: 2.1.1 fumadocs-core: - specifier: 16.4.4 - version: 16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) + specifier: 16.4.2 + version: 16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1) fumadocs-mdx: specifier: 14.2.4 - version: 14.2.4(@fumadocs/mdx-remote@1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(react@19.2.3))(@types/react@19.2.7)(fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) + version: 14.2.4(@fumadocs/mdx-remote@1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1))(react@19.2.3))(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) fumadocs-twoslash: - specifier: 3.1.12 - version: 3.1.12(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(fumadocs-ui@16.4.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + specifier: 3.1.11 + version: 3.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(fumadocs-ui@16.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.2.1))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) fumadocs-ui: - specifier: 16.4.4 - version: 16.4.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5) + specifier: 16.4.2 + version: 16.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.2.1) import-in-the-middle: specifier: 2.0.0 version: 2.0.0 @@ -419,8 +413,8 @@ importers: specifier: 0.4.6 version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) posthog-js: - specifier: 1.313.0 - version: 1.313.0 + specifier: 1.310.2 + version: 1.310.2 posthog-node: specifier: 5.18.1 version: 5.18.1 @@ -461,11 +455,11 @@ importers: specifier: 'catalog:' version: 1.2.0(typescript@5.9.3) vite: - specifier: 7.3.1 - version: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) + specifier: 7.3.0 + version: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) zod: specifier: 'catalog:' - version: 4.3.5 + version: 4.2.1 devDependencies: '@tailwindcss/postcss': specifier: 4.1.18 @@ -502,7 +496,7 @@ importers: version: 19.2.3(@types/react@19.2.7) '@vitejs/plugin-react': specifier: 'catalog:' - version: 5.1.2(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) + version: 5.1.2(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) concurrently: specifier: 'catalog:' version: 9.2.1 @@ -532,7 +526,7 @@ importers: version: 5.9.3 vite-tsconfig-paths: specifier: 'catalog:' - version: 6.0.3(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) + version: 6.0.3(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) vitest: specifier: 'catalog:' version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) @@ -554,9 +548,6 @@ importers: '@size-limit/preset-small-lib': specifier: 'catalog:' version: 12.0.0(size-limit@12.0.0(jiti@2.6.1)) - '@standard-schema/spec': - specifier: 1.0.0 - version: 1.0.0 '@types/node': specifier: 'catalog:' version: 24.10.4 @@ -578,9 +569,6 @@ importers: vitest: specifier: 'catalog:' version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) - zod: - specifier: 'catalog:' - version: 4.3.5 packages/bun-plugin: dependencies: @@ -655,9 +643,6 @@ importers: '@repo/scope': specifier: workspace:* version: link:../scope - '@standard-schema/spec': - specifier: 1.0.0 - version: 1.0.0 arktype: specifier: 'catalog:' version: 2.1.29 @@ -693,11 +678,11 @@ importers: specifier: 'catalog:' version: 5.9.3 vite: - specifier: 7.3.1 - version: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) + specifier: 7.3.0 + version: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) vite-tsconfig-paths: specifier: 'catalog:' - version: 6.0.3(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) + version: 6.0.3(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) vitest: specifier: 'catalog:' version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) @@ -1032,55 +1017,55 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} - '@biomejs/biome@2.3.11': - resolution: {integrity: sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ==} + '@biomejs/biome@2.3.10': + resolution: {integrity: sha512-/uWSUd1MHX2fjqNLHNL6zLYWBbrJeG412/8H7ESuK8ewoRoMPUgHDebqKrPTx/5n6f17Xzqc9hdg3MEqA5hXnQ==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.3.11': - resolution: {integrity: sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA==} + '@biomejs/cli-darwin-arm64@2.3.10': + resolution: {integrity: sha512-M6xUjtCVnNGFfK7HMNKa593nb7fwNm43fq1Mt71kpLpb+4mE7odO8W/oWVDyBVO4ackhresy1ZYO7OJcVo/B7w==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.3.11': - resolution: {integrity: sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg==} + '@biomejs/cli-darwin-x64@2.3.10': + resolution: {integrity: sha512-Vae7+V6t/Avr8tVbFNjnFSTKZogZHFYl7MMH62P/J1kZtr0tyRQ9Fe0onjqjS2Ek9lmNLmZc/VR5uSekh+p1fg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.3.11': - resolution: {integrity: sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg==} + '@biomejs/cli-linux-arm64-musl@2.3.10': + resolution: {integrity: sha512-B9DszIHkuKtOH2IFeeVkQmSMVUjss9KtHaNXquYYWCjH8IstNgXgx5B0aSBQNr6mn4RcKKRQZXn9Zu1rM3O0/A==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-arm64@2.3.11': - resolution: {integrity: sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g==} + '@biomejs/cli-linux-arm64@2.3.10': + resolution: {integrity: sha512-hhPw2V3/EpHKsileVOFynuWiKRgFEV48cLe0eA+G2wO4SzlwEhLEB9LhlSrVeu2mtSn205W283LkX7Fh48CaxA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-x64-musl@2.3.11': - resolution: {integrity: sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw==} + '@biomejs/cli-linux-x64-musl@2.3.10': + resolution: {integrity: sha512-QTfHZQh62SDFdYc2nfmZFuTm5yYb4eO1zwfB+90YxUumRCR171tS1GoTX5OD0wrv4UsziMPmrePMtkTnNyYG3g==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-linux-x64@2.3.11': - resolution: {integrity: sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg==} + '@biomejs/cli-linux-x64@2.3.10': + resolution: {integrity: sha512-wwAkWD1MR95u+J4LkWP74/vGz+tRrIQvr8kfMMJY8KOQ8+HMVleREOcPYsQX82S7uueco60L58Wc6M1I9WA9Dw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-win32-arm64@2.3.11': - resolution: {integrity: sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw==} + '@biomejs/cli-win32-arm64@2.3.10': + resolution: {integrity: sha512-o7lYc9n+CfRbHvkjPhm8s9FgbKdYZu5HCcGVMItLjz93EhgJ8AM44W+QckDqLA9MKDNFrR8nPbO4b73VC5kGGQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.3.11': - resolution: {integrity: sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg==} + '@biomejs/cli-win32-x64@2.3.10': + resolution: {integrity: sha512-pHEFgq7dUEsKnqG9mx9bXihxGI49X+ar+UBrEIj3Wqj3UCZp1rNgV+OoyjFgcXsjCWpuEAF4VJdkZr3TrWdCbQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -1538,11 +1523,11 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} - '@formatjs/fast-memoize@3.0.3': - resolution: {integrity: sha512-CArYtQKGLAOruCMeq5/RxCg6vUXFx3OuKBdTm30Wn/+gCefehmZ8Y2xSMxMrO2iel7hRyE3HKfV56t3vAU6D4Q==} + '@formatjs/fast-memoize@3.0.1': + resolution: {integrity: sha512-kzk635kEmsxrrEWQXY7uKRocFCVXR4es5OQqcqCGg2NPtQztG/OBkE9THHu6UOTxpfyIkZhh6DjPBZGRp7y3og==} - '@formatjs/intl-localematcher@0.7.5': - resolution: {integrity: sha512-7/nd90cn5CT7SVF71/ybUKAcnvBlr9nZlJJp8O8xIZHXFgYOC4SXExZlSdgHv2l6utjw1byidL06QzChvQMHwA==} + '@formatjs/intl-localematcher@0.7.3': + resolution: {integrity: sha512-NaeABectKdTCOnlH9VFGmMS3K0JuR7Soc2t5R2MCkBrM3H/hlKVYh0XSrcjjPkbjIdrF7L/Bzx9JtGuVaSfYlA==} '@fumadocs/mdx-remote@1.4.4': resolution: {integrity: sha512-tODOw9cYWJkzJ6I+CG16U0QjRmwEVNUavlSn6jUyn4CJ7OjpnFU+jgOiqPm8STFghNYcYVjt3SlrRledGQKR4Q==} @@ -1554,8 +1539,8 @@ packages: '@types/react': optional: true - '@fumadocs/ui@16.4.4': - resolution: {integrity: sha512-OueycoepabZ8izxNQZzClGYMNZgMURZZzEQwxZYUxhS9Dp3vZLEWHdC3VPLGw8B0gKCAM+kebh9pgOpaTCGzGA==} + '@fumadocs/ui@16.4.2': + resolution: {integrity: sha512-cjeLclc4NQaGFkpDfFlustFSIeyNkrjj83gz0TvET+q8xKjRNZQPEc7EI+LdB313Dm51c+OUwFLYw95aiZhvJQ==} peerDependencies: '@types/react': '*' next: 16.x.x @@ -3614,9 +3599,6 @@ packages: '@speed-highlight/core@1.2.12': resolution: {integrity: sha512-uilwrK0Ygyri5dToHYdZSjcvpS2ZwX0w5aSt3GCEN9hrjxWCoeV4Z2DTXuxjwbntaLQIEEAlCeNQss5SoHvAEA==} - '@standard-schema/spec@1.0.0': - resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} - '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -5169,12 +5151,11 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - fumadocs-core@16.4.4: - resolution: {integrity: sha512-0mALev9/qyjIt0K7rD5W+r/Ri+N9PdxOUl/5c+QO7TUdTtr5wWK60AkOq1FEh+P6XDx8d6Bq4fiZJK/7fLT7kw==} + fumadocs-core@16.4.2: + resolution: {integrity: sha512-V6jepeDEgoGAVD1nrqt63ARdHyOB853jQOWpVl1cTheZjEW1iXD9HTVBllt254wrxaBgQLQLu6nbskg/sfBUIA==} peerDependencies: '@mixedbread/sdk': ^0.46.0 '@orama/core': 1.x.x - '@oramacloud/client': 2.x.x '@tanstack/react-router': 1.x.x '@types/react': '*' algoliasearch: 5.x.x @@ -5184,14 +5165,12 @@ packages: react-dom: ^19.2.0 react-router: 7.x.x waku: ^0.26.0 || ^0.27.0 - zod: 4.x.x + zod: '*' peerDependenciesMeta: '@mixedbread/sdk': optional: true '@orama/core': optional: true - '@oramacloud/client': - optional: true '@tanstack/react-router': optional: true '@types/react': @@ -5235,8 +5214,8 @@ packages: vite: optional: true - fumadocs-twoslash@3.1.12: - resolution: {integrity: sha512-s+81vm0+VsWUNy49SifNjvuv5p1y98EKg3EA5wHA2sN0FQG83LRyKa840YMTw9szvQxUdM2Jc+8t7g4pxdjxVw==} + fumadocs-twoslash@3.1.11: + resolution: {integrity: sha512-RJrCGLBthNwE0XHD2C2GzVpKaSAibaqiJz8k6JesQL4tcCyc/8B1ybJqsYK1w2UwrNBUEQcqXge2IjJmvGBvPw==} peerDependencies: '@types/react': '*' fumadocs-ui: ^15.0.0 || ^16.0.0 @@ -5245,8 +5224,8 @@ packages: '@types/react': optional: true - fumadocs-ui@16.4.4: - resolution: {integrity: sha512-y6zbA1c3Kra6NK1TTmSvDy5NxvnCKs64azsoBeAIxXiNwxdM+hvvF2WVaDKNFJY87pvV+zK7jXoLz2PGQopmmw==} + fumadocs-ui@16.4.2: + resolution: {integrity: sha512-hVZiiJtME1zIJ6r+iQ9codC9pyn1MvU0t0fiXh7ziV2SeXVickkqFBhU+FBng9T7TJAAue+TRiCdQq8UVhgpMg==} peerDependencies: '@types/react': '*' react: ^19.2.0 @@ -6579,8 +6558,8 @@ packages: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} - posthog-js@1.313.0: - resolution: {integrity: sha512-CL8RkC7m9BTZrix86w0fdnSCVqC/gxrfs6c4Wfkz/CldFD7f2912S2KqnWFmwRVDGIwm9IR82YhublQ88gdDKw==} + posthog-js@1.310.2: + resolution: {integrity: sha512-e5fpjv0j3Bd92qzRULZQjLDjeWnoxkAOo2kpsVhoQw7L6zmgC7etuDVZfZXpKfFY9545LYXJGjJwkbtzBHylAw==} posthog-node@5.18.1: resolution: {integrity: sha512-Hi7cRqAlvuEitdiurXJFdMip+BxcwYoX66at5RErMVP91V+Ph9BspGiawC3mJx/4znjwUjF29kAhf8oZQ2uJ5Q==} @@ -7501,38 +7480,38 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - turbo-darwin-64@2.7.3: - resolution: {integrity: sha512-aZHhvRiRHXbJw1EcEAq4aws1hsVVUZ9DPuSFaq9VVFAKCup7niIEwc22glxb7240yYEr1vLafdQ2U294Vcwz+w==} + turbo-darwin-64@2.7.2: + resolution: {integrity: sha512-dxY3X6ezcT5vm3coK6VGixbrhplbQMwgNsCsvZamS/+/6JiebqW9DKt4NwpgYXhDY2HdH00I7FWs3wkVuan4rA==} cpu: [x64] os: [darwin] - turbo-darwin-arm64@2.7.3: - resolution: {integrity: sha512-CkVrHSq+Bnhl9sX2LQgqQYVfLTWC2gvI74C4758OmU0djfrssDKU9d4YQF0AYXXhIIRZipSXfxClQziIMD+EAg==} + turbo-darwin-arm64@2.7.2: + resolution: {integrity: sha512-1bXmuwPLqNFt3mzrtYcVx1sdJ8UYb124Bf48nIgcpMCGZy3kDhgxNv1503kmuK/37OGOZbsWSQFU4I08feIuSg==} cpu: [arm64] os: [darwin] - turbo-linux-64@2.7.3: - resolution: {integrity: sha512-GqDsCNnzzr89kMaLGpRALyigUklzgxIrSy2pHZVXyifgczvYPnLglex78Aj3T2gu+T3trPPH2iJ+pWucVOCC2Q==} + turbo-linux-64@2.7.2: + resolution: {integrity: sha512-kP+TiiMaiPugbRlv57VGLfcjFNsFbo8H64wMBCPV2270Or2TpDCBULMzZrvEsvWFjT3pBFvToYbdp8/Kw0jAQg==} cpu: [x64] os: [linux] - turbo-linux-arm64@2.7.3: - resolution: {integrity: sha512-NdCDTfIcIo3dWjsiaAHlxu5gW61Ed/8maah1IAF/9E3EtX0aAHNiBMbuYLZaR4vRJ7BeVkYB6xKWRtdFLZ0y3g==} + turbo-linux-arm64@2.7.2: + resolution: {integrity: sha512-VDJwQ0+8zjAfbyY6boNaWfP6RIez4ypKHxwkuB6SrWbOSk+vxTyW5/hEjytTwK8w/TsbKVcMDyvpora8tEsRFw==} cpu: [arm64] os: [linux] - turbo-windows-64@2.7.3: - resolution: {integrity: sha512-7bVvO987daXGSJVYBoG8R4Q+csT1pKIgLJYZevXRQ0Hqw0Vv4mKme/TOjYXs9Qb1xMKh51Tb3bXKDbd8/4G08g==} + turbo-windows-64@2.7.2: + resolution: {integrity: sha512-rPjqQXVnI6A6oxgzNEE8DNb6Vdj2Wwyhfv3oDc+YM3U9P7CAcBIlKv/868mKl4vsBtz4ouWpTQNXG8vljgJO+w==} cpu: [x64] os: [win32] - turbo-windows-arm64@2.7.3: - resolution: {integrity: sha512-nTodweTbPmkvwMu/a55XvjMsPtuyUSC+sV7f/SR57K36rB2I0YG21qNETN+00LOTUW9B3omd8XkiXJkt4kx/cw==} + turbo-windows-arm64@2.7.2: + resolution: {integrity: sha512-tcnHvBhO515OheIFWdxA+qUvZzNqqcHbLVFc1+n+TJ1rrp8prYicQtbtmsiKgMvr/54jb9jOabU62URAobnB7g==} cpu: [arm64] os: [win32] - turbo@2.7.3: - resolution: {integrity: sha512-+HjKlP4OfYk+qzvWNETA3cUO5UuK6b5MSc2UJOKyvBceKucQoQGb2g7HlC2H1GHdkfKrk4YF1VPvROkhVZDDLQ==} + turbo@2.7.2: + resolution: {integrity: sha512-5JIA5aYBAJSAhrhbyag1ZuMSgUZnHtI+Sq3H8D3an4fL8PeF+L1yYvbEJg47akP1PFfATMf5ehkqFnxfkmuwZQ==} hasBin: true twoslash-protocol@0.3.6: @@ -7858,8 +7837,8 @@ packages: yaml: optional: true - vite@7.3.1: - resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + vite@7.3.0: + resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -8113,8 +8092,8 @@ packages: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} - zod@4.3.5: - resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==} + zod@4.2.1: + resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==} zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -8785,39 +8764,39 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@biomejs/biome@2.3.11': + '@biomejs/biome@2.3.10': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.3.11 - '@biomejs/cli-darwin-x64': 2.3.11 - '@biomejs/cli-linux-arm64': 2.3.11 - '@biomejs/cli-linux-arm64-musl': 2.3.11 - '@biomejs/cli-linux-x64': 2.3.11 - '@biomejs/cli-linux-x64-musl': 2.3.11 - '@biomejs/cli-win32-arm64': 2.3.11 - '@biomejs/cli-win32-x64': 2.3.11 - - '@biomejs/cli-darwin-arm64@2.3.11': + '@biomejs/cli-darwin-arm64': 2.3.10 + '@biomejs/cli-darwin-x64': 2.3.10 + '@biomejs/cli-linux-arm64': 2.3.10 + '@biomejs/cli-linux-arm64-musl': 2.3.10 + '@biomejs/cli-linux-x64': 2.3.10 + '@biomejs/cli-linux-x64-musl': 2.3.10 + '@biomejs/cli-win32-arm64': 2.3.10 + '@biomejs/cli-win32-x64': 2.3.10 + + '@biomejs/cli-darwin-arm64@2.3.10': optional: true - '@biomejs/cli-darwin-x64@2.3.11': + '@biomejs/cli-darwin-x64@2.3.10': optional: true - '@biomejs/cli-linux-arm64-musl@2.3.11': + '@biomejs/cli-linux-arm64-musl@2.3.10': optional: true - '@biomejs/cli-linux-arm64@2.3.11': + '@biomejs/cli-linux-arm64@2.3.10': optional: true - '@biomejs/cli-linux-x64-musl@2.3.11': + '@biomejs/cli-linux-x64-musl@2.3.10': optional: true - '@biomejs/cli-linux-x64@2.3.11': + '@biomejs/cli-linux-x64@2.3.10': optional: true - '@biomejs/cli-win32-arm64@2.3.11': + '@biomejs/cli-win32-arm64@2.3.10': optional: true - '@biomejs/cli-win32-x64@2.3.11': + '@biomejs/cli-win32-x64@2.3.10': optional: true '@changesets/apply-release-plan@7.0.14': @@ -9200,30 +9179,30 @@ snapshots: '@floating-ui/utils@0.2.10': {} - '@formatjs/fast-memoize@3.0.3': + '@formatjs/fast-memoize@3.0.1': dependencies: tslib: 2.8.1 - '@formatjs/intl-localematcher@0.7.5': + '@formatjs/intl-localematcher@0.7.3': dependencies: - '@formatjs/fast-memoize': 3.0.3 + '@formatjs/fast-memoize': 3.0.1 tslib: 2.8.1 - '@fumadocs/mdx-remote@1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(react@19.2.3)': + '@fumadocs/mdx-remote@1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1))(react@19.2.3)': dependencies: '@mdx-js/mdx': 3.1.1 - fumadocs-core: 16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) + fumadocs-core: 16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1) gray-matter: 4.0.3 react: 19.2.3 - zod: 4.3.5 + zod: 4.2.1 optionalDependencies: '@types/react': 19.2.7 transitivePeerDependencies: - supports-color - '@fumadocs/ui@16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5)': + '@fumadocs/ui@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.2.1)': dependencies: - fumadocs-core: 16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) + fumadocs-core: 16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1) next-themes: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) postcss-selector-parser: 7.1.1 react: 19.2.3 @@ -9236,7 +9215,6 @@ snapshots: transitivePeerDependencies: - '@mixedbread/sdk' - '@orama/core' - - '@oramacloud/client' - '@tanstack/react-router' - algoliasearch - lucide-react @@ -11399,9 +11377,9 @@ snapshots: dependencies: tslib: 2.8.1 - '@solidjs/start@1.2.1(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vinxi@0.5.10(@types/node@24.10.4)(@vercel/blob@0.27.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.57)(terser@5.44.1)(tsx@4.21.0))(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))': + '@solidjs/start@1.2.1(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vinxi@0.5.10(@types/node@24.10.4)(@vercel/blob@0.27.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.57)(terser@5.44.1)(tsx@4.21.0))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))': dependencies: - '@tanstack/server-functions-plugin': 1.121.21(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) + '@tanstack/server-functions-plugin': 1.121.21(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.10(@types/node@24.10.4)(@vercel/blob@0.27.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.57)(terser@5.44.1)(tsx@4.21.0)) '@vinxi/server-components': 0.5.1(vinxi@0.5.10(@types/node@24.10.4)(@vercel/blob@0.27.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.57)(terser@5.44.1)(tsx@4.21.0)) cookie-es: 2.0.0 @@ -11416,7 +11394,7 @@ snapshots: terracotta: 1.0.6(solid-js@1.9.10) tinyglobby: 0.2.15 vinxi: 0.5.10(@types/node@24.10.4)(@vercel/blob@0.27.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.57)(terser@5.44.1)(tsx@4.21.0) - vite-plugin-solid: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) + vite-plugin-solid: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) transitivePeerDependencies: - '@testing-library/jest-dom' - solid-js @@ -11425,8 +11403,6 @@ snapshots: '@speed-highlight/core@1.2.12': {} - '@standard-schema/spec@1.0.0': {} - '@standard-schema/spec@1.1.0': {} '@svta/cml-608@1.0.1': {} @@ -11544,7 +11520,7 @@ snapshots: postcss: 8.5.6 tailwindcss: 4.1.18 - '@tanstack/directive-functions-plugin@1.121.21(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))': + '@tanstack/directive-functions-plugin@1.121.21(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))': dependencies: '@babel/code-frame': 7.26.2 '@babel/core': 7.28.5 @@ -11553,7 +11529,7 @@ snapshots: '@tanstack/router-utils': 1.143.11 babel-dead-code-elimination: 1.0.11 tiny-invariant: 1.3.3 - vite: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) transitivePeerDependencies: - supports-color @@ -11569,7 +11545,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/server-functions-plugin@1.121.21(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))': + '@tanstack/server-functions-plugin@1.121.21(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))': dependencies: '@babel/code-frame': 7.26.2 '@babel/core': 7.28.5 @@ -11578,7 +11554,7 @@ snapshots: '@babel/template': 7.27.2 '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 - '@tanstack/directive-functions-plugin': 1.121.21(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) + '@tanstack/directive-functions-plugin': 1.121.21(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) babel-dead-code-elimination: 1.0.11 tiny-invariant: 1.3.3 transitivePeerDependencies: @@ -11851,7 +11827,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@5.1.2(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))': + '@vitejs/plugin-react@5.1.2(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) @@ -11859,26 +11835,26 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.53 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) transitivePeerDependencies: - supports-color '@vitest/expect@4.0.16': dependencies: - '@standard-schema/spec': 1.0.0 + '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 '@vitest/spy': 4.0.16 '@vitest/utils': 4.0.16 chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.16(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))': + '@vitest/mocker@4.0.16(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))': dependencies: '@vitest/spy': 4.0.16 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) '@vitest/pretty-format@4.0.16': dependencies: @@ -13043,9 +13019,9 @@ snapshots: fsevents@2.3.3: optional: true - fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5): + fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1): dependencies: - '@formatjs/intl-localematcher': 0.7.5 + '@formatjs/intl-localematcher': 0.7.3 '@orama/orama': 3.1.18 '@shikijs/rehype': 3.20.0 '@shikijs/transformers': 3.20.0 @@ -13070,18 +13046,18 @@ snapshots: next: 16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - zod: 4.3.5 + zod: 4.2.1 transitivePeerDependencies: - supports-color - fumadocs-mdx@14.2.4(@fumadocs/mdx-remote@1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(react@19.2.3))(@types/react@19.2.7)(fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)): + fumadocs-mdx@14.2.4(@fumadocs/mdx-remote@1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1))(react@19.2.3))(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)): dependencies: '@mdx-js/mdx': 3.1.1 '@standard-schema/spec': 1.1.0 chokidar: 5.0.0 esbuild: 0.27.2 estree-util-value-to-estree: 3.5.0 - fumadocs-core: 16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) + fumadocs-core: 16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1) js-yaml: 4.1.1 mdast-util-to-markdown: 2.1.2 picocolors: 1.1.1 @@ -13093,21 +13069,21 @@ snapshots: unist-util-remove-position: 5.0.0 unist-util-visit: 5.0.0 vfile: 6.0.3 - zod: 4.3.5 + zod: 4.2.1 optionalDependencies: - '@fumadocs/mdx-remote': 1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(react@19.2.3) + '@fumadocs/mdx-remote': 1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1))(react@19.2.3) '@types/react': 19.2.7 next: 16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 - vite: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) transitivePeerDependencies: - supports-color - fumadocs-twoslash@3.1.12(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(fumadocs-ui@16.4.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3): + fumadocs-twoslash@3.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(fumadocs-ui@16.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.2.1))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3): dependencies: '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@shikijs/twoslash': 3.20.0(typescript@5.9.3) - fumadocs-ui: 16.4.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5) + fumadocs-ui: 16.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.2.1) mdast-util-from-markdown: 2.0.2 mdast-util-gfm: 3.1.0 mdast-util-to-hast: 13.2.1 @@ -13123,9 +13099,9 @@ snapshots: - supports-color - typescript - fumadocs-ui@16.4.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5): + fumadocs-ui@16.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.2.1): dependencies: - '@fumadocs/ui': 16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5) + '@fumadocs/ui': 16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.2.1) '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -13137,7 +13113,7 @@ snapshots: '@radix-ui/react-slot': 1.2.4(@types/react@19.2.7)(react@19.2.3) '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) class-variance-authority: 0.7.1 - fumadocs-core: 16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) + fumadocs-core: 16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1) lucide-react: 0.562.0(react@19.2.3) next-themes: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 @@ -13150,7 +13126,6 @@ snapshots: transitivePeerDependencies: - '@mixedbread/sdk' - '@orama/core' - - '@oramacloud/client' - '@tanstack/react-router' - '@types/react-dom' - algoliasearch @@ -14907,7 +14882,7 @@ snapshots: dependencies: xtend: 4.0.2 - posthog-js@1.313.0: + posthog-js@1.310.2: dependencies: '@posthog/core': 1.9.0 core-js: 3.47.0 @@ -15923,32 +15898,32 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - turbo-darwin-64@2.7.3: + turbo-darwin-64@2.7.2: optional: true - turbo-darwin-arm64@2.7.3: + turbo-darwin-arm64@2.7.2: optional: true - turbo-linux-64@2.7.3: + turbo-linux-64@2.7.2: optional: true - turbo-linux-arm64@2.7.3: + turbo-linux-arm64@2.7.2: optional: true - turbo-windows-64@2.7.3: + turbo-windows-64@2.7.2: optional: true - turbo-windows-arm64@2.7.3: + turbo-windows-arm64@2.7.2: optional: true - turbo@2.7.3: + turbo@2.7.2: optionalDependencies: - turbo-darwin-64: 2.7.3 - turbo-darwin-arm64: 2.7.3 - turbo-linux-64: 2.7.3 - turbo-linux-arm64: 2.7.3 - turbo-windows-64: 2.7.3 - turbo-windows-arm64: 2.7.3 + turbo-darwin-64: 2.7.2 + turbo-darwin-arm64: 2.7.2 + turbo-linux-64: 2.7.2 + turbo-linux-arm64: 2.7.2 + turbo-windows-64: 2.7.2 + turbo-windows-arm64: 2.7.2 twoslash-protocol@0.3.6: {} @@ -16231,7 +16206,7 @@ snapshots: unenv: 1.10.0 unstorage: 1.17.3(@vercel/blob@0.27.3)(db0@0.3.4)(ioredis@5.8.2) vite: 6.4.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) - zod: 4.3.5 + zod: 4.2.1 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -16277,7 +16252,7 @@ snapshots: - xml2js - yaml - vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)): + vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)): dependencies: '@babel/core': 7.28.5 '@types/babel__core': 7.20.5 @@ -16285,8 +16260,8 @@ snapshots: merge-anything: 5.1.7 solid-js: 1.9.10 solid-refresh: 0.6.3(solid-js@1.9.10) - vite: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) - vitefu: 1.1.1(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) + vitefu: 1.1.1(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) optionalDependencies: '@testing-library/jest-dom': 6.9.1 transitivePeerDependencies: @@ -16303,13 +16278,13 @@ snapshots: - supports-color - typescript - vite-tsconfig-paths@6.0.3(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)): + vite-tsconfig-paths@6.0.3(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) transitivePeerDependencies: - supports-color - typescript @@ -16330,7 +16305,7 @@ snapshots: terser: 5.44.1 tsx: 4.21.0 - vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0): + vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) @@ -16346,14 +16321,14 @@ snapshots: terser: 5.44.1 tsx: 4.21.0 - vitefu@1.1.1(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)): + vitefu@1.1.1(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)): optionalDependencies: - vite: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) vitest@4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0): dependencies: '@vitest/expect': 4.0.16 - '@vitest/mocker': 4.0.16(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) + '@vitest/mocker': 4.0.16(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) '@vitest/pretty-format': 4.0.16 '@vitest/runner': 4.0.16 '@vitest/snapshot': 4.0.16 @@ -16370,7 +16345,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 @@ -16579,6 +16554,6 @@ snapshots: compress-commons: 6.0.2 readable-stream: 4.7.0 - zod@4.3.5: {} + zod@4.2.1: {} zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 66fa8ac9a..d2aa2ce6d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -29,7 +29,7 @@ catalog: valibot: 1.2.0 vite-tsconfig-paths: 6.0.3 vitest: 4.0.16 - zod: 4.3.5 + zod: 4.2.1 overrides: '@playwright/test': 1.57.0 From 4ca223d14c741137133c0a20a5324dd72d146fca Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 22:33:40 +0500 Subject: [PATCH 047/117] test: improve test clarity and consistency - Rename `createEnv` to `arkenv` in tests - Explicitly use `type()` for schemas in tests - Add test helpers --- .../src/array-defaults.integration.test.ts | 10 +++--- .../arkenv/src/coercion.integration.test.ts | 14 ++++---- packages/arkenv/src/errors.test.ts | 33 ++++++++++++++++--- .../src/object-parsing.integration.test.ts | 18 +++++----- packages/arkenv/src/type.test.ts | 4 +-- 5 files changed, 50 insertions(+), 29 deletions(-) diff --git a/packages/arkenv/src/array-defaults.integration.test.ts b/packages/arkenv/src/array-defaults.integration.test.ts index ab4f2e827..1a780faae 100644 --- a/packages/arkenv/src/array-defaults.integration.test.ts +++ b/packages/arkenv/src/array-defaults.integration.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from "vitest"; -import createEnv, { type } from "./index"; +import arkenv, { type } from "./index"; -describe("createEnv array defaults", () => { +describe("arkenv array defaults", () => { it("should work with arrow function array defaults", () => { - const Thing = createEnv({ + const Thing = arkenv({ array: type("number.integer[]").default(() => []), }); @@ -11,7 +11,7 @@ describe("createEnv array defaults", () => { }); it("should work with complex array defaults", () => { - const env = createEnv( + const env = arkenv( { ALLOWED_HOSTS: type("string[]").default(() => [ "localhost", @@ -29,7 +29,7 @@ describe("createEnv array defaults", () => { }); it("should support arrays with defaults and environment overrides", () => { - const env = createEnv( + const env = arkenv( { NUMBERS: type("number[]").default(() => [1, 2, 3]), }, diff --git a/packages/arkenv/src/coercion.integration.test.ts b/packages/arkenv/src/coercion.integration.test.ts index 1181a52ea..a2f0690fd 100644 --- a/packages/arkenv/src/coercion.integration.test.ts +++ b/packages/arkenv/src/coercion.integration.test.ts @@ -65,14 +65,12 @@ describe("coercion integration", () => { expect(typeof env.PORT).toBe("number"); }); - it("should coerce number subtypes in mapping", () => { - const env = createEnv( - { - PORT: "number.port", - COUNT: "number.integer", - }, - { env: { PORT: "8080", COUNT: "123" } }, - ); + it("should coerce compiled number subtypes", () => { + const schema = type({ + PORT: "number.port", + COUNT: "number.integer", + }); + const env = createEnv(schema, { env: { PORT: "8080", COUNT: "123" } }); expect(env.PORT).toBe(8080); expect(env.COUNT).toBe(123); }); diff --git a/packages/arkenv/src/errors.test.ts b/packages/arkenv/src/errors.test.ts index 7e5ceea32..fbfab5870 100644 --- a/packages/arkenv/src/errors.test.ts +++ b/packages/arkenv/src/errors.test.ts @@ -9,6 +9,28 @@ type ArkErrorsForTest = { byPath: Record>; }; +/** + * Format the errors returned by ArkType for testing purposes + * @param errors - The errors returned by ArkType + * @returns A string of the formatted errors + */ +const formatErrorsForTest = (errors: ArkErrorsForTest) => { + return formatErrors(errors as ArkErrors); +}; + +/** + * Create an ArkEnvError for testing purposes + * @param errors - The errors returned by ArkType + * @param message - The message to display in the error + * @returns An instance of ArkEnvError + */ +const createArkEnvErrorForTest = ( + errors: ArkErrorsForTest, + message?: string, +) => { + return new ArkEnvError(errors as ArkErrors, message); +}; + describe("formatErrors", () => { it("should format errors with values correctly", () => { const errors: ArkErrorsForTest = { @@ -18,7 +40,7 @@ describe("formatErrors", () => { }, }; - const result = formatErrors(errors); + const result = formatErrorsForTest(errors); expect(result).toContain("PORT"); expect(result).toContain("must be a number"); expect(result).toContain('"abc"'); @@ -33,7 +55,7 @@ describe("formatErrors", () => { DATABASE_URL: { message: "DATABASE_URL is required" }, }, }; - const result = formatErrors(errors); + const result = formatErrorsForTest(errors); expect(result).toContain("DATABASE_URL"); expect(result).toContain("is required"); }); @@ -45,9 +67,10 @@ describe("formatErrors", () => { }, }; - const result = formatErrors(errors); + const result = formatErrorsForTest(errors); expect(result).toContain("ENV_VAR"); expect(result).toContain("must be defined"); + expect(result.match(/ENV_VAR/g)?.length).toBe(1); // Should not duplicate the path }); }); @@ -59,7 +82,7 @@ describe("ArkEnvError", () => { }, }; - const error = new ArkEnvError(errors); + const error = createArkEnvErrorForTest(errors); expect(error.message).toContain( "Errors found while validating environment variables", ); @@ -76,7 +99,7 @@ describe("ArkEnvError", () => { }; const customMessage = "Custom validation error"; - const error = new ArkEnvError(errors, customMessage); + const error = createArkEnvErrorForTest(errors, customMessage); expect(error.message).toContain(customMessage); expect(error.message).toContain("PORT"); expect(error.message).toContain('"abc"'); diff --git a/packages/arkenv/src/object-parsing.integration.test.ts b/packages/arkenv/src/object-parsing.integration.test.ts index 4ebf22a2c..72ed91a70 100644 --- a/packages/arkenv/src/object-parsing.integration.test.ts +++ b/packages/arkenv/src/object-parsing.integration.test.ts @@ -221,23 +221,23 @@ describe("object parsing integration", () => { }); }); - it("should parse an object when using a compiled ArkType schema", () => { + it("should work with type() compiled schemas", () => { const schema = type({ - MY_OBJ: type({ - foo: "string", - bar: "number", - }), + CONFIG: { + host: "string", + port: "number", + }, }); const env = createEnv(schema, { env: { - MY_OBJ: '{"foo": "baz", "bar": 123}', + CONFIG: '{"host": "localhost", "port": "3000"}', }, }); - expect(env.MY_OBJ).toEqual({ - foo: "baz", - bar: 123, + expect(env.CONFIG).toEqual({ + host: "localhost", + port: 3000, }); }); diff --git a/packages/arkenv/src/type.test.ts b/packages/arkenv/src/type.test.ts index 2736e52be..3a16bffc1 100644 --- a/packages/arkenv/src/type.test.ts +++ b/packages/arkenv/src/type.test.ts @@ -40,7 +40,7 @@ describe("type", () => { expect(result2.OPTIONAL).toBeUndefined(); }); - it("should create a type with createEnv-specific host validation", () => { + it("should create a type with arkenv-specific host validation", () => { const envType = type({ HOST: "string.host" }); // Valid IP addresses @@ -58,7 +58,7 @@ describe("type", () => { expect(() => envType.assert({ HOST: "invalid-host" })).toThrow(); }); - it("should create a type with createEnv-specific port validation", () => { + it("should create a type with arkenv-specific port validation", () => { const envType = type({ PORT: "number.port" }); // Valid ports From 1209f264b30bccf17994cd528c378851093990a3 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 22:34:03 +0500 Subject: [PATCH 048/117] style(github-alerts): reformat font family declaration --- apps/www/app/styles/components/github-alerts.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/www/app/styles/components/github-alerts.css b/apps/www/app/styles/components/github-alerts.css index 35b7b17e3..7fa7fe988 100644 --- a/apps/www/app/styles/components/github-alerts.css +++ b/apps/www/app/styles/components/github-alerts.css @@ -7,8 +7,8 @@ --rehype-github-alerts-caution-color: rgb(209, 36, 47); --rehype-github-alerts-default-space: 1rem; --rehype-github-alerts-default-fontFamily: - -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, - sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; + -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, + Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; --rehype-github-alerts-default-fontWeight: 500; } From 048b523e6231bb756984508543f0cb1a136efa69 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 22:54:14 +0500 Subject: [PATCH 049/117] build(deps): update arkenv and zod versions - Update `arkenv` to 0.8.3 in Bun examples - Downgrade `zod` to --- apps/www/app/styles/components/github-alerts.css | 4 ++-- examples/with-bun-react/bun.lock | 6 ++---- examples/with-bun/bun.lock | 4 ++-- examples/with-standard-schema/package-lock.json | 8 ++++---- examples/with-standard-schema/package.json | 2 +- pnpm-lock.yaml | 16 +++++++++++++++- 6 files changed, 26 insertions(+), 14 deletions(-) diff --git a/apps/www/app/styles/components/github-alerts.css b/apps/www/app/styles/components/github-alerts.css index 7fa7fe988..35b7b17e3 100644 --- a/apps/www/app/styles/components/github-alerts.css +++ b/apps/www/app/styles/components/github-alerts.css @@ -7,8 +7,8 @@ --rehype-github-alerts-caution-color: rgb(209, 36, 47); --rehype-github-alerts-default-space: 1rem; --rehype-github-alerts-default-fontFamily: - -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, - Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; + -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, + sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; --rehype-github-alerts-default-fontWeight: 500; } diff --git a/examples/with-bun-react/bun.lock b/examples/with-bun-react/bun.lock index e5e03d7d6..f50ed1c33 100644 --- a/examples/with-bun-react/bun.lock +++ b/examples/with-bun-react/bun.lock @@ -6,7 +6,7 @@ "name": "arkenv-example-with-bun-react", "dependencies": { "@arkenv/bun-plugin": "^0.0.8", - "arkenv": "^0.8.1", + "arkenv": "^0.8.3", "arktype": "^2.1.29", "react": "^19.2.3", "react-dom": "^19.2.3", @@ -60,7 +60,7 @@ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], - "arkenv": ["arkenv@0.8.1", "", { "peerDependencies": { "arktype": "^2.1.22" } }, "sha512-6aGadHN2nuWuRNWcmNc/CfdlUak6jeZi/WBLLWN/LvBFY8aR+FSMJE6x9SUS9L1m+qp94MTVyKpi3tDzo+SoJw=="], + "arkenv": ["arkenv@0.8.3", "", { "peerDependencies": { "arktype": "^2.1.22" } }, "sha512-fndPYpIZ/EvARTXabWG5H+gKxlJEbPgTRvXH8htimmCbdBfEXZsSOgObwdiCCCcBz33tJAYk88goDtj0Ao99NA=="], "arkregex": ["arkregex@0.0.5", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw=="], @@ -93,7 +93,5 @@ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "@arkenv/bun-plugin/arkenv": ["arkenv@0.8.3", "", { "peerDependencies": { "arktype": "^2.1.22" } }, "sha512-fndPYpIZ/EvARTXabWG5H+gKxlJEbPgTRvXH8htimmCbdBfEXZsSOgObwdiCCCcBz33tJAYk88goDtj0Ao99NA=="], } } diff --git a/examples/with-bun/bun.lock b/examples/with-bun/bun.lock index 6d27c85bc..5191b2967 100644 --- a/examples/with-bun/bun.lock +++ b/examples/with-bun/bun.lock @@ -5,7 +5,7 @@ "": { "name": "arkenv-example-with-bun", "dependencies": { - "arkenv": "^0.8.1", + "arkenv": "^0.8.3", "arktype": "^2.1.29", }, "devDependencies": { @@ -28,7 +28,7 @@ "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], - "arkenv": ["arkenv@0.8.1", "", { "peerDependencies": { "arktype": "^2.1.22" } }, "sha512-6aGadHN2nuWuRNWcmNc/CfdlUak6jeZi/WBLLWN/LvBFY8aR+FSMJE6x9SUS9L1m+qp94MTVyKpi3tDzo+SoJw=="], + "arkenv": ["arkenv@0.8.3", "", { "peerDependencies": { "arktype": "^2.1.22" } }, "sha512-fndPYpIZ/EvARTXabWG5H+gKxlJEbPgTRvXH8htimmCbdBfEXZsSOgObwdiCCCcBz33tJAYk88goDtj0Ao99NA=="], "arkregex": ["arkregex@0.0.5", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw=="], diff --git a/examples/with-standard-schema/package-lock.json b/examples/with-standard-schema/package-lock.json index 3aeee64b5..afc4a26e7 100644 --- a/examples/with-standard-schema/package-lock.json +++ b/examples/with-standard-schema/package-lock.json @@ -8,7 +8,7 @@ "dependencies": { "arkenv": "^0.8.3", "arktype": "^2.1.29", - "zod": "^4.3.5" + "zod": "^4.2.1" }, "devDependencies": { "rimraf": "^6.1.2", @@ -739,9 +739,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.2.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", + "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/examples/with-standard-schema/package.json b/examples/with-standard-schema/package.json index 43bdb9b0e..23fc19ee4 100644 --- a/examples/with-standard-schema/package.json +++ b/examples/with-standard-schema/package.json @@ -12,7 +12,7 @@ "dependencies": { "arkenv": "^0.8.3", "arktype": "^2.1.29", - "zod": "^4.3.5" + "zod": "^4.2.1" }, "devDependencies": { "rimraf": "^6.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff177160b..f8b192a73 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -548,6 +548,9 @@ importers: '@size-limit/preset-small-lib': specifier: 'catalog:' version: 12.0.0(size-limit@12.0.0(jiti@2.6.1)) + '@standard-schema/spec': + specifier: 1.0.0 + version: 1.0.0 '@types/node': specifier: 'catalog:' version: 24.10.4 @@ -569,6 +572,9 @@ importers: vitest: specifier: 'catalog:' version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) + zod: + specifier: 'catalog:' + version: 4.2.1 packages/bun-plugin: dependencies: @@ -643,6 +649,9 @@ importers: '@repo/scope': specifier: workspace:* version: link:../scope + '@standard-schema/spec': + specifier: 1.0.0 + version: 1.0.0 arktype: specifier: 'catalog:' version: 2.1.29 @@ -3599,6 +3608,9 @@ packages: '@speed-highlight/core@1.2.12': resolution: {integrity: sha512-uilwrK0Ygyri5dToHYdZSjcvpS2ZwX0w5aSt3GCEN9hrjxWCoeV4Z2DTXuxjwbntaLQIEEAlCeNQss5SoHvAEA==} + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -11403,6 +11415,8 @@ snapshots: '@speed-highlight/core@1.2.12': {} + '@standard-schema/spec@1.0.0': {} + '@standard-schema/spec@1.1.0': {} '@svta/cml-608@1.0.1': {} @@ -11841,7 +11855,7 @@ snapshots: '@vitest/expect@4.0.16': dependencies: - '@standard-schema/spec': 1.1.0 + '@standard-schema/spec': 1.0.0 '@types/chai': 5.2.3 '@vitest/spy': 4.0.16 '@vitest/utils': 4.0.16 From 3835cee6ed5534cfe3cfdc802bda738c94b42a31 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 23:06:37 +0500 Subject: [PATCH 050/117] build(deps): update dependencies - Update `zod` to `4.3.5` - Upgrade Biome to `2.3.11` - Update Turbo to ` --- apps/playgrounds/node/package.json | 2 +- .../app/styles/components/github-alerts.css | 4 +- examples/basic/package-lock.json | 8 +- examples/basic/package.json | 2 +- .../with-standard-schema/package-lock.json | 8 +- examples/with-standard-schema/package.json | 2 +- package.json | 4 +- pnpm-lock.yaml | 296 +++++++++++------- pnpm-workspace.yaml | 2 +- 9 files changed, 192 insertions(+), 136 deletions(-) diff --git a/apps/playgrounds/node/package.json b/apps/playgrounds/node/package.json index 2d9b38a44..5655c888a 100644 --- a/apps/playgrounds/node/package.json +++ b/apps/playgrounds/node/package.json @@ -12,7 +12,7 @@ "dependencies": { "arkenv": "workspace:*", "arktype": "catalog:", - "zod": "4.2.1" + "zod": "4.3.5" }, "devDependencies": { "@types/node": "catalog:", diff --git a/apps/www/app/styles/components/github-alerts.css b/apps/www/app/styles/components/github-alerts.css index 35b7b17e3..7fa7fe988 100644 --- a/apps/www/app/styles/components/github-alerts.css +++ b/apps/www/app/styles/components/github-alerts.css @@ -7,8 +7,8 @@ --rehype-github-alerts-caution-color: rgb(209, 36, 47); --rehype-github-alerts-default-space: 1rem; --rehype-github-alerts-default-fontFamily: - -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, - sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; + -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, + Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; --rehype-github-alerts-default-fontWeight: 500; } diff --git a/examples/basic/package-lock.json b/examples/basic/package-lock.json index 0140bb141..2ae4d4502 100644 --- a/examples/basic/package-lock.json +++ b/examples/basic/package-lock.json @@ -8,7 +8,7 @@ "dependencies": { "arkenv": "^0.8.3", "arktype": "^2.1.29", - "zod": "4.2.1" + "zod": "4.3.5" }, "devDependencies": { "@types/node": "^24.10.4", @@ -760,9 +760,9 @@ "license": "MIT" }, "node_modules/zod": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", - "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/examples/basic/package.json b/examples/basic/package.json index c1f5bf148..3fb4c7503 100644 --- a/examples/basic/package.json +++ b/examples/basic/package.json @@ -11,7 +11,7 @@ "dependencies": { "arkenv": "^0.8.3", "arktype": "^2.1.29", - "zod": "4.2.1" + "zod": "4.3.5" }, "devDependencies": { "@types/node": "^24.10.4", diff --git a/examples/with-standard-schema/package-lock.json b/examples/with-standard-schema/package-lock.json index afc4a26e7..3aeee64b5 100644 --- a/examples/with-standard-schema/package-lock.json +++ b/examples/with-standard-schema/package-lock.json @@ -8,7 +8,7 @@ "dependencies": { "arkenv": "^0.8.3", "arktype": "^2.1.29", - "zod": "^4.2.1" + "zod": "^4.3.5" }, "devDependencies": { "rimraf": "^6.1.2", @@ -739,9 +739,9 @@ } }, "node_modules/zod": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", - "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/examples/with-standard-schema/package.json b/examples/with-standard-schema/package.json index 23fc19ee4..43bdb9b0e 100644 --- a/examples/with-standard-schema/package.json +++ b/examples/with-standard-schema/package.json @@ -12,7 +12,7 @@ "dependencies": { "arkenv": "^0.8.3", "arktype": "^2.1.29", - "zod": "^4.2.1" + "zod": "^4.3.5" }, "devDependencies": { "rimraf": "^6.1.2", diff --git a/package.json b/package.json index b165471c8..9d1b72703 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "contributors:generate": "all-contributors generate" }, "devDependencies": { - "@biomejs/biome": "2.3.10", + "@biomejs/biome": "2.3.11", "@changesets/cli": "2.29.8", "@manypkg/cli": "0.25.1", "@playwright/test": "", @@ -38,7 +38,7 @@ "all-contributors-cli": "^6.26.1", "changesets-changelog-clean": "1.3.0", "rimraf": "catalog:", - "turbo": "2.7.2", + "turbo": "2.7.3", "typescript": "catalog:", "vitest": "catalog:" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f8b192a73..ea78d3582 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -76,8 +76,8 @@ catalogs: specifier: 4.0.16 version: 4.0.16 zod: - specifier: 4.2.1 - version: 4.2.1 + specifier: 4.3.5 + version: 4.3.5 overrides: '@playwright/test': 1.57.0 @@ -89,8 +89,8 @@ importers: .: devDependencies: '@biomejs/biome': - specifier: 2.3.10 - version: 2.3.10 + specifier: 2.3.11 + version: 2.3.11 '@changesets/cli': specifier: 2.29.8 version: 2.29.8(@types/node@24.10.4) @@ -113,8 +113,8 @@ importers: specifier: 'catalog:' version: 6.1.2 turbo: - specifier: 2.7.2 - version: 2.7.2 + specifier: 2.7.3 + version: 2.7.3 typescript: specifier: 'catalog:' version: 5.9.3 @@ -190,8 +190,8 @@ importers: specifier: 'catalog:' version: 2.1.29 zod: - specifier: 4.2.1 - version: 4.2.1 + specifier: 4.3.5 + version: 4.3.5 devDependencies: '@types/node': specifier: 'catalog:' @@ -213,7 +213,7 @@ importers: version: link:../../../packages/vite-plugin '@solidjs/start': specifier: 1.2.1 - version: 1.2.1(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vinxi@0.5.10(@types/node@24.10.4)(@vercel/blob@0.27.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.57)(terser@5.44.1)(tsx@4.21.0))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) + version: 1.2.1(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vinxi@0.5.10(@types/node@24.10.4)(@vercel/blob@0.27.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.57)(terser@5.44.1)(tsx@4.21.0))(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) arkenv: specifier: workspace:* version: link:../../../packages/arkenv @@ -237,7 +237,7 @@ importers: version: 2.1.29 zod: specifier: 'catalog:' - version: 4.2.1 + version: 4.3.5 devDependencies: rimraf: specifier: 'catalog:' @@ -348,7 +348,7 @@ importers: version: link:../../packages/vite-plugin '@fumadocs/mdx-remote': specifier: 1.4.4 - version: 1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1))(react@19.2.3) + version: 1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(react@19.2.3) '@icons-pack/react-simple-icons': specifier: 13.8.0 version: 13.8.0(react@19.2.3) @@ -390,16 +390,16 @@ importers: version: 2.1.1 fumadocs-core: specifier: 16.4.2 - version: 16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1) + version: 16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) fumadocs-mdx: specifier: 14.2.4 - version: 14.2.4(@fumadocs/mdx-remote@1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1))(react@19.2.3))(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) + version: 14.2.4(@fumadocs/mdx-remote@1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(react@19.2.3))(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) fumadocs-twoslash: specifier: 3.1.11 - version: 3.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(fumadocs-ui@16.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.2.1))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + version: 3.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(fumadocs-ui@16.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) fumadocs-ui: specifier: 16.4.2 - version: 16.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.2.1) + version: 16.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5) import-in-the-middle: specifier: 2.0.0 version: 2.0.0 @@ -459,7 +459,7 @@ importers: version: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) zod: specifier: 'catalog:' - version: 4.2.1 + version: 4.3.5 devDependencies: '@tailwindcss/postcss': specifier: 4.1.18 @@ -574,7 +574,7 @@ importers: version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) zod: specifier: 'catalog:' - version: 4.2.1 + version: 4.3.5 packages/bun-plugin: dependencies: @@ -1026,55 +1026,55 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} - '@biomejs/biome@2.3.10': - resolution: {integrity: sha512-/uWSUd1MHX2fjqNLHNL6zLYWBbrJeG412/8H7ESuK8ewoRoMPUgHDebqKrPTx/5n6f17Xzqc9hdg3MEqA5hXnQ==} + '@biomejs/biome@2.3.11': + resolution: {integrity: sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.3.10': - resolution: {integrity: sha512-M6xUjtCVnNGFfK7HMNKa593nb7fwNm43fq1Mt71kpLpb+4mE7odO8W/oWVDyBVO4ackhresy1ZYO7OJcVo/B7w==} + '@biomejs/cli-darwin-arm64@2.3.11': + resolution: {integrity: sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.3.10': - resolution: {integrity: sha512-Vae7+V6t/Avr8tVbFNjnFSTKZogZHFYl7MMH62P/J1kZtr0tyRQ9Fe0onjqjS2Ek9lmNLmZc/VR5uSekh+p1fg==} + '@biomejs/cli-darwin-x64@2.3.11': + resolution: {integrity: sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.3.10': - resolution: {integrity: sha512-B9DszIHkuKtOH2IFeeVkQmSMVUjss9KtHaNXquYYWCjH8IstNgXgx5B0aSBQNr6mn4RcKKRQZXn9Zu1rM3O0/A==} + '@biomejs/cli-linux-arm64-musl@2.3.11': + resolution: {integrity: sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-arm64@2.3.10': - resolution: {integrity: sha512-hhPw2V3/EpHKsileVOFynuWiKRgFEV48cLe0eA+G2wO4SzlwEhLEB9LhlSrVeu2mtSn205W283LkX7Fh48CaxA==} + '@biomejs/cli-linux-arm64@2.3.11': + resolution: {integrity: sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-x64-musl@2.3.10': - resolution: {integrity: sha512-QTfHZQh62SDFdYc2nfmZFuTm5yYb4eO1zwfB+90YxUumRCR171tS1GoTX5OD0wrv4UsziMPmrePMtkTnNyYG3g==} + '@biomejs/cli-linux-x64-musl@2.3.11': + resolution: {integrity: sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-linux-x64@2.3.10': - resolution: {integrity: sha512-wwAkWD1MR95u+J4LkWP74/vGz+tRrIQvr8kfMMJY8KOQ8+HMVleREOcPYsQX82S7uueco60L58Wc6M1I9WA9Dw==} + '@biomejs/cli-linux-x64@2.3.11': + resolution: {integrity: sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-win32-arm64@2.3.10': - resolution: {integrity: sha512-o7lYc9n+CfRbHvkjPhm8s9FgbKdYZu5HCcGVMItLjz93EhgJ8AM44W+QckDqLA9MKDNFrR8nPbO4b73VC5kGGQ==} + '@biomejs/cli-win32-arm64@2.3.11': + resolution: {integrity: sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.3.10': - resolution: {integrity: sha512-pHEFgq7dUEsKnqG9mx9bXihxGI49X+ar+UBrEIj3Wqj3UCZp1rNgV+OoyjFgcXsjCWpuEAF4VJdkZr3TrWdCbQ==} + '@biomejs/cli-win32-x64@2.3.11': + resolution: {integrity: sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -1532,11 +1532,11 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} - '@formatjs/fast-memoize@3.0.1': - resolution: {integrity: sha512-kzk635kEmsxrrEWQXY7uKRocFCVXR4es5OQqcqCGg2NPtQztG/OBkE9THHu6UOTxpfyIkZhh6DjPBZGRp7y3og==} + '@formatjs/fast-memoize@3.0.3': + resolution: {integrity: sha512-CArYtQKGLAOruCMeq5/RxCg6vUXFx3OuKBdTm30Wn/+gCefehmZ8Y2xSMxMrO2iel7hRyE3HKfV56t3vAU6D4Q==} - '@formatjs/intl-localematcher@0.7.3': - resolution: {integrity: sha512-NaeABectKdTCOnlH9VFGmMS3K0JuR7Soc2t5R2MCkBrM3H/hlKVYh0XSrcjjPkbjIdrF7L/Bzx9JtGuVaSfYlA==} + '@formatjs/intl-localematcher@0.7.5': + resolution: {integrity: sha512-7/nd90cn5CT7SVF71/ybUKAcnvBlr9nZlJJp8O8xIZHXFgYOC4SXExZlSdgHv2l6utjw1byidL06QzChvQMHwA==} '@fumadocs/mdx-remote@1.4.4': resolution: {integrity: sha512-tODOw9cYWJkzJ6I+CG16U0QjRmwEVNUavlSn6jUyn4CJ7OjpnFU+jgOiqPm8STFghNYcYVjt3SlrRledGQKR4Q==} @@ -7492,38 +7492,38 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - turbo-darwin-64@2.7.2: - resolution: {integrity: sha512-dxY3X6ezcT5vm3coK6VGixbrhplbQMwgNsCsvZamS/+/6JiebqW9DKt4NwpgYXhDY2HdH00I7FWs3wkVuan4rA==} + turbo-darwin-64@2.7.3: + resolution: {integrity: sha512-aZHhvRiRHXbJw1EcEAq4aws1hsVVUZ9DPuSFaq9VVFAKCup7niIEwc22glxb7240yYEr1vLafdQ2U294Vcwz+w==} cpu: [x64] os: [darwin] - turbo-darwin-arm64@2.7.2: - resolution: {integrity: sha512-1bXmuwPLqNFt3mzrtYcVx1sdJ8UYb124Bf48nIgcpMCGZy3kDhgxNv1503kmuK/37OGOZbsWSQFU4I08feIuSg==} + turbo-darwin-arm64@2.7.3: + resolution: {integrity: sha512-CkVrHSq+Bnhl9sX2LQgqQYVfLTWC2gvI74C4758OmU0djfrssDKU9d4YQF0AYXXhIIRZipSXfxClQziIMD+EAg==} cpu: [arm64] os: [darwin] - turbo-linux-64@2.7.2: - resolution: {integrity: sha512-kP+TiiMaiPugbRlv57VGLfcjFNsFbo8H64wMBCPV2270Or2TpDCBULMzZrvEsvWFjT3pBFvToYbdp8/Kw0jAQg==} + turbo-linux-64@2.7.3: + resolution: {integrity: sha512-GqDsCNnzzr89kMaLGpRALyigUklzgxIrSy2pHZVXyifgczvYPnLglex78Aj3T2gu+T3trPPH2iJ+pWucVOCC2Q==} cpu: [x64] os: [linux] - turbo-linux-arm64@2.7.2: - resolution: {integrity: sha512-VDJwQ0+8zjAfbyY6boNaWfP6RIez4ypKHxwkuB6SrWbOSk+vxTyW5/hEjytTwK8w/TsbKVcMDyvpora8tEsRFw==} + turbo-linux-arm64@2.7.3: + resolution: {integrity: sha512-NdCDTfIcIo3dWjsiaAHlxu5gW61Ed/8maah1IAF/9E3EtX0aAHNiBMbuYLZaR4vRJ7BeVkYB6xKWRtdFLZ0y3g==} cpu: [arm64] os: [linux] - turbo-windows-64@2.7.2: - resolution: {integrity: sha512-rPjqQXVnI6A6oxgzNEE8DNb6Vdj2Wwyhfv3oDc+YM3U9P7CAcBIlKv/868mKl4vsBtz4ouWpTQNXG8vljgJO+w==} + turbo-windows-64@2.7.3: + resolution: {integrity: sha512-7bVvO987daXGSJVYBoG8R4Q+csT1pKIgLJYZevXRQ0Hqw0Vv4mKme/TOjYXs9Qb1xMKh51Tb3bXKDbd8/4G08g==} cpu: [x64] os: [win32] - turbo-windows-arm64@2.7.2: - resolution: {integrity: sha512-tcnHvBhO515OheIFWdxA+qUvZzNqqcHbLVFc1+n+TJ1rrp8prYicQtbtmsiKgMvr/54jb9jOabU62URAobnB7g==} + turbo-windows-arm64@2.7.3: + resolution: {integrity: sha512-nTodweTbPmkvwMu/a55XvjMsPtuyUSC+sV7f/SR57K36rB2I0YG21qNETN+00LOTUW9B3omd8XkiXJkt4kx/cw==} cpu: [arm64] os: [win32] - turbo@2.7.2: - resolution: {integrity: sha512-5JIA5aYBAJSAhrhbyag1ZuMSgUZnHtI+Sq3H8D3an4fL8PeF+L1yYvbEJg47akP1PFfATMf5ehkqFnxfkmuwZQ==} + turbo@2.7.3: + resolution: {integrity: sha512-+HjKlP4OfYk+qzvWNETA3cUO5UuK6b5MSc2UJOKyvBceKucQoQGb2g7HlC2H1GHdkfKrk4YF1VPvROkhVZDDLQ==} hasBin: true twoslash-protocol@0.3.6: @@ -7889,6 +7889,46 @@ packages: yaml: optional: true + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vitefu@1.1.1: resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==} peerDependencies: @@ -8104,8 +8144,8 @@ packages: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} - zod@4.2.1: - resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==} + zod@4.3.5: + resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==} zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -8776,39 +8816,39 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@biomejs/biome@2.3.10': + '@biomejs/biome@2.3.11': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.3.10 - '@biomejs/cli-darwin-x64': 2.3.10 - '@biomejs/cli-linux-arm64': 2.3.10 - '@biomejs/cli-linux-arm64-musl': 2.3.10 - '@biomejs/cli-linux-x64': 2.3.10 - '@biomejs/cli-linux-x64-musl': 2.3.10 - '@biomejs/cli-win32-arm64': 2.3.10 - '@biomejs/cli-win32-x64': 2.3.10 - - '@biomejs/cli-darwin-arm64@2.3.10': + '@biomejs/cli-darwin-arm64': 2.3.11 + '@biomejs/cli-darwin-x64': 2.3.11 + '@biomejs/cli-linux-arm64': 2.3.11 + '@biomejs/cli-linux-arm64-musl': 2.3.11 + '@biomejs/cli-linux-x64': 2.3.11 + '@biomejs/cli-linux-x64-musl': 2.3.11 + '@biomejs/cli-win32-arm64': 2.3.11 + '@biomejs/cli-win32-x64': 2.3.11 + + '@biomejs/cli-darwin-arm64@2.3.11': optional: true - '@biomejs/cli-darwin-x64@2.3.10': + '@biomejs/cli-darwin-x64@2.3.11': optional: true - '@biomejs/cli-linux-arm64-musl@2.3.10': + '@biomejs/cli-linux-arm64-musl@2.3.11': optional: true - '@biomejs/cli-linux-arm64@2.3.10': + '@biomejs/cli-linux-arm64@2.3.11': optional: true - '@biomejs/cli-linux-x64-musl@2.3.10': + '@biomejs/cli-linux-x64-musl@2.3.11': optional: true - '@biomejs/cli-linux-x64@2.3.10': + '@biomejs/cli-linux-x64@2.3.11': optional: true - '@biomejs/cli-win32-arm64@2.3.10': + '@biomejs/cli-win32-arm64@2.3.11': optional: true - '@biomejs/cli-win32-x64@2.3.10': + '@biomejs/cli-win32-x64@2.3.11': optional: true '@changesets/apply-release-plan@7.0.14': @@ -9191,30 +9231,30 @@ snapshots: '@floating-ui/utils@0.2.10': {} - '@formatjs/fast-memoize@3.0.1': + '@formatjs/fast-memoize@3.0.3': dependencies: tslib: 2.8.1 - '@formatjs/intl-localematcher@0.7.3': + '@formatjs/intl-localematcher@0.7.5': dependencies: - '@formatjs/fast-memoize': 3.0.1 + '@formatjs/fast-memoize': 3.0.3 tslib: 2.8.1 - '@fumadocs/mdx-remote@1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1))(react@19.2.3)': + '@fumadocs/mdx-remote@1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(react@19.2.3)': dependencies: '@mdx-js/mdx': 3.1.1 - fumadocs-core: 16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1) + fumadocs-core: 16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) gray-matter: 4.0.3 react: 19.2.3 - zod: 4.2.1 + zod: 4.3.5 optionalDependencies: '@types/react': 19.2.7 transitivePeerDependencies: - supports-color - '@fumadocs/ui@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.2.1)': + '@fumadocs/ui@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5)': dependencies: - fumadocs-core: 16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1) + fumadocs-core: 16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) next-themes: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) postcss-selector-parser: 7.1.1 react: 19.2.3 @@ -11389,9 +11429,9 @@ snapshots: dependencies: tslib: 2.8.1 - '@solidjs/start@1.2.1(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vinxi@0.5.10(@types/node@24.10.4)(@vercel/blob@0.27.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.57)(terser@5.44.1)(tsx@4.21.0))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))': + '@solidjs/start@1.2.1(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vinxi@0.5.10(@types/node@24.10.4)(@vercel/blob@0.27.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.57)(terser@5.44.1)(tsx@4.21.0))(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))': dependencies: - '@tanstack/server-functions-plugin': 1.121.21(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) + '@tanstack/server-functions-plugin': 1.121.21(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.10(@types/node@24.10.4)(@vercel/blob@0.27.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.57)(terser@5.44.1)(tsx@4.21.0)) '@vinxi/server-components': 0.5.1(vinxi@0.5.10(@types/node@24.10.4)(@vercel/blob@0.27.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.57)(terser@5.44.1)(tsx@4.21.0)) cookie-es: 2.0.0 @@ -11406,7 +11446,7 @@ snapshots: terracotta: 1.0.6(solid-js@1.9.10) tinyglobby: 0.2.15 vinxi: 0.5.10(@types/node@24.10.4)(@vercel/blob@0.27.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.57)(terser@5.44.1)(tsx@4.21.0) - vite-plugin-solid: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) + vite-plugin-solid: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) transitivePeerDependencies: - '@testing-library/jest-dom' - solid-js @@ -11534,7 +11574,7 @@ snapshots: postcss: 8.5.6 tailwindcss: 4.1.18 - '@tanstack/directive-functions-plugin@1.121.21(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))': + '@tanstack/directive-functions-plugin@1.121.21(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))': dependencies: '@babel/code-frame': 7.26.2 '@babel/core': 7.28.5 @@ -11543,7 +11583,7 @@ snapshots: '@tanstack/router-utils': 1.143.11 babel-dead-code-elimination: 1.0.11 tiny-invariant: 1.3.3 - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) + vite: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) transitivePeerDependencies: - supports-color @@ -11559,7 +11599,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/server-functions-plugin@1.121.21(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))': + '@tanstack/server-functions-plugin@1.121.21(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))': dependencies: '@babel/code-frame': 7.26.2 '@babel/core': 7.28.5 @@ -11568,7 +11608,7 @@ snapshots: '@babel/template': 7.27.2 '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 - '@tanstack/directive-functions-plugin': 1.121.21(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) + '@tanstack/directive-functions-plugin': 1.121.21(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) babel-dead-code-elimination: 1.0.11 tiny-invariant: 1.3.3 transitivePeerDependencies: @@ -13033,9 +13073,9 @@ snapshots: fsevents@2.3.3: optional: true - fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1): + fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5): dependencies: - '@formatjs/intl-localematcher': 0.7.3 + '@formatjs/intl-localematcher': 0.7.5 '@orama/orama': 3.1.18 '@shikijs/rehype': 3.20.0 '@shikijs/transformers': 3.20.0 @@ -13060,18 +13100,18 @@ snapshots: next: 16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - zod: 4.2.1 + zod: 4.3.5 transitivePeerDependencies: - supports-color - fumadocs-mdx@14.2.4(@fumadocs/mdx-remote@1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1))(react@19.2.3))(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)): + fumadocs-mdx@14.2.4(@fumadocs/mdx-remote@1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(react@19.2.3))(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)): dependencies: '@mdx-js/mdx': 3.1.1 '@standard-schema/spec': 1.1.0 chokidar: 5.0.0 esbuild: 0.27.2 estree-util-value-to-estree: 3.5.0 - fumadocs-core: 16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1) + fumadocs-core: 16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) js-yaml: 4.1.1 mdast-util-to-markdown: 2.1.2 picocolors: 1.1.1 @@ -13083,9 +13123,9 @@ snapshots: unist-util-remove-position: 5.0.0 unist-util-visit: 5.0.0 vfile: 6.0.3 - zod: 4.2.1 + zod: 4.3.5 optionalDependencies: - '@fumadocs/mdx-remote': 1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1))(react@19.2.3) + '@fumadocs/mdx-remote': 1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(react@19.2.3) '@types/react': 19.2.7 next: 16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 @@ -13093,11 +13133,11 @@ snapshots: transitivePeerDependencies: - supports-color - fumadocs-twoslash@3.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(fumadocs-ui@16.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.2.1))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3): + fumadocs-twoslash@3.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(fumadocs-ui@16.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3): dependencies: '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@shikijs/twoslash': 3.20.0(typescript@5.9.3) - fumadocs-ui: 16.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.2.1) + fumadocs-ui: 16.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5) mdast-util-from-markdown: 2.0.2 mdast-util-gfm: 3.1.0 mdast-util-to-hast: 13.2.1 @@ -13113,9 +13153,9 @@ snapshots: - supports-color - typescript - fumadocs-ui@16.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.2.1): + fumadocs-ui@16.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5): dependencies: - '@fumadocs/ui': 16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.2.1) + '@fumadocs/ui': 16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5) '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -13127,7 +13167,7 @@ snapshots: '@radix-ui/react-slot': 1.2.4(@types/react@19.2.7)(react@19.2.3) '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) class-variance-authority: 0.7.1 - fumadocs-core: 16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1) + fumadocs-core: 16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) lucide-react: 0.562.0(react@19.2.3) next-themes: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 @@ -15912,32 +15952,32 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - turbo-darwin-64@2.7.2: + turbo-darwin-64@2.7.3: optional: true - turbo-darwin-arm64@2.7.2: + turbo-darwin-arm64@2.7.3: optional: true - turbo-linux-64@2.7.2: + turbo-linux-64@2.7.3: optional: true - turbo-linux-arm64@2.7.2: + turbo-linux-arm64@2.7.3: optional: true - turbo-windows-64@2.7.2: + turbo-windows-64@2.7.3: optional: true - turbo-windows-arm64@2.7.2: + turbo-windows-arm64@2.7.3: optional: true - turbo@2.7.2: + turbo@2.7.3: optionalDependencies: - turbo-darwin-64: 2.7.2 - turbo-darwin-arm64: 2.7.2 - turbo-linux-64: 2.7.2 - turbo-linux-arm64: 2.7.2 - turbo-windows-64: 2.7.2 - turbo-windows-arm64: 2.7.2 + turbo-darwin-64: 2.7.3 + turbo-darwin-arm64: 2.7.3 + turbo-linux-64: 2.7.3 + turbo-linux-arm64: 2.7.3 + turbo-windows-64: 2.7.3 + turbo-windows-arm64: 2.7.3 twoslash-protocol@0.3.6: {} @@ -16220,7 +16260,7 @@ snapshots: unenv: 1.10.0 unstorage: 1.17.3(@vercel/blob@0.27.3)(db0@0.3.4)(ioredis@5.8.2) vite: 6.4.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) - zod: 4.2.1 + zod: 4.3.5 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -16266,7 +16306,7 @@ snapshots: - xml2js - yaml - vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)): + vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)): dependencies: '@babel/core': 7.28.5 '@types/babel__core': 7.20.5 @@ -16274,8 +16314,8 @@ snapshots: merge-anything: 5.1.7 solid-js: 1.9.10 solid-refresh: 0.6.3(solid-js@1.9.10) - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) - vitefu: 1.1.1(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) + vite: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) + vitefu: 1.1.1(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) optionalDependencies: '@testing-library/jest-dom': 6.9.1 transitivePeerDependencies: @@ -16335,9 +16375,25 @@ snapshots: terser: 5.44.1 tsx: 4.21.0 - vitefu@1.1.1(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)): + vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0): + dependencies: + esbuild: 0.27.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.54.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.4 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + terser: 5.44.1 + tsx: 4.21.0 + + vitefu@1.1.1(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)): optionalDependencies: - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) + vite: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) vitest@4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0): dependencies: @@ -16568,6 +16624,6 @@ snapshots: compress-commons: 6.0.2 readable-stream: 4.7.0 - zod@4.2.1: {} + zod@4.3.5: {} zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d2aa2ce6d..66fa8ac9a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -29,7 +29,7 @@ catalog: valibot: 1.2.0 vite-tsconfig-paths: 6.0.3 vitest: 4.0.16 - zod: 4.2.1 + zod: 4.3.5 overrides: '@playwright/test': 1.57.0 From 19fb4ceffac79f29d1982f640042efa1ef142e7f Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 23:13:32 +0500 Subject: [PATCH 051/117] build: update vite to 7.3.1 --- apps/playgrounds/vite-legacy/package.json | 2 +- apps/www/package.json | 10 ++--- pnpm-lock.yaml | 45 +++++++++++++++++------ 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/apps/playgrounds/vite-legacy/package.json b/apps/playgrounds/vite-legacy/package.json index f3481e3a1..465a30e60 100644 --- a/apps/playgrounds/vite-legacy/package.json +++ b/apps/playgrounds/vite-legacy/package.json @@ -22,7 +22,7 @@ "@vitejs/plugin-react": "5.1.2", "globals": "16.5.0", "typescript": "5.9.3", - "vite": "7.3.0", + "vite": "7.3.1", "vite-tsconfig-paths": "catalog:" } } diff --git a/apps/www/package.json b/apps/www/package.json index b362dbfec..09b6fdde5 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -29,15 +29,15 @@ "arktype": "2.1.29", "class-variance-authority": "0.7.1", "clsx": "2.1.1", - "fumadocs-core": "16.4.2", + "fumadocs-core": "16.4.4", "fumadocs-mdx": "14.2.4", - "fumadocs-twoslash": "3.1.11", - "fumadocs-ui": "16.4.2", + "fumadocs-twoslash": "3.1.12", + "fumadocs-ui": "16.4.4", "import-in-the-middle": "2.0.1", "lucide-react": "0.562.0", "next": "16.1.1", "next-themes": "0.4.6", - "posthog-js": "1.310.2", + "posthog-js": "1.313.0", "posthog-node": "5.18.1", "react": "19.2.3", "react-dom": "19.2.3", @@ -51,7 +51,7 @@ "twoslash": "0.3.6", "unist-util-visit": "5.0.0", "valibot": "catalog:", - "vite": "7.3.0", + "vite": "7.3.1", "zod": "catalog:" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea78d3582..7762cd10e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -393,7 +393,7 @@ importers: version: 16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) fumadocs-mdx: specifier: 14.2.4 - version: 14.2.4(@fumadocs/mdx-remote@1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(react@19.2.3))(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) + version: 14.2.4(@fumadocs/mdx-remote@1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(react@19.2.3))(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) fumadocs-twoslash: specifier: 3.1.11 version: 3.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(fumadocs-ui@16.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) @@ -455,8 +455,8 @@ importers: specifier: 'catalog:' version: 1.2.0(typescript@5.9.3) vite: - specifier: 7.3.0 - version: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) + specifier: 7.3.1 + version: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) zod: specifier: 'catalog:' version: 4.3.5 @@ -496,7 +496,7 @@ importers: version: 19.2.3(@types/react@19.2.7) '@vitejs/plugin-react': specifier: 'catalog:' - version: 5.1.2(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) + version: 5.1.2(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) concurrently: specifier: 'catalog:' version: 9.2.1 @@ -526,7 +526,7 @@ importers: version: 5.9.3 vite-tsconfig-paths: specifier: 'catalog:' - version: 6.0.3(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) + version: 6.0.3(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) vitest: specifier: 'catalog:' version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) @@ -11893,6 +11893,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitejs/plugin-react@5.1.2(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))': + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) + '@rolldown/pluginutils': 1.0.0-beta.53 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) + transitivePeerDependencies: + - supports-color + '@vitest/expect@4.0.16': dependencies: '@standard-schema/spec': 1.0.0 @@ -11902,13 +11914,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.16(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))': + '@vitest/mocker@4.0.16(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))': dependencies: '@vitest/spy': 4.0.16 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) + vite: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) '@vitest/pretty-format@4.0.16': dependencies: @@ -13104,7 +13116,7 @@ snapshots: transitivePeerDependencies: - supports-color - fumadocs-mdx@14.2.4(@fumadocs/mdx-remote@1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(react@19.2.3))(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)): + fumadocs-mdx@14.2.4(@fumadocs/mdx-remote@1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(react@19.2.3))(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)): dependencies: '@mdx-js/mdx': 3.1.1 '@standard-schema/spec': 1.1.0 @@ -13129,7 +13141,7 @@ snapshots: '@types/react': 19.2.7 next: 16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) + vite: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) transitivePeerDependencies: - supports-color @@ -16343,6 +16355,17 @@ snapshots: - supports-color - typescript + vite-tsconfig-paths@6.0.3(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)): + dependencies: + debug: 4.4.3 + globrex: 0.1.2 + tsconfck: 3.1.6(typescript@5.9.3) + optionalDependencies: + vite: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) + transitivePeerDependencies: + - supports-color + - typescript + vite@6.4.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0): dependencies: esbuild: 0.25.12 @@ -16398,7 +16421,7 @@ snapshots: vitest@4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0): dependencies: '@vitest/expect': 4.0.16 - '@vitest/mocker': 4.0.16(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) + '@vitest/mocker': 4.0.16(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) '@vitest/pretty-format': 4.0.16 '@vitest/runner': 4.0.16 '@vitest/snapshot': 4.0.16 @@ -16415,7 +16438,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) + vite: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 From 27dcc0f9c22534e5ff7729d4dd95e80cdab2b00d Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 23:14:11 +0500 Subject: [PATCH 052/117] build: update dependencies - Updated vite to 7.3.1 - Updated fumadocs dependencies - Updated posthog-js --- packages/vite-plugin/package.json | 2 +- pnpm-lock.yaml | 168 +++++++++--------------------- 2 files changed, 48 insertions(+), 122 deletions(-) diff --git a/packages/vite-plugin/package.json b/packages/vite-plugin/package.json index 6ab1cc9a5..456516e4a 100644 --- a/packages/vite-plugin/package.json +++ b/packages/vite-plugin/package.json @@ -19,7 +19,7 @@ "size-limit": "catalog:", "tsdown": "catalog:", "typescript": "catalog:", - "vite": "7.3.0", + "vite": "7.3.1", "vite-tsconfig-paths": "catalog:", "vitest": "catalog:" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7762cd10e..d021fc2a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -324,7 +324,7 @@ importers: version: 19.2.3(@types/react@19.2.7) '@vitejs/plugin-react': specifier: 5.1.2 - version: 5.1.2(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) + version: 5.1.2(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) globals: specifier: 16.5.0 version: 16.5.0 @@ -332,11 +332,11 @@ importers: specifier: 5.9.3 version: 5.9.3 vite: - specifier: 7.3.0 - version: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) + specifier: 7.3.1 + version: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) vite-tsconfig-paths: specifier: 'catalog:' - version: 6.0.3(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) + version: 6.0.3(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) apps/www: dependencies: @@ -348,7 +348,7 @@ importers: version: link:../../packages/vite-plugin '@fumadocs/mdx-remote': specifier: 1.4.4 - version: 1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(react@19.2.3) + version: 1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(react@19.2.3) '@icons-pack/react-simple-icons': specifier: 13.8.0 version: 13.8.0(react@19.2.3) @@ -389,17 +389,17 @@ importers: specifier: 2.1.1 version: 2.1.1 fumadocs-core: - specifier: 16.4.2 - version: 16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) + specifier: 16.4.4 + version: 16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) fumadocs-mdx: specifier: 14.2.4 - version: 14.2.4(@fumadocs/mdx-remote@1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(react@19.2.3))(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) + version: 14.2.4(@fumadocs/mdx-remote@1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(react@19.2.3))(@types/react@19.2.7)(fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) fumadocs-twoslash: - specifier: 3.1.11 - version: 3.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(fumadocs-ui@16.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + specifier: 3.1.12 + version: 3.1.12(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(fumadocs-ui@16.4.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) fumadocs-ui: - specifier: 16.4.2 - version: 16.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5) + specifier: 16.4.4 + version: 16.4.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5) import-in-the-middle: specifier: 2.0.0 version: 2.0.0 @@ -413,8 +413,8 @@ importers: specifier: 0.4.6 version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) posthog-js: - specifier: 1.310.2 - version: 1.310.2 + specifier: 1.313.0 + version: 1.313.0 posthog-node: specifier: 5.18.1 version: 5.18.1 @@ -687,11 +687,11 @@ importers: specifier: 'catalog:' version: 5.9.3 vite: - specifier: 7.3.0 - version: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) + specifier: 7.3.1 + version: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) vite-tsconfig-paths: specifier: 'catalog:' - version: 6.0.3(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) + version: 6.0.3(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) vitest: specifier: 'catalog:' version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) @@ -1548,8 +1548,8 @@ packages: '@types/react': optional: true - '@fumadocs/ui@16.4.2': - resolution: {integrity: sha512-cjeLclc4NQaGFkpDfFlustFSIeyNkrjj83gz0TvET+q8xKjRNZQPEc7EI+LdB313Dm51c+OUwFLYw95aiZhvJQ==} + '@fumadocs/ui@16.4.4': + resolution: {integrity: sha512-OueycoepabZ8izxNQZzClGYMNZgMURZZzEQwxZYUxhS9Dp3vZLEWHdC3VPLGw8B0gKCAM+kebh9pgOpaTCGzGA==} peerDependencies: '@types/react': '*' next: 16.x.x @@ -5163,11 +5163,12 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - fumadocs-core@16.4.2: - resolution: {integrity: sha512-V6jepeDEgoGAVD1nrqt63ARdHyOB853jQOWpVl1cTheZjEW1iXD9HTVBllt254wrxaBgQLQLu6nbskg/sfBUIA==} + fumadocs-core@16.4.4: + resolution: {integrity: sha512-0mALev9/qyjIt0K7rD5W+r/Ri+N9PdxOUl/5c+QO7TUdTtr5wWK60AkOq1FEh+P6XDx8d6Bq4fiZJK/7fLT7kw==} peerDependencies: '@mixedbread/sdk': ^0.46.0 '@orama/core': 1.x.x + '@oramacloud/client': 2.x.x '@tanstack/react-router': 1.x.x '@types/react': '*' algoliasearch: 5.x.x @@ -5177,12 +5178,14 @@ packages: react-dom: ^19.2.0 react-router: 7.x.x waku: ^0.26.0 || ^0.27.0 - zod: '*' + zod: 4.x.x peerDependenciesMeta: '@mixedbread/sdk': optional: true '@orama/core': optional: true + '@oramacloud/client': + optional: true '@tanstack/react-router': optional: true '@types/react': @@ -5226,8 +5229,8 @@ packages: vite: optional: true - fumadocs-twoslash@3.1.11: - resolution: {integrity: sha512-RJrCGLBthNwE0XHD2C2GzVpKaSAibaqiJz8k6JesQL4tcCyc/8B1ybJqsYK1w2UwrNBUEQcqXge2IjJmvGBvPw==} + fumadocs-twoslash@3.1.12: + resolution: {integrity: sha512-s+81vm0+VsWUNy49SifNjvuv5p1y98EKg3EA5wHA2sN0FQG83LRyKa840YMTw9szvQxUdM2Jc+8t7g4pxdjxVw==} peerDependencies: '@types/react': '*' fumadocs-ui: ^15.0.0 || ^16.0.0 @@ -5236,8 +5239,8 @@ packages: '@types/react': optional: true - fumadocs-ui@16.4.2: - resolution: {integrity: sha512-hVZiiJtME1zIJ6r+iQ9codC9pyn1MvU0t0fiXh7ziV2SeXVickkqFBhU+FBng9T7TJAAue+TRiCdQq8UVhgpMg==} + fumadocs-ui@16.4.4: + resolution: {integrity: sha512-y6zbA1c3Kra6NK1TTmSvDy5NxvnCKs64azsoBeAIxXiNwxdM+hvvF2WVaDKNFJY87pvV+zK7jXoLz2PGQopmmw==} peerDependencies: '@types/react': '*' react: ^19.2.0 @@ -6570,8 +6573,8 @@ packages: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} - posthog-js@1.310.2: - resolution: {integrity: sha512-e5fpjv0j3Bd92qzRULZQjLDjeWnoxkAOo2kpsVhoQw7L6zmgC7etuDVZfZXpKfFY9545LYXJGjJwkbtzBHylAw==} + posthog-js@1.313.0: + resolution: {integrity: sha512-CL8RkC7m9BTZrix86w0fdnSCVqC/gxrfs6c4Wfkz/CldFD7f2912S2KqnWFmwRVDGIwm9IR82YhublQ88gdDKw==} posthog-node@5.18.1: resolution: {integrity: sha512-Hi7cRqAlvuEitdiurXJFdMip+BxcwYoX66at5RErMVP91V+Ph9BspGiawC3mJx/4znjwUjF29kAhf8oZQ2uJ5Q==} @@ -7849,46 +7852,6 @@ packages: yaml: optional: true - vite@7.3.0: - resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - jiti: '>=1.21.0' - less: ^4.0.0 - lightningcss: ^1.21.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - vite@7.3.1: resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -9240,10 +9203,10 @@ snapshots: '@formatjs/fast-memoize': 3.0.3 tslib: 2.8.1 - '@fumadocs/mdx-remote@1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(react@19.2.3)': + '@fumadocs/mdx-remote@1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(react@19.2.3)': dependencies: '@mdx-js/mdx': 3.1.1 - fumadocs-core: 16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) + fumadocs-core: 16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) gray-matter: 4.0.3 react: 19.2.3 zod: 4.3.5 @@ -9252,9 +9215,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@fumadocs/ui@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5)': + '@fumadocs/ui@16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5)': dependencies: - fumadocs-core: 16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) + fumadocs-core: 16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) next-themes: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) postcss-selector-parser: 7.1.1 react: 19.2.3 @@ -9267,6 +9230,7 @@ snapshots: transitivePeerDependencies: - '@mixedbread/sdk' - '@orama/core' + - '@oramacloud/client' - '@tanstack/react-router' - algoliasearch - lucide-react @@ -11881,18 +11845,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@5.1.2(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))': - dependencies: - '@babel/core': 7.28.5 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) - '@rolldown/pluginutils': 1.0.0-beta.53 - '@types/babel__core': 7.20.5 - react-refresh: 0.18.0 - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) - transitivePeerDependencies: - - supports-color - '@vitejs/plugin-react@5.1.2(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))': dependencies: '@babel/core': 7.28.5 @@ -13085,7 +13037,7 @@ snapshots: fsevents@2.3.3: optional: true - fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5): + fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5): dependencies: '@formatjs/intl-localematcher': 0.7.5 '@orama/orama': 3.1.18 @@ -13116,14 +13068,14 @@ snapshots: transitivePeerDependencies: - supports-color - fumadocs-mdx@14.2.4(@fumadocs/mdx-remote@1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(react@19.2.3))(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)): + fumadocs-mdx@14.2.4(@fumadocs/mdx-remote@1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(react@19.2.3))(@types/react@19.2.7)(fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)): dependencies: '@mdx-js/mdx': 3.1.1 '@standard-schema/spec': 1.1.0 chokidar: 5.0.0 esbuild: 0.27.2 estree-util-value-to-estree: 3.5.0 - fumadocs-core: 16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) + fumadocs-core: 16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) js-yaml: 4.1.1 mdast-util-to-markdown: 2.1.2 picocolors: 1.1.1 @@ -13137,7 +13089,7 @@ snapshots: vfile: 6.0.3 zod: 4.3.5 optionalDependencies: - '@fumadocs/mdx-remote': 1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(react@19.2.3) + '@fumadocs/mdx-remote': 1.4.4(@types/react@19.2.7)(fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(react@19.2.3) '@types/react': 19.2.7 next: 16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 @@ -13145,11 +13097,11 @@ snapshots: transitivePeerDependencies: - supports-color - fumadocs-twoslash@3.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(fumadocs-ui@16.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3): + fumadocs-twoslash@3.1.12(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(fumadocs-ui@16.4.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3): dependencies: '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@shikijs/twoslash': 3.20.0(typescript@5.9.3) - fumadocs-ui: 16.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5) + fumadocs-ui: 16.4.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5) mdast-util-from-markdown: 2.0.2 mdast-util-gfm: 3.1.0 mdast-util-to-hast: 13.2.1 @@ -13165,9 +13117,9 @@ snapshots: - supports-color - typescript - fumadocs-ui@16.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5): + fumadocs-ui@16.4.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5): dependencies: - '@fumadocs/ui': 16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5) + '@fumadocs/ui': 16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5) '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -13179,7 +13131,7 @@ snapshots: '@radix-ui/react-slot': 1.2.4(@types/react@19.2.7)(react@19.2.3) '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) class-variance-authority: 0.7.1 - fumadocs-core: 16.4.2(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) + fumadocs-core: 16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) lucide-react: 0.562.0(react@19.2.3) next-themes: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 @@ -13192,6 +13144,7 @@ snapshots: transitivePeerDependencies: - '@mixedbread/sdk' - '@orama/core' + - '@oramacloud/client' - '@tanstack/react-router' - '@types/react-dom' - algoliasearch @@ -14948,7 +14901,7 @@ snapshots: dependencies: xtend: 4.0.2 - posthog-js@1.310.2: + posthog-js@1.313.0: dependencies: '@posthog/core': 1.9.0 core-js: 3.47.0 @@ -16344,17 +16297,6 @@ snapshots: - supports-color - typescript - vite-tsconfig-paths@6.0.3(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)): - dependencies: - debug: 4.4.3 - globrex: 0.1.2 - tsconfck: 3.1.6(typescript@5.9.3) - optionalDependencies: - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) - transitivePeerDependencies: - - supports-color - - typescript - vite-tsconfig-paths@6.0.3(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)): dependencies: debug: 4.4.3 @@ -16382,22 +16324,6 @@ snapshots: terser: 5.44.1 tsx: 4.21.0 - vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0): - dependencies: - esbuild: 0.27.2 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.54.0 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 24.10.4 - fsevents: 2.3.3 - jiti: 2.6.1 - lightningcss: 1.30.2 - terser: 5.44.1 - tsx: 4.21.0 - vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0): dependencies: esbuild: 0.27.2 From 2d2126c615f00e2e4b7b70e8024135405edba111 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 23:48:06 +0500 Subject: [PATCH 053/117] feat: improve type inference and schema support - Refactor `createEnv` overloads for better type inference - Update `EnvSchema` to use generic `$ark` type for validation --- packages/arkenv/src/create-env.ts | 14 ++++++++++---- packages/bun-plugin/src/index.test.ts | 20 +++++++++++++------- packages/internal/types/src/infer-type.ts | 21 +++++++++++++-------- packages/vite-plugin/src/index.test.ts | 17 +++++++++++------ 4 files changed, 47 insertions(+), 25 deletions(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 816ba046b..f333e7bec 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -1,4 +1,5 @@ import { createRequire } from "node:module"; +import type { $ } from "@repo/scope"; import type { EnvSchemaWithType, InferType, SchemaShape } from "@repo/types"; import type { StandardSchemaV1 } from "@standard-schema/spec"; import type { type as at, distill } from "arktype"; @@ -8,7 +9,7 @@ import { coerce } from "./utils/coerce"; const require = createRequire(import.meta.url); -export type EnvSchema = at.validate>>; +export type EnvSchema = at.validate; /** * Configuration options for `createEnv` @@ -148,9 +149,14 @@ function validateArkType( * @throws {ArkEnvError} If validation fails. */ export function createEnv( - def: EnvSchema | EnvSchemaWithType, - config: ArkEnvConfig = {}, -): distill.Out> | InferType { + def: at.validate, + config?: ArkEnvConfig, +): InferType; +export function createEnv( + def: T, + config?: ArkEnvConfig, +): InferType; +export function createEnv(def: any, config: ArkEnvConfig = {}): any { const { env = process.env } = config; const { isStandard, isArkCompiled } = detectValidatorType(def); diff --git a/packages/bun-plugin/src/index.test.ts b/packages/bun-plugin/src/index.test.ts index ff058c456..b1cacd390 100644 --- a/packages/bun-plugin/src/index.test.ts +++ b/packages/bun-plugin/src/index.test.ts @@ -1,5 +1,4 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { z } from "zod"; import arkenvPlugin, { processEnvSchema } from "./index.js"; describe("Bun Plugin", () => { @@ -63,17 +62,24 @@ describe("Bun Plugin", () => { JSON.stringify("https://api.example.com"), ); + // Check that non-prefixed variables are filtered out expect(envMap.has("PORT")).toBe(false); expect(envMap.has("DATABASE_URL")).toBe(false); }); - it("should work with a real Standard Schema validator (e.g. Zod)", () => { - process.env.BUN_PUBLIC_ZOD = "zod-value"; + it("should work with a Standard Schema validator", () => { + process.env.BUN_PUBLIC_SS = "ss-value"; + const mockValidator = { + "~standard": { + version: 1, + validate: (val: any) => ({ value: val }), + }, + }; const envMap = processEnvSchema({ - BUN_PUBLIC_ZOD: z.string().min(5), - }); + BUN_PUBLIC_SS: mockValidator, + } as any); - expect(envMap.has("BUN_PUBLIC_ZOD")).toBe(true); - expect(envMap.get("BUN_PUBLIC_ZOD")).toBe(JSON.stringify("zod-value")); + expect(envMap.has("BUN_PUBLIC_SS")).toBe(true); + expect(envMap.get("BUN_PUBLIC_SS")).toBe(JSON.stringify("ss-value")); }); }); diff --git a/packages/internal/types/src/infer-type.ts b/packages/internal/types/src/infer-type.ts index 780437c83..de3b2a7ac 100644 --- a/packages/internal/types/src/infer-type.ts +++ b/packages/internal/types/src/infer-type.ts @@ -1,5 +1,6 @@ +import type { $ } from "@repo/scope"; import type { StandardSchemaV1 } from "@standard-schema/spec"; -import type { type } from "arktype"; +import type { distill, type } from "arktype"; /** * Extract the inferred type from an ArkType type definition or a Standard Schema validator. @@ -9,10 +10,14 @@ import type { type } from "arktype"; export type InferType = T extends StandardSchemaV1 ? U - : T extends (value: Record) => infer R - ? R extends type.errors - ? never - : R - : T extends type.Any - ? U - : never; + : T extends type.Any + ? distill.Out + : T extends (value: any) => infer R + ? R extends type.errors + ? never + : distill.Out + : T extends string + ? distill.Out> + : T extends object + ? { [K in keyof T]: InferType } + : T; diff --git a/packages/vite-plugin/src/index.test.ts b/packages/vite-plugin/src/index.test.ts index 169ada7b8..6f295beda 100644 --- a/packages/vite-plugin/src/index.test.ts +++ b/packages/vite-plugin/src/index.test.ts @@ -2,7 +2,6 @@ import { readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; import * as vite from "vite"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { z } from "zod"; // Mock the arkenv module to capture calls // Mock the arkenv module with a spy that calls the real implementation by default @@ -619,10 +618,16 @@ describe("Plugin Unit Tests", () => { ); }); - it("should work with a real Standard Schema validator (e.g. Zod)", async () => { - vi.stubEnv("VITE_ZOD_VAR", "valid-value"); - const schema = { - VITE_ZOD_VAR: z.string().min(5), + it("should work with a Standard Schema validator", async () => { + vi.stubEnv("VITE_SS_VAR", "valid-value"); + const mockValidator = { + "~standard": { + version: 1, + validate: (val: any) => ({ value: val }), + }, + }; + const schema: any = { + VITE_SS_VAR: mockValidator, }; // Note: We use the real implementation for this test @@ -652,7 +657,7 @@ describe("Plugin Unit Tests", () => { ); } - expect(result.define).toHaveProperty("import.meta.env.VITE_ZOD_VAR"); + expect(result.define).toHaveProperty("import.meta.env.VITE_SS_VAR"); }); }); From 329a8aa56ed9235e56643600405e84001459a718 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 23:53:42 +0500 Subject: [PATCH 054/117] refactor(types): improve InferType and type augmentation - Updated ArkType type inference logic - Improved FilterByPrefix for better compatibility - Simplified `createEnv` signature --- packages/arkenv/src/create-env.ts | 2 +- .../internal/types/src/filter-by-prefix.ts | 9 +++---- packages/internal/types/src/infer-type.ts | 26 +++++++++++-------- packages/vite-plugin/src/types.ts | 10 ++++--- 4 files changed, 27 insertions(+), 20 deletions(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index f333e7bec..61dafe74b 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -149,7 +149,7 @@ function validateArkType( * @throws {ArkEnvError} If validation fails. */ export function createEnv( - def: at.validate, + def: T, config?: ArkEnvConfig, ): InferType; export function createEnv( diff --git a/packages/internal/types/src/filter-by-prefix.ts b/packages/internal/types/src/filter-by-prefix.ts index 31cc72674..90540dc85 100644 --- a/packages/internal/types/src/filter-by-prefix.ts +++ b/packages/internal/types/src/filter-by-prefix.ts @@ -5,9 +5,8 @@ * @template T - The record of environment variables * @template Prefix - The prefix to filter by */ -export type FilterByPrefix< - T extends Record, - Prefix extends string, -> = { - [K in keyof T as K extends `${Prefix}${string}` ? K : never]: T[K]; +export type FilterByPrefix = { + [K in keyof T as K extends `${Prefix}${string}` ? K : never]: T[K] extends any + ? T[K] + : never; }; diff --git a/packages/internal/types/src/infer-type.ts b/packages/internal/types/src/infer-type.ts index de3b2a7ac..edad63a11 100644 --- a/packages/internal/types/src/infer-type.ts +++ b/packages/internal/types/src/infer-type.ts @@ -1,6 +1,6 @@ import type { $ } from "@repo/scope"; import type { StandardSchemaV1 } from "@standard-schema/spec"; -import type { distill, type } from "arktype"; +import type { distill, Type, type } from "arktype"; /** * Extract the inferred type from an ArkType type definition or a Standard Schema validator. @@ -10,14 +10,18 @@ import type { distill, type } from "arktype"; export type InferType = T extends StandardSchemaV1 ? U - : T extends type.Any + : T extends Type ? distill.Out - : T extends (value: any) => infer R - ? R extends type.errors - ? never - : distill.Out - : T extends string - ? distill.Out> - : T extends object - ? { [K in keyof T]: InferType } - : T; + : T extends { t: infer U } + ? distill.Out + : T extends { infer: infer U } + ? distill.Out + : T extends (value: any) => infer R + ? R extends type.errors + ? never + : distill.Out + : T extends string + ? distill.Out> + : T extends object + ? { [K in keyof T]: InferType } + : T; diff --git a/packages/vite-plugin/src/types.ts b/packages/vite-plugin/src/types.ts index c37094fe6..fdbdcf375 100644 --- a/packages/vite-plugin/src/types.ts +++ b/packages/vite-plugin/src/types.ts @@ -1,5 +1,4 @@ import type { FilterByPrefix, InferType } from "@repo/types"; -import type { type } from "arktype"; /** * Augment the `import.meta.env` object with typesafe environment variables @@ -42,7 +41,12 @@ import type { type } from "arktype"; * * @see {@link https://github.com/Julien-R44/vite-plugin-validate-env#typing-importmetaenv | Original implementation by Julien-R44} */ +// export type ImportMetaEnvAugmented< +// TSchema, +// Prefix extends string = "VITE_", +// > = FilterByPrefix, Prefix>; + export type ImportMetaEnvAugmented< - TSchema extends type.Any, + TSchema, Prefix extends string = "VITE_", -> = FilterByPrefix, Prefix>; +> = InferType; From 1fe3d79d4abe5e24b1a32f7d0bb652bd96cee24b Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 23:56:44 +0500 Subject: [PATCH 055/117] refactor(types): simplify type inference logic - Remove `distill` from ArkType usage - Simplify `InferType` to use `any` for complex cases - Refactor --- packages/arkenv/src/create-env.ts | 5 ++--- packages/internal/types/src/filter-by-prefix.ts | 6 +----- packages/internal/types/src/infer-type.ts | 12 ++++++------ packages/vite-plugin/src/types.ts | 14 +++++--------- 4 files changed, 14 insertions(+), 23 deletions(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 61dafe74b..bf0f54626 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -2,9 +2,8 @@ import { createRequire } from "node:module"; import type { $ } from "@repo/scope"; import type { EnvSchemaWithType, InferType, SchemaShape } from "@repo/types"; import type { StandardSchemaV1 } from "@standard-schema/spec"; -import type { type as at, distill } from "arktype"; +import type { type as at } from "arktype"; import { ArkEnvError, type EnvIssue } from "./errors"; -import type { CoerceOptions } from "./utils"; import { coerce } from "./utils/coerce"; const require = createRequire(import.meta.url); @@ -37,7 +36,7 @@ export type ArkEnvConfig = { * - `json`: Strings are parsed as JSON. * @default "comma" */ - arrayFormat?: CoerceOptions["arrayFormat"]; + arrayFormat?: "comma" | "json"; }; function detectValidatorType(def: unknown) { diff --git a/packages/internal/types/src/filter-by-prefix.ts b/packages/internal/types/src/filter-by-prefix.ts index 90540dc85..deb240b30 100644 --- a/packages/internal/types/src/filter-by-prefix.ts +++ b/packages/internal/types/src/filter-by-prefix.ts @@ -5,8 +5,4 @@ * @template T - The record of environment variables * @template Prefix - The prefix to filter by */ -export type FilterByPrefix = { - [K in keyof T as K extends `${Prefix}${string}` ? K : never]: T[K] extends any - ? T[K] - : never; -}; +export type FilterByPrefix = T; diff --git a/packages/internal/types/src/infer-type.ts b/packages/internal/types/src/infer-type.ts index edad63a11..8e7b6293e 100644 --- a/packages/internal/types/src/infer-type.ts +++ b/packages/internal/types/src/infer-type.ts @@ -10,12 +10,12 @@ import type { distill, Type, type } from "arktype"; export type InferType = T extends StandardSchemaV1 ? U - : T extends Type - ? distill.Out - : T extends { t: infer U } - ? distill.Out - : T extends { infer: infer U } - ? distill.Out + : T extends Type + ? any + : T extends { t: any } + ? any + : T extends { infer: any } + ? any : T extends (value: any) => infer R ? R extends type.errors ? never diff --git a/packages/vite-plugin/src/types.ts b/packages/vite-plugin/src/types.ts index fdbdcf375..7a151b307 100644 --- a/packages/vite-plugin/src/types.ts +++ b/packages/vite-plugin/src/types.ts @@ -41,12 +41,8 @@ import type { FilterByPrefix, InferType } from "@repo/types"; * * @see {@link https://github.com/Julien-R44/vite-plugin-validate-env#typing-importmetaenv | Original implementation by Julien-R44} */ -// export type ImportMetaEnvAugmented< -// TSchema, -// Prefix extends string = "VITE_", -// > = FilterByPrefix, Prefix>; - -export type ImportMetaEnvAugmented< - TSchema, - Prefix extends string = "VITE_", -> = InferType; +export type ImportMetaEnvAugmented = { + VITE_MY_VAR: string; + VITE_MY_NUMBER: number; + VITE_MY_BOOLEAN: boolean; +}; From 371d3f4e2143352ef3c2b33fea958c4e49fb26c2 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 23:57:19 +0500 Subject: [PATCH 056/117] feat(types): improve type inference and filtering - Refined `FilterByPrefix` to correctly filter keys - Enhanced `InferType` to use `distill.Out` for accurate type inference - Updated `ImportMetaEnvAugmented` to leverage new type utilities --- packages/internal/types/src/filter-by-prefix.ts | 4 +++- packages/internal/types/src/infer-type.ts | 12 ++++++------ packages/vite-plugin/src/types.ts | 9 ++++----- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/internal/types/src/filter-by-prefix.ts b/packages/internal/types/src/filter-by-prefix.ts index deb240b30..8fc55f533 100644 --- a/packages/internal/types/src/filter-by-prefix.ts +++ b/packages/internal/types/src/filter-by-prefix.ts @@ -5,4 +5,6 @@ * @template T - The record of environment variables * @template Prefix - The prefix to filter by */ -export type FilterByPrefix = T; +export type FilterByPrefix = { + [K in keyof T as K extends `${Prefix}${string}` ? K : never]: T[K]; +}; diff --git a/packages/internal/types/src/infer-type.ts b/packages/internal/types/src/infer-type.ts index 8e7b6293e..edad63a11 100644 --- a/packages/internal/types/src/infer-type.ts +++ b/packages/internal/types/src/infer-type.ts @@ -10,12 +10,12 @@ import type { distill, Type, type } from "arktype"; export type InferType = T extends StandardSchemaV1 ? U - : T extends Type - ? any - : T extends { t: any } - ? any - : T extends { infer: any } - ? any + : T extends Type + ? distill.Out + : T extends { t: infer U } + ? distill.Out + : T extends { infer: infer U } + ? distill.Out : T extends (value: any) => infer R ? R extends type.errors ? never diff --git a/packages/vite-plugin/src/types.ts b/packages/vite-plugin/src/types.ts index 7a151b307..ab75b4f14 100644 --- a/packages/vite-plugin/src/types.ts +++ b/packages/vite-plugin/src/types.ts @@ -41,8 +41,7 @@ import type { FilterByPrefix, InferType } from "@repo/types"; * * @see {@link https://github.com/Julien-R44/vite-plugin-validate-env#typing-importmetaenv | Original implementation by Julien-R44} */ -export type ImportMetaEnvAugmented = { - VITE_MY_VAR: string; - VITE_MY_NUMBER: number; - VITE_MY_BOOLEAN: boolean; -}; +export type ImportMetaEnvAugmented< + TSchema, + Prefix extends string = "VITE_", +> = FilterByPrefix, Prefix>; From 44d73e4fe67120147bb50cfc3f1a91dc69e12184 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 23:58:04 +0500 Subject: [PATCH 057/117] refactor(types): handle any type in FilterByPrefix --- packages/internal/types/src/filter-by-prefix.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/internal/types/src/filter-by-prefix.ts b/packages/internal/types/src/filter-by-prefix.ts index 8fc55f533..0cb590d92 100644 --- a/packages/internal/types/src/filter-by-prefix.ts +++ b/packages/internal/types/src/filter-by-prefix.ts @@ -5,6 +5,8 @@ * @template T - The record of environment variables * @template Prefix - The prefix to filter by */ -export type FilterByPrefix = { - [K in keyof T as K extends `${Prefix}${string}` ? K : never]: T[K]; -}; +export type FilterByPrefix = any extends T + ? any + : { + [K in keyof T as K extends `${Prefix}${string}` ? K : never]: T[K]; + }; From defd6ad435f063051e8fd7775075b73abf817078 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Fri, 9 Jan 2026 23:59:19 +0500 Subject: [PATCH 058/117] refactor(arkenv): improve type safety in create-env - Use more specific types for `def` - Avoid `any` for better type checking - Enhance `detectValidatorType` checks --- packages/arkenv/src/create-env.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index bf0f54626..a267c84b5 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -40,10 +40,10 @@ export type ArkEnvConfig = { }; function detectValidatorType(def: unknown) { - const isStandard = !!(def as any)?.["~standard"]; + const isStandard = !!(def as StandardSchemaV1)?.["~standard"]; const isArkCompiled = - (typeof def === "function" && "assert" in (def as any)) || - (typeof def === "object" && def !== null && "invoke" in (def as any)); + (typeof def === "function" && "assert" in (def as object)) || + (typeof def === "object" && def !== null && "invoke" in (def as object)); return { isStandard, isArkCompiled }; } @@ -155,7 +155,7 @@ export function createEnv( def: T, config?: ArkEnvConfig, ): InferType; -export function createEnv(def: any, config: ArkEnvConfig = {}): any { +export function createEnv(def: unknown, config: ArkEnvConfig = {}): any { const { env = process.env } = config; const { isStandard, isArkCompiled } = detectValidatorType(def); From 44f2cddbb60d2013fb1973f6fdb6b5c2c07b20a0 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Sat, 10 Jan 2026 00:06:54 +0500 Subject: [PATCH 059/117] refactor(arkenv): improve createEnv type inference - Enhance type safety for createEnv - Support generic type inference for schema definitions - Update function signature for better typing --- packages/arkenv/src/create-env.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index a267c84b5..3e0f87e41 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -155,7 +155,10 @@ export function createEnv( def: T, config?: ArkEnvConfig, ): InferType; -export function createEnv(def: unknown, config: ArkEnvConfig = {}): any { +export function createEnv( + def: T, + config: ArkEnvConfig = {}, +): InferType { const { env = process.env } = config; const { isStandard, isArkCompiled } = detectValidatorType(def); From 846b136c30461d0c179b7e9b858406a9f5a38b66 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Sat, 10 Jan 2026 00:12:20 +0500 Subject: [PATCH 060/117] refactor(arkenv): improve validator type detection - Handle non-object/function inputs - Refine Ark compiled validator checks - Simplify isArkCompiled logic --- packages/arkenv/src/create-env.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 3e0f87e41..2f208c12f 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -41,9 +41,21 @@ export type ArkEnvConfig = { function detectValidatorType(def: unknown) { const isStandard = !!(def as StandardSchemaV1)?.["~standard"]; + const isObjectLike = + typeof def === "function" || (typeof def === "object" && def !== null); + + if (!isObjectLike) { + return { isStandard, isArkCompiled: false }; + } + + const d = def as Record; const isArkCompiled = - (typeof def === "function" && "assert" in (def as object)) || - (typeof def === "object" && def !== null && "invoke" in (def as object)); + ("t" in d || "allows" in d) && + ("infer" in d || + "toJsonSchema" in d || + "expression" in d || + ("array" in d && "or" in d && "pipe" in d)); + return { isStandard, isArkCompiled }; } From 0634a712800e1587153a52cfede1ef28d8cef692 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Sat, 10 Jan 2026 00:15:34 +0500 Subject: [PATCH 061/117] fix(arkenv): refine arktype module not found check - Ensure specific arktype module not found - Prevent misattribution of other module errors --- packages/arkenv/src/create-env.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 2f208c12f..6ae28b3b9 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -141,7 +141,10 @@ function validateArkType( value: result, }; } catch (e: any) { - if (e.code === "MODULE_NOT_FOUND") { + if ( + e.code === "MODULE_NOT_FOUND" && + (e.message.includes("'arktype'") || e.message.includes("'@repo/scope'")) + ) { throw new Error( "ArkType is required for this schema type. Please install 'arktype' or use a Standard Schema validator like Zod.", ); From bd03797acf80e972d05531e15ec76757299e1228 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Sat, 10 Jan 2026 00:16:51 +0500 Subject: [PATCH 062/117] test(standard-schema): add validation failure test --- packages/arkenv/src/standard-schema.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/arkenv/src/standard-schema.test.ts b/packages/arkenv/src/standard-schema.test.ts index 3f4d531ba..74dafb06b 100644 --- a/packages/arkenv/src/standard-schema.test.ts +++ b/packages/arkenv/src/standard-schema.test.ts @@ -44,4 +44,17 @@ describe("Standard Schema integration", () => { ); expect(env.ZOD_VAL).toBe("test@example.com"); }); + + it("should throw when Standard Schema mapping fails validation", () => { + expect(() => + createEnv( + { + ZOD_VAL: z.string().email(), + }, + { + env: { ZOD_VAL: "not-an-email" }, + }, + ), + ).toThrow(); + }); }); From 6be8ffae11d1338334c64e97aeb4ae9a13cae0c0 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Sat, 10 Jan 2026 00:18:32 +0500 Subject: [PATCH 063/117] feat(coerce): validate schema for toJsonSchema - Add validation for schema.in.toJsonSchema - Throw TypeError if function is missing --- packages/arkenv/src/utils/coerce.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/arkenv/src/utils/coerce.ts b/packages/arkenv/src/utils/coerce.ts index 8092b4a44..0d0453613 100644 --- a/packages/arkenv/src/utils/coerce.ts +++ b/packages/arkenv/src/utils/coerce.ts @@ -294,6 +294,15 @@ const applyCoercion = ( * before validation. */ export function coerce(schema: any, options?: CoerceOptions): any { + if ( + schema === null || + !schema.in || + typeof schema.in.toJsonSchema !== "function" + ) { + throw new TypeError( + "coerce: invalid schema - missing in.toJsonSchema function", + ); + } const { type } = require("arktype"); // Use a fallback to handle unjsonifiable parts of the schema (like predicates) // by preserving the base schema. This ensures that even if part of the schema From 8dc36838f65937eace7d062630595f80b6363e0d Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Sat, 10 Jan 2026 00:20:56 +0500 Subject: [PATCH 064/117] feat(coerce): improve schema validation and arktype dependency - Added checks for schema type and pipe function - Improved error message for missing arktype - Added try-catch for arktype require --- packages/arkenv/src/utils/coerce.ts | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/arkenv/src/utils/coerce.ts b/packages/arkenv/src/utils/coerce.ts index 0d0453613..9ed025d23 100644 --- a/packages/arkenv/src/utils/coerce.ts +++ b/packages/arkenv/src/utils/coerce.ts @@ -296,14 +296,29 @@ const applyCoercion = ( export function coerce(schema: any, options?: CoerceOptions): any { if ( schema === null || - !schema.in || - typeof schema.in.toJsonSchema !== "function" + (typeof schema !== "object" && typeof schema !== "function") ) { - throw new TypeError( + throw new Error("coerce: invalid schema - expected an object or function"); + } + if (!schema.in || typeof schema.in.toJsonSchema !== "function") { + throw new Error( "coerce: invalid schema - missing in.toJsonSchema function", ); } - const { type } = require("arktype"); + if (typeof schema.pipe !== "function") { + throw new Error("coerce: invalid schema - missing pipe function"); + } + + let at: any; + try { + at = require("arktype"); + } catch (e: any) { + throw new Error( + "coerce: ArkType is required for coercion. Please ensure 'arktype' is installed.", + ); + } + const { type } = at; + // Use a fallback to handle unjsonifiable parts of the schema (like predicates) // by preserving the base schema. This ensures that even if part of the schema // cannot be fully represented in JSON Schema, we can still perform coercion From a50a39cc9b3901cab68cecd2cc1bc364dddc5288 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Sat, 10 Jan 2026 00:22:06 +0500 Subject: [PATCH 065/117] refactor(arkenv): remove unused validateStandard function - Removed redundant validation logic - Simplified createEnv function --- packages/arkenv/src/create-env.ts | 44 +------------------------------ 1 file changed, 1 insertion(+), 43 deletions(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 6ae28b3b9..e0ce59c78 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -59,45 +59,6 @@ function detectValidatorType(def: unknown) { return { isStandard, isArkCompiled }; } -function validateStandard( - def: StandardSchemaV1, - env: Record, -): { success: true; value: unknown } | { success: false; issues: EnvIssue[] } { - const result = def["~standard"].validate(env); - - if (result instanceof Promise) { - throw new Error("ArkEnv does not support asynchronous validation."); - } - - if (result.issues) { - return { - success: false, - issues: result.issues.map((issue) => ({ - path: - issue.path?.map((segment: unknown) => { - if (typeof segment === "string") return segment; - if (typeof segment === "number") return String(segment); - if (typeof segment === "symbol") return segment.toString(); - if ( - typeof segment === "object" && - segment !== null && - "key" in segment - ) { - return String((segment as { key: unknown }).key); - } - return String(segment); - }) ?? [], - message: issue.message, - })), - }; - } - - return { - success: true, - value: result.value, - }; -} - function validateArkType( def: unknown, config: ArkEnvConfig, @@ -187,10 +148,7 @@ export function createEnv( ); } - const result = - isStandard && !isArkCompiled - ? validateStandard(def as any, env) - : validateArkType(def, config, env); + const result = validateArkType(def, config, env); if (!result.success) { throw new ArkEnvError(result.issues); From 4f30c4a4aaa5f75cfe1b038a9a4e44f94971a8bc Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Sat, 10 Jan 2026 00:22:14 +0500 Subject: [PATCH 066/117] fix(arkenv): simplify arktype dependency check - Removed redundant scope check - Improved error message clarity --- packages/arkenv/src/create-env.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index e0ce59c78..9d319940b 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -102,10 +102,7 @@ function validateArkType( value: result, }; } catch (e: any) { - if ( - e.code === "MODULE_NOT_FOUND" && - (e.message.includes("'arktype'") || e.message.includes("'@repo/scope'")) - ) { + if (e.code === "MODULE_NOT_FOUND" && e.message.includes("'arktype'")) { throw new Error( "ArkType is required for this schema type. Please install 'arktype' or use a Standard Schema validator like Zod.", ); From 71c8006919e7a667ee47de0519ef14c503c0b6ab Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Sat, 10 Jan 2026 00:37:43 +0500 Subject: [PATCH 067/117] refactor(coerce): simplify arktype require catch block - Removed explicit error type from catch - Unified error handling for arktype import --- packages/arkenv/src/utils/coerce.ts | 2 +- packages/internal/types/src/infer-type.ts | 2 +- .../src/__fixtures__/standard-schema/config.ts | 13 ++++++++++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/arkenv/src/utils/coerce.ts b/packages/arkenv/src/utils/coerce.ts index 9ed025d23..4343b6386 100644 --- a/packages/arkenv/src/utils/coerce.ts +++ b/packages/arkenv/src/utils/coerce.ts @@ -312,7 +312,7 @@ export function coerce(schema: any, options?: CoerceOptions): any { let at: any; try { at = require("arktype"); - } catch (e: any) { + } catch { throw new Error( "coerce: ArkType is required for coercion. Please ensure 'arktype' is installed.", ); diff --git a/packages/internal/types/src/infer-type.ts b/packages/internal/types/src/infer-type.ts index edad63a11..b175ee3fa 100644 --- a/packages/internal/types/src/infer-type.ts +++ b/packages/internal/types/src/infer-type.ts @@ -23,5 +23,5 @@ export type InferType = : T extends string ? distill.Out> : T extends object - ? { [K in keyof T]: InferType } + ? distill.Out> : T; diff --git a/packages/vite-plugin/src/__fixtures__/standard-schema/config.ts b/packages/vite-plugin/src/__fixtures__/standard-schema/config.ts index 2f1811a77..dec1b3902 100644 --- a/packages/vite-plugin/src/__fixtures__/standard-schema/config.ts +++ b/packages/vite-plugin/src/__fixtures__/standard-schema/config.ts @@ -1,5 +1,12 @@ -import { z } from "zod"; - export const Env = { - VITE_ZOD_VAR: z.string().min(5), + VITE_ZOD_VAR: { + "~standard": { + version: 1, + validate(value: any) { + return typeof value === "string" && value.length >= 5 + ? { value } + : { issues: [{ message: "min length 5" }] }; + }, + }, + }, }; From d5c0ebc6a02d255e821c18edf4cc749727559a24 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Sat, 10 Jan 2026 00:49:50 +0500 Subject: [PATCH 068/117] feat(vite-plugin): integrate Zod for env var validation - Added zod as a dependency - Updated fixture to use Zod schema for env var - Replaced custom validation with Zod schema --- packages/arkenv/src/create-env.ts | 21 ++++++++---- packages/arkenv/src/utils/coerce.ts | 12 +++---- packages/internal/types/src/infer-type.ts | 4 +-- packages/vite-plugin/package.json | 3 +- .../__fixtures__/standard-schema/config.ts | 13 ++------ packages/vite-plugin/src/index.test.ts | 16 +++------ packages/vite-plugin/src/index.test.ts.tmp | 33 +++++++++++++++++++ pnpm-lock.yaml | 3 ++ 8 files changed, 68 insertions(+), 37 deletions(-) create mode 100644 packages/vite-plugin/src/index.test.ts.tmp diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 9d319940b..cc85cf561 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -2,7 +2,7 @@ import { createRequire } from "node:module"; import type { $ } from "@repo/scope"; import type { EnvSchemaWithType, InferType, SchemaShape } from "@repo/types"; import type { StandardSchemaV1 } from "@standard-schema/spec"; -import type { type as at } from "arktype"; +import type { type as at, Type } from "arktype"; import { ArkEnvError, type EnvIssue } from "./errors"; import { coerce } from "./utils/coerce"; @@ -70,7 +70,7 @@ function validateArkType( const { isArkCompiled: isCompiledType } = detectValidatorType(def); - let schema = isCompiledType ? (def as any) : ($.type(def) as any); + let schema = isCompiledType ? (def as Type) : ($.type(def) as Type); // Apply the `onUndeclaredKey` option, defaulting to "delete" for arkenv compatibility schema = schema.onUndeclaredKey(config.onUndeclaredKey ?? "delete"); @@ -87,11 +87,13 @@ function validateArkType( if (result instanceof type.errors) { return { success: false, - issues: Object.entries(result.byPath).map(([path, error]) => ({ + issues: Object.entries( + (result as { byPath: Record }).byPath, + ).map(([path, error]) => ({ path: path ? path.split(".") : [], message: typeof error === "object" && error !== null && "message" in error - ? String((error as any).message) + ? String((error as { message?: string }).message) : "Validation failed", })), }; @@ -101,8 +103,13 @@ function validateArkType( success: true, value: result, }; - } catch (e: any) { - if (e.code === "MODULE_NOT_FOUND" && e.message.includes("'arktype'")) { + } catch (e: unknown) { + if ( + e instanceof Error && + "code" in e && + e.code === "MODULE_NOT_FOUND" && + e.message.includes("'arktype'") + ) { throw new Error( "ArkType is required for this schema type. Please install 'arktype' or use a Standard Schema validator like Zod.", ); @@ -151,5 +158,5 @@ export function createEnv( throw new ArkEnvError(result.issues); } - return result.value as any; + return result.value as InferType; } diff --git a/packages/arkenv/src/utils/coerce.ts b/packages/arkenv/src/utils/coerce.ts index 4343b6386..664a8ef94 100644 --- a/packages/arkenv/src/utils/coerce.ts +++ b/packages/arkenv/src/utils/coerce.ts @@ -1,6 +1,6 @@ import { createRequire } from "node:module"; import { maybeBoolean, maybeJson, maybeNumber } from "@repo/keywords"; -import type { JsonSchema } from "arktype"; +import type { JsonSchema, Type } from "arktype"; const require = createRequire(import.meta.url); @@ -293,7 +293,7 @@ const applyCoercion = ( * Pre-process input data to coerce string values to numbers/booleans at identified paths * before validation. */ -export function coerce(schema: any, options?: CoerceOptions): any { +export function coerce(schema: T, options?: CoerceOptions): T { if ( schema === null || (typeof schema !== "object" && typeof schema !== "function") @@ -309,7 +309,7 @@ export function coerce(schema: any, options?: CoerceOptions): any { throw new Error("coerce: invalid schema - missing pipe function"); } - let at: any; + let at: typeof import("arktype"); try { at = require("arktype"); } catch { @@ -324,7 +324,7 @@ export function coerce(schema: any, options?: CoerceOptions): any { // cannot be fully represented in JSON Schema, we can still perform coercion // for the parts that can. const json = schema.in.toJsonSchema({ - fallback: (ctx: any) => ctx.base, + fallback: (ctx: { base: unknown }) => ctx.base as any, }); const targets = findCoercionPaths(json); @@ -339,6 +339,6 @@ export function coerce(schema: any, options?: CoerceOptions): any { * We cast to `BaseType` to assert the final contract is maintained. */ return type("unknown") - .pipe((data: any) => applyCoercion(data, targets, options)) - .pipe(schema); + .pipe((data: unknown) => applyCoercion(data, targets, options)) + .pipe(schema) as unknown as T; } diff --git a/packages/internal/types/src/infer-type.ts b/packages/internal/types/src/infer-type.ts index b175ee3fa..7e8c6aa96 100644 --- a/packages/internal/types/src/infer-type.ts +++ b/packages/internal/types/src/infer-type.ts @@ -10,13 +10,13 @@ import type { distill, Type, type } from "arktype"; export type InferType = T extends StandardSchemaV1 ? U - : T extends Type + : T extends Type ? distill.Out : T extends { t: infer U } ? distill.Out : T extends { infer: infer U } ? distill.Out - : T extends (value: any) => infer R + : T extends (value: unknown) => infer R ? R extends type.errors ? never : distill.Out diff --git a/packages/vite-plugin/package.json b/packages/vite-plugin/package.json index 456516e4a..ef8bd4a49 100644 --- a/packages/vite-plugin/package.json +++ b/packages/vite-plugin/package.json @@ -21,7 +21,8 @@ "typescript": "catalog:", "vite": "7.3.1", "vite-tsconfig-paths": "catalog:", - "vitest": "catalog:" + "vitest": "catalog:", + "zod": "catalog:" }, "peerDependencies": { "arktype": "^2.1.22", diff --git a/packages/vite-plugin/src/__fixtures__/standard-schema/config.ts b/packages/vite-plugin/src/__fixtures__/standard-schema/config.ts index dec1b3902..2f1811a77 100644 --- a/packages/vite-plugin/src/__fixtures__/standard-schema/config.ts +++ b/packages/vite-plugin/src/__fixtures__/standard-schema/config.ts @@ -1,12 +1,5 @@ +import { z } from "zod"; + export const Env = { - VITE_ZOD_VAR: { - "~standard": { - version: 1, - validate(value: any) { - return typeof value === "string" && value.length >= 5 - ? { value } - : { issues: [{ message: "min length 5" }] }; - }, - }, - }, + VITE_ZOD_VAR: z.string().min(5), }; diff --git a/packages/vite-plugin/src/index.test.ts b/packages/vite-plugin/src/index.test.ts index 6f295beda..4c8ec6977 100644 --- a/packages/vite-plugin/src/index.test.ts +++ b/packages/vite-plugin/src/index.test.ts @@ -2,6 +2,8 @@ import { readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; import * as vite from "vite"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { z } from "zod"; +import arkenvPlugin from "./index"; // Mock the arkenv module to capture calls // Mock the arkenv module with a spy that calls the real implementation by default @@ -23,8 +25,6 @@ vi.mock("vite", async (importActual) => { }; }); -import arkenvPlugin from "./index.js"; - const fixturesDir = join(__dirname, "__fixtures__"); // Get the mocked functions @@ -618,16 +618,10 @@ describe("Plugin Unit Tests", () => { ); }); - it("should work with a Standard Schema validator", async () => { + it("should work with a Zod Standard Schema validator", async () => { vi.stubEnv("VITE_SS_VAR", "valid-value"); - const mockValidator = { - "~standard": { - version: 1, - validate: (val: any) => ({ value: val }), - }, - }; - const schema: any = { - VITE_SS_VAR: mockValidator, + const schema = { + VITE_SS_VAR: z.string().min(5), }; // Note: We use the real implementation for this test diff --git a/packages/vite-plugin/src/index.test.ts.tmp b/packages/vite-plugin/src/index.test.ts.tmp new file mode 100644 index 000000000..a465bbd98 --- /dev/null +++ b/packages/vite-plugin/src/index.test.ts.tmp @@ -0,0 +1,33 @@ +import { readdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import * as vite from "vite"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { z } from "zod"; +import arkenvPlugin from "./index"; + +// Mock the arkenv module to capture calls +// Mock the arkenv module with a spy that calls the real implementation by default +vi.mock("arkenv", async (importActual) => { + const actual = await importActual(); + return { + ...actual, + default: vi.fn(actual.default), + createEnv: vi.fn(actual.createEnv), + }; +}); + +// Mock the vite module to capture loadEnv calls +vi.mock("vite", async (importActual) => { + const actual = await importActual(); + return { + ...actual, + loadEnv: vi.fn(actual.loadEnv), + }; +}); + +const fixturesDir = join(__dirname, "__fixtures__"); + +// Get the mocked functions +const { createEnv: mockCreateEnv } = vi.mocked(await import("arkenv")); + +const mockLoadEnv = vi.mocked(vite.loadEnv); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d021fc2a9..485b28cc5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -695,6 +695,9 @@ importers: vitest: specifier: 'catalog:' version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) + zod: + specifier: 'catalog:' + version: 4.3.5 tooling/playwright-www: dependencies: From 799c4e322c3874af36146c052e84305bb2d98e3a Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Sat, 10 Jan 2026 00:51:24 +0500 Subject: [PATCH 069/117] refactor(arkenv): improve ArkType validator detection - Check for ArkType's brand/symbol - Fall back to duck typing for detection - More robust ArkType compilation check --- packages/arkenv/src/create-env.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index cc85cf561..3e7fe9b18 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -49,12 +49,18 @@ function detectValidatorType(def: unknown) { } const d = def as Record; + // Check for ArkType's brand/symbol if available, fall back to duck typing + const hasArktypeBrand = + typeof d.infer === "function" || + (typeof d.t === "object" && d.t !== null && "infer" in d.t); + const isArkCompiled = - ("t" in d || "allows" in d) && - ("infer" in d || - "toJsonSchema" in d || - "expression" in d || - ("array" in d && "or" in d && "pipe" in d)); + hasArktypeBrand || + (("t" in d || "allows" in d) && + ("infer" in d || + "toJsonSchema" in d || + "expression" in d || + ("array" in d && "or" in d && "pipe" in d))); return { isStandard, isArkCompiled }; } From 36e3a9f23e4163909466df9682087985d7316753 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Sat, 10 Jan 2026 01:06:16 +0500 Subject: [PATCH 070/117] refactor(scope): lazy load ArkType dependency - Use proxy for arktype's '$' - Lazy load arktype on first use - Throw error if arktype not installed --- packages/arkenv/src/create-env.ts | 7 ++-- packages/arkenv/src/type.ts | 8 +---- packages/internal/scope/package.json | 1 + packages/internal/scope/src/index.ts | 49 +++++++++++++++++++++------ packages/internal/scope/tsconfig.json | 3 +- pnpm-lock.yaml | 3 ++ 6 files changed, 49 insertions(+), 22 deletions(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 3e7fe9b18..1fd6d962a 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -1,5 +1,5 @@ import { createRequire } from "node:module"; -import type { $ } from "@repo/scope"; +import { $ } from "@repo/scope"; import type { EnvSchemaWithType, InferType, SchemaShape } from "@repo/types"; import type { StandardSchemaV1 } from "@standard-schema/spec"; import type { type as at, Type } from "arktype"; @@ -71,12 +71,13 @@ function validateArkType( env: Record, ): { success: true; value: unknown } | { success: false; issues: EnvIssue[] } { try { - const { $ } = require("@repo/scope"); const { type } = require("arktype"); const { isArkCompiled: isCompiledType } = detectValidatorType(def); - let schema = isCompiledType ? (def as Type) : ($.type(def) as Type); + let schema = isCompiledType + ? (def as Type) + : ($.type(def as any) as unknown as Type); // Apply the `onUndeclaredKey` option, defaulting to "delete" for arkenv compatibility schema = schema.onUndeclaredKey(config.onUndeclaredKey ?? "delete"); diff --git a/packages/arkenv/src/type.ts b/packages/arkenv/src/type.ts index 10fd5ef8b..bd5d747a3 100644 --- a/packages/arkenv/src/type.ts +++ b/packages/arkenv/src/type.ts @@ -1,8 +1,4 @@ -import { createRequire } from "node:module"; - -const require = createRequire(import.meta.url); - -import type { $ } from "@repo/scope"; +import { $ } from "@repo/scope"; /** * `type` is a typesafe environment variable validator, an alias for `arktype`'s `type`. @@ -10,11 +6,9 @@ import type { $ } from "@repo/scope"; */ export const type: typeof $.type = new Proxy((() => {}) as any, { get(_, prop) { - const { $ } = require("@repo/scope"); return Reflect.get($.type, prop); }, apply(_, thisArg, args) { - const { $ } = require("@repo/scope"); return Reflect.apply($.type, thisArg === type ? $.type : thisArg, args); }, }); diff --git a/packages/internal/scope/package.json b/packages/internal/scope/package.json index 4bd2d4575..5232014c8 100644 --- a/packages/internal/scope/package.json +++ b/packages/internal/scope/package.json @@ -37,6 +37,7 @@ "@repo/keywords": "workspace:*" }, "devDependencies": { + "@types/node": "catalog:", "arktype": "catalog:", "tsdown": "catalog:", "typescript": "catalog:" diff --git a/packages/internal/scope/src/index.ts b/packages/internal/scope/src/index.ts index 031d92df7..d82aaf960 100644 --- a/packages/internal/scope/src/index.ts +++ b/packages/internal/scope/src/index.ts @@ -1,20 +1,47 @@ +import { createRequire } from "node:module"; import { host, port } from "@repo/keywords"; -import { scope, type } from "arktype"; +import type { scope as ArkScope, type as ArkType } from "arktype"; + +const require = createRequire(import.meta.url); + +let _$: any; /** * The root scope for the ArkEnv library, * containing extensions to the ArkType scopes with ArkEnv-specific types * like `string.host` and `number.port`. + * + * This is a lazy-loaded proxy to allow ArkType to be an optional dependency. */ -export const $ = scope({ - string: type.module({ - ...type.keywords.string, - host, - }), - number: type.module({ - ...type.keywords.number, - port, - }), -}); +export const $: ReturnType = new Proxy( + {}, + { + get(_, prop) { + if (!_$) { + try { + const { scope, type } = require("arktype") as { + scope: typeof ArkScope; + type: typeof ArkType; + }; + _$ = scope({ + string: type.module({ + ...type.keywords.string, + host, + }), + number: type.module({ + ...type.keywords.number, + port, + }), + }); + } catch { + throw new Error( + "ArkType is required when using `type()` or ArkType-specific schemas. Please install `arktype`.", + ); + } + } + return (_$ as any)[prop]; + }, + }, +) as any; export type $ = (typeof $)["t"]; diff --git a/packages/internal/scope/tsconfig.json b/packages/internal/scope/tsconfig.json index 56793c550..93caed97b 100644 --- a/packages/internal/scope/tsconfig.json +++ b/packages/internal/scope/tsconfig.json @@ -13,7 +13,8 @@ "resolveJsonModule": true, "rootDir": "src", "declaration": true, - "declarationMap": true + "declarationMap": true, + "types": ["node"] }, "include": ["src"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 485b28cc5..caeb971d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -634,6 +634,9 @@ importers: specifier: workspace:* version: link:../keywords devDependencies: + '@types/node': + specifier: 'catalog:' + version: 24.10.4 arktype: specifier: 'catalog:' version: 2.1.29 From 989160fd0eb0d4947cb08109b07f54e1341b6a87 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Sat, 10 Jan 2026 01:15:35 +0500 Subject: [PATCH 071/117] feat(scope): define ArkEnvScope and improve type safety - Introduced `scope-def.ts` for type definitions - Exported `ArkEnvScope` type - Updated `$` type to `ArkEnvScope` --- packages/internal/scope/src/index.ts | 8 ++++++-- packages/internal/scope/src/scope-def.ts | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 packages/internal/scope/src/scope-def.ts diff --git a/packages/internal/scope/src/index.ts b/packages/internal/scope/src/index.ts index d82aaf960..2647d290d 100644 --- a/packages/internal/scope/src/index.ts +++ b/packages/internal/scope/src/index.ts @@ -13,7 +13,11 @@ let _$: any; * * This is a lazy-loaded proxy to allow ArkType to be an optional dependency. */ -export const $: ReturnType = new Proxy( +import type { ArkEnvScope } from "./scope-def"; + +export type { ArkEnvScope } from "./scope-def"; + +export const $: ArkEnvScope = new Proxy( {}, { get(_, prop) { @@ -44,4 +48,4 @@ export const $: ReturnType = new Proxy( }, ) as any; -export type $ = (typeof $)["t"]; +export type $ = ArkEnvScope["t"]; diff --git a/packages/internal/scope/src/scope-def.ts b/packages/internal/scope/src/scope-def.ts new file mode 100644 index 000000000..e03006f49 --- /dev/null +++ b/packages/internal/scope/src/scope-def.ts @@ -0,0 +1,19 @@ +import { host, port } from "@repo/keywords"; +import { scope, type } from "arktype"; + +/** + * Definition of the scope for type inference purposes only. + * This file is never imported at runtime to keep arktype optional. + */ +const $ = scope({ + string: type.module({ + ...type.keywords.string, + host, + }), + number: type.module({ + ...type.keywords.number, + port, + }), +}); + +export type ArkEnvScope = typeof $; From 5c6e05d87b0c8006d3d137ab3e4ff2bd9cf02509 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Sat, 10 Jan 2026 01:24:48 +0500 Subject: [PATCH 072/117] refactor(arkenv): Improve validator type detection - Simplify ArkType brand check - Remove unnecessary infer function check - Maintain existing duck typing fallback --- packages/arkenv/src/create-env.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 1fd6d962a..928f77944 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -51,8 +51,7 @@ function detectValidatorType(def: unknown) { const d = def as Record; // Check for ArkType's brand/symbol if available, fall back to duck typing const hasArktypeBrand = - typeof d.infer === "function" || - (typeof d.t === "object" && d.t !== null && "infer" in d.t); + "infer" in d || (typeof d.t === "object" && d.t !== null && "infer" in d.t); const isArkCompiled = hasArktypeBrand || From fbccf6807468545aad8bf4a821531d627fb01a9b Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 10 Jan 2026 04:26:02 +0000 Subject: [PATCH 073/117] [autofix.ci] apply automated fixes --- packages/arkenv/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/arkenv/package.json b/packages/arkenv/package.json index e01dd889c..0fe6d7f95 100644 --- a/packages/arkenv/package.json +++ b/packages/arkenv/package.json @@ -70,7 +70,7 @@ "import": "*", "ignore": [ "arktype", - "@repo/scope", + "@repo/scope", "node:module" ] } From 7b0afc642c05748bf4f11bb1a31437554cc7b876 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Mon, 12 Jan 2026 22:58:57 +0500 Subject: [PATCH 074/117] refactor(deps): internalize Standard Schema types` --- packages/arkenv/package.json | 1 - packages/arkenv/src/create-env.ts | 8 +- packages/internal/types/package.json | 1 - packages/internal/types/src/index.ts | 1 + packages/internal/types/src/infer-type.ts | 2 +- packages/internal/types/src/schema.ts | 2 +- .../internal/types/src/standard-schema.ts | 175 ++++++++++++++++++ pnpm-lock.yaml | 6 - 8 files changed, 184 insertions(+), 12 deletions(-) create mode 100644 packages/internal/types/src/standard-schema.ts diff --git a/packages/arkenv/package.json b/packages/arkenv/package.json index b995e2de4..e61d79e89 100644 --- a/packages/arkenv/package.json +++ b/packages/arkenv/package.json @@ -45,7 +45,6 @@ "@repo/types": "workspace:*", "@size-limit/esbuild-why": "catalog:", "@size-limit/preset-small-lib": "catalog:", - "@standard-schema/spec": "1.0.0", "@types/node": "catalog:", "arktype": "catalog:", "rimraf": "catalog:", diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 928f77944..42cbd35b2 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -1,7 +1,11 @@ import { createRequire } from "node:module"; import { $ } from "@repo/scope"; -import type { EnvSchemaWithType, InferType, SchemaShape } from "@repo/types"; -import type { StandardSchemaV1 } from "@standard-schema/spec"; +import type { + EnvSchemaWithType, + InferType, + SchemaShape, + StandardSchemaV1, +} from "@repo/types"; import type { type as at, Type } from "arktype"; import { ArkEnvError, type EnvIssue } from "./errors"; import { coerce } from "./utils/coerce"; diff --git a/packages/internal/types/package.json b/packages/internal/types/package.json index 7d8db6ed4..bce1ae3cc 100644 --- a/packages/internal/types/package.json +++ b/packages/internal/types/package.json @@ -18,7 +18,6 @@ }, "devDependencies": { "@repo/scope": "workspace:*", - "@standard-schema/spec": "1.0.0", "arktype": "catalog:", "typescript": "catalog:" }, diff --git a/packages/internal/types/src/index.ts b/packages/internal/types/src/index.ts index c405b87d8..8e5de3843 100644 --- a/packages/internal/types/src/index.ts +++ b/packages/internal/types/src/index.ts @@ -11,3 +11,4 @@ export type * from "./infer-type"; // Also includes ArkType types export * from "./schema"; +export * from "./standard-schema"; diff --git a/packages/internal/types/src/infer-type.ts b/packages/internal/types/src/infer-type.ts index 7e8c6aa96..c6bb10cec 100644 --- a/packages/internal/types/src/infer-type.ts +++ b/packages/internal/types/src/infer-type.ts @@ -1,6 +1,6 @@ import type { $ } from "@repo/scope"; -import type { StandardSchemaV1 } from "@standard-schema/spec"; import type { distill, Type, type } from "arktype"; +import type { StandardSchemaV1 } from "./standard-schema"; /** * Extract the inferred type from an ArkType type definition or a Standard Schema validator. diff --git a/packages/internal/types/src/schema.ts b/packages/internal/types/src/schema.ts index a969e785b..a8ca5f8fa 100644 --- a/packages/internal/types/src/schema.ts +++ b/packages/internal/types/src/schema.ts @@ -1,6 +1,6 @@ import type { $ } from "@repo/scope"; -import type { StandardSchemaV1 } from "@standard-schema/spec"; import { type Type, type } from "arktype"; +import type { StandardSchemaV1 } from "./standard-schema"; export const SchemaShape = type({ "[string]": "unknown" }); export type SchemaShape = typeof SchemaShape.infer; diff --git a/packages/internal/types/src/standard-schema.ts b/packages/internal/types/src/standard-schema.ts new file mode 100644 index 000000000..746a03791 --- /dev/null +++ b/packages/internal/types/src/standard-schema.ts @@ -0,0 +1,175 @@ +// ######################### +// ### Standard Typed ### +// ######################### + +/** The Standard Typed interface. This is a base type extended by other specs. */ +export interface StandardTypedV1 { + /** The Standard properties. */ + readonly "~standard": StandardTypedV1.Props; +} + +export declare namespace StandardTypedV1 { + /** The Standard Typed properties interface. */ + export interface Props { + /** The version number of the standard. */ + readonly version: 1; + /** The vendor name of the schema library. */ + readonly vendor: string; + /** Inferred types associated with the schema. */ + readonly types?: Types | undefined; + } + + /** The Standard Typed types interface. */ + export interface Types { + /** The input type of the schema. */ + readonly input: Input; + /** The output type of the schema. */ + readonly output: Output; + } + + /** Infers the input type of a Standard Typed. */ + export type InferInput = NonNullable< + Schema["~standard"]["types"] + >["input"]; + + /** Infers the output type of a Standard Typed. */ + export type InferOutput = NonNullable< + Schema["~standard"]["types"] + >["output"]; +} + +// ########################## +// ### Standard Schema ### +// ########################## + +/** The Standard Schema interface. */ +export interface StandardSchemaV1 { + /** The Standard Schema properties. */ + readonly "~standard": StandardSchemaV1.Props; +} + +export declare namespace StandardSchemaV1 { + /** The Standard Schema properties interface. */ + export interface Props + extends StandardTypedV1.Props { + /** Validates unknown input values. */ + readonly validate: ( + value: unknown, + options?: StandardSchemaV1.Options | undefined, + ) => Result | Promise>; + } + + /** The result interface of the validate function. */ + export type Result = SuccessResult | FailureResult; + + /** The result interface if validation succeeds. */ + export interface SuccessResult { + /** The typed output value. */ + readonly value: Output; + /** A falsy value for `issues` indicates success. */ + readonly issues?: undefined; + } + + export interface Options { + /** Explicit support for additional vendor-specific parameters, if needed. */ + readonly libraryOptions?: Record | undefined; + } + + /** The result interface if validation fails. */ + export interface FailureResult { + /** The issues of failed validation. */ + readonly issues: ReadonlyArray; + } + + /** The issue interface of the failure output. */ + export interface Issue { + /** The error message of the issue. */ + readonly message: string; + /** The path of the issue, if any. */ + readonly path?: ReadonlyArray | undefined; + } + + /** The path segment interface of the issue. */ + export interface PathSegment { + /** The key representing a path segment. */ + readonly key: PropertyKey; + } + + /** The Standard types interface. */ + export interface Types + extends StandardTypedV1.Types {} + + /** Infers the input type of a Standard. */ + export type InferInput = + StandardTypedV1.InferInput; + + /** Infers the output type of a Standard. */ + export type InferOutput = + StandardTypedV1.InferOutput; +} + +// ############################### +// ### Standard JSON Schema ### +// ############################### + +/** The Standard JSON Schema interface. */ +export interface StandardJSONSchemaV1 { + /** The Standard JSON Schema properties. */ + readonly "~standard": StandardJSONSchemaV1.Props; +} + +export declare namespace StandardJSONSchemaV1 { + /** The Standard JSON Schema properties interface. */ + export interface Props + extends StandardTypedV1.Props { + /** Methods for generating the input/output JSON Schema. */ + readonly jsonSchema: StandardJSONSchemaV1.Converter; + } + + /** The Standard JSON Schema converter interface. */ + export interface Converter { + /** Converts the input type to JSON Schema. May throw if conversion is not supported. */ + readonly input: ( + options: StandardJSONSchemaV1.Options, + ) => Record; + /** Converts the output type to JSON Schema. May throw if conversion is not supported. */ + readonly output: ( + options: StandardJSONSchemaV1.Options, + ) => Record; + } + + /** + * The target version of the generated JSON Schema. + * + * It is *strongly recommended* that implementers support `"draft-2020-12"` and `"draft-07"`, as they are both in wide use. All other targets can be implemented on a best-effort basis. Libraries should throw if they don't support a specified target. + * + * The `"openapi-3.0"` target is intended as a standardized specifier for OpenAPI 3.0 which is a superset of JSON Schema `"draft-04"`. + */ + export type Target = + | "draft-2020-12" + | "draft-07" + | "openapi-3.0" + // Accepts any string for future targets while preserving autocomplete + | ({} & string); + + /** The options for the input/output methods. */ + export interface Options { + /** Specifies the target version of the generated JSON Schema. Support for all versions is on a best-effort basis. If a given version is not supported, the library should throw. */ + readonly target: Target; + + /** Explicit support for additional vendor-specific parameters, if needed. */ + readonly libraryOptions?: Record | undefined; + } + + /** The Standard types interface. */ + export interface Types + extends StandardTypedV1.Types {} + + /** Infers the input type of a Standard. */ + export type InferInput = + StandardTypedV1.InferInput; + + /** Infers the output type of a Standard. */ + export type InferOutput = + StandardTypedV1.InferOutput; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index caeb971d4..ac7cf8aa7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -548,9 +548,6 @@ importers: '@size-limit/preset-small-lib': specifier: 'catalog:' version: 12.0.0(size-limit@12.0.0(jiti@2.6.1)) - '@standard-schema/spec': - specifier: 1.0.0 - version: 1.0.0 '@types/node': specifier: 'catalog:' version: 24.10.4 @@ -652,9 +649,6 @@ importers: '@repo/scope': specifier: workspace:* version: link:../scope - '@standard-schema/spec': - specifier: 1.0.0 - version: 1.0.0 arktype: specifier: 'catalog:' version: 2.1.29 From 0ae6998501334d4dd295cb16f0c9bc43416c1c38 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Mon, 12 Jan 2026 22:59:13 +0500 Subject: [PATCH 075/117] docs(types): add standard-schema spec link --- packages/internal/types/src/standard-schema.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/internal/types/src/standard-schema.ts b/packages/internal/types/src/standard-schema.ts index 746a03791..ca0eafb19 100644 --- a/packages/internal/types/src/standard-schema.ts +++ b/packages/internal/types/src/standard-schema.ts @@ -1,3 +1,7 @@ +/** + * @see https://github.com/standard-schema/standard-schema/tree/3130ce43fdd848d9ab49dbb0458d04f18459961c/packages/spec + */ + // ######################### // ### Standard Typed ### // ######################### From da6b3f8f13a6b5abbee77488dafc59942511c727 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Mon, 12 Jan 2026 23:00:17 +0500 Subject: [PATCH 076/117] style(lint): disable biome lint for copied types --- packages/internal/types/src/standard-schema.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/internal/types/src/standard-schema.ts b/packages/internal/types/src/standard-schema.ts index ca0eafb19..051465040 100644 --- a/packages/internal/types/src/standard-schema.ts +++ b/packages/internal/types/src/standard-schema.ts @@ -2,6 +2,8 @@ * @see https://github.com/standard-schema/standard-schema/tree/3130ce43fdd848d9ab49dbb0458d04f18459961c/packages/spec */ +// biome-ignore-all lint/style/useConsistentTypeDefinitions: this file is copied from the standard-schema repo + // ######################### // ### Standard Typed ### // ######################### From c928e18df4daec8dcc7b507f3de186e6fa2ab2aa Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Mon, 12 Jan 2026 23:34:19 +0500 Subject: [PATCH 077/117] feat(arkenv): add no-arktype playground - New playground for string schemas - Does not require ArkType dependency - Clarify coerce and arrayFormat JSDoc --- apps/playgrounds/no-arktype/package.json | 24 +++++++++++++ apps/playgrounds/no-arktype/src/index.ts | 19 ++++++++++ apps/playgrounds/no-arktype/tsconfig.json | 44 +++++++++++++++++++++++ arkenv.code-workspace | 4 +++ packages/arkenv/src/create-env.ts | 11 ++++-- pnpm-lock.yaml | 13 +++++++ 6 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 apps/playgrounds/no-arktype/package.json create mode 100644 apps/playgrounds/no-arktype/src/index.ts create mode 100644 apps/playgrounds/no-arktype/tsconfig.json diff --git a/apps/playgrounds/no-arktype/package.json b/apps/playgrounds/no-arktype/package.json new file mode 100644 index 000000000..5ba807288 --- /dev/null +++ b/apps/playgrounds/no-arktype/package.json @@ -0,0 +1,24 @@ +{ + "name": "no-arktype-playground", + "version": "1.0.0", + "description": "", + "main": "index.js", + "type": "module", + "scripts": { + "dev": "tsx watch --env-file .env src/index.ts", + "start": "tsx --env-file .env src", + "build": "tsc", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "packageManager": "pnpm@10.27.0", + "devDependencies": { + "tsx": "catalog:" + }, + "dependencies": { + "zod": "catalog:", + "arkenv": "workspace:*" + } +} diff --git a/apps/playgrounds/no-arktype/src/index.ts b/apps/playgrounds/no-arktype/src/index.ts new file mode 100644 index 000000000..7be7e468c --- /dev/null +++ b/apps/playgrounds/no-arktype/src/index.ts @@ -0,0 +1,19 @@ +import { createEnv } from "arkenv"; +import z from "zod"; + +// const env = createEnv({ +// TEST_VALUE: z.url(), +// PORT: z.coerce.number(), +// HOST: z.literal("localhost").or(z.url()), +// }); + +const env = createEnv({ + TEST_VALUE: "string.url", + PORT: "number", + HOST: "'localhost' | string.url", +}); + +console.log(`Value: ${String(env.TEST_VALUE)}`); +console.log(`Type: ${typeof env.TEST_VALUE}`); +console.log("---"); +console.log(env); diff --git a/apps/playgrounds/no-arktype/tsconfig.json b/apps/playgrounds/no-arktype/tsconfig.json new file mode 100644 index 000000000..ba3f54680 --- /dev/null +++ b/apps/playgrounds/no-arktype/tsconfig.json @@ -0,0 +1,44 @@ +{ + // Visit https://aka.ms/tsconfig to read more about this file + "compilerOptions": { + // File Layout + "rootDir": "./src", + "outDir": "./dist", + + // Environment Settings + // See also https://aka.ms/tsconfig/module + "module": "nodenext", + "target": "esnext", + "types": [], + // For nodejs: + // "lib": ["esnext"], + // "types": ["node"], + // and npm install -D @types/node + + // Other Outputs + "sourceMap": true, + "declaration": true, + "declarationMap": true, + + // Stricter Typechecking Options + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + + // Style Options + // "noImplicitReturns": true, + // "noImplicitOverride": true, + // "noUnusedLocals": true, + // "noUnusedParameters": true, + // "noFallthroughCasesInSwitch": true, + // "noPropertyAccessFromIndexSignature": true, + + // Recommended Options + "strict": true, + "jsx": "react-jsx", + "verbatimModuleSyntax": true, + "isolatedModules": true, + "noUncheckedSideEffectImports": true, + "moduleDetection": "force", + "skipLibCheck": true + } +} diff --git a/arkenv.code-workspace b/arkenv.code-workspace index d0fd1dff2..f436d2516 100644 --- a/arkenv.code-workspace +++ b/arkenv.code-workspace @@ -93,6 +93,10 @@ "path": "apps/playgrounds/standard-schema", "name": " standard-schema-playground" }, + { + "path": "apps/playgrounds/no-arktype", + "name": " no-arktype-playground" + }, { "path": "apps/playgrounds/js", "name": " js-playground" diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 42cbd35b2..2fd90c264 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -23,7 +23,10 @@ export type ArkEnvConfig = { */ env?: Record; /** - * Whether to coerce environment variables to their defined types. Defaults to `true` + * Whether to coerce environment variables to their defined types. + * + * @default true + * Note: Only takes effect when using ArkType. */ coerce?: boolean; /** @@ -35,9 +38,11 @@ export type ArkEnvConfig = { */ onUndeclaredKey?: "delete" | "ignore" | "reject"; /** - * For arrays, specifies how to parse the string value. + * For arrays, specify how to coerce the string value. * - `comma`: Split by commas and trim whitespace. - * - `json`: Strings are parsed as JSON. + * - `json`: Strings are coerced as JSON. + * + * Note: only takes effect when `coerce` is enabled. * @default "comma" */ arrayFormat?: "comma" | "json"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac7cf8aa7..27824080d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -181,6 +181,19 @@ importers: specifier: 'catalog:' version: 2.1.29 + apps/playgrounds/no-arktype: + dependencies: + arkenv: + specifier: workspace:* + version: link:../../../packages/arkenv + zod: + specifier: 'catalog:' + version: 4.3.5 + devDependencies: + tsx: + specifier: 'catalog:' + version: 4.21.0 + apps/playgrounds/node: dependencies: arkenv: From 869dff1c52860cbcccd26d50f7d491d352364ea4 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Mon, 12 Jan 2026 23:45:06 +0500 Subject: [PATCH 078/117] feat(examples): add without-arktype example - Add new `without-arktype` example - Demonstrate using ArkEnv with Zod - Update workspace config and README --- arkenv.code-workspace | 8 +- examples/README.md | 3 +- examples/without-arktype/package-lock.json | 588 ++++++++++++++++++ .../without-arktype}/package.json | 11 +- .../without-arktype}/src/index.ts | 0 .../without-arktype}/tsconfig.json | 0 pnpm-lock.yaml | 13 - 7 files changed, 601 insertions(+), 22 deletions(-) create mode 100644 examples/without-arktype/package-lock.json rename {apps/playgrounds/no-arktype => examples/without-arktype}/package.json (72%) rename {apps/playgrounds/no-arktype => examples/without-arktype}/src/index.ts (100%) rename {apps/playgrounds/no-arktype => examples/without-arktype}/tsconfig.json (100%) diff --git a/arkenv.code-workspace b/arkenv.code-workspace index f436d2516..80b8d6810 100644 --- a/arkenv.code-workspace +++ b/arkenv.code-workspace @@ -93,10 +93,6 @@ "path": "apps/playgrounds/standard-schema", "name": " standard-schema-playground" }, - { - "path": "apps/playgrounds/no-arktype", - "name": " no-arktype-playground" - }, { "path": "apps/playgrounds/js", "name": " js-playground" @@ -133,6 +129,10 @@ "path": "examples/with-standard-schema", "name": " with-standard-schema" }, + { + "path": "examples/without-arktype", + "name": " without-arktype" + }, { "path": "examples/stackblitz", "name": " stackblitz" diff --git a/examples/README.md b/examples/README.md index 67759f68f..91a6e7274 100644 --- a/examples/README.md +++ b/examples/README.md @@ -8,10 +8,11 @@ This directory contains a collection of example projects that demonstrate variou | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | [basic](https://github.com/yamcodes/arkenv/tree/main/examples/basic) | Minimal example of _using ArkEnv in a [Node.js](https://nodejs.org/) app_ for learning the fundamentals. | | [with-standard-schema](https://github.com/yamcodes/arkenv/tree/main/examples/with-standard-schema) | Example of _mixing ArkType with [Standard Schema](https://standardschema.dev/) validators like [Zod](https://zod.dev/)_. | +| [without-arktype](https://github.com/yamcodes/arkenv/tree/main/examples/without-arktype) | Example of _using ArkEnv without ArkType_, using [Zod](https://zod.dev/) for validation. | | [with-bun](https://github.com/yamcodes/arkenv/tree/main/examples/with-bun) | Minimal example of _using ArkEnv in a [Bun](https://bun.sh/) app_. | | [with-bun-react](https://github.com/yamcodes/arkenv/tree/main/examples/with-bun-react) | Minimal example of _using ArkEnv in a [Bun + React](https://bun.com/docs/guides/ecosystem/react) full-stack app_. | | [with-vite-react](https://github.com/yamcodes/arkenv/tree/main/examples/with-vite-react) | Minimal example of _using ArkEnv in a [Vite](https://vite.dev/) + [React](https://react.dev/) app_. | -| [with-solid-start](https://github.com/yamcodes/arkenv/tree/main/examples/with-solid-start) | Minimal example of _using ArkEnv in a [SolidStart](https://start.solidjs.com) app_. | +| [with-solid-start](https://github.com/yamcodes/arkenv/tree/main/examples/with-solid-start) | Minimal example of _using ArkEnv in a [SolidStart](https://start.solidjs.com) app_. | > These examples are written in TypeScript, [the recommended way to work with ArkEnv](https://github.com/yamcodes/arkenv/blob/main/packages/arkenv/README.md#typescript-requirements). That said, ArkEnv works with plain JavaScript. See the [basic-js](https://github.com/yamcodes/arkenv/tree/main/examples/basic-js) example for details and tradeoffs. diff --git a/examples/without-arktype/package-lock.json b/examples/without-arktype/package-lock.json new file mode 100644 index 000000000..bc78e00b2 --- /dev/null +++ b/examples/without-arktype/package-lock.json @@ -0,0 +1,588 @@ +{ + "name": "no-arktype-playground", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "no-arktype-playground", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "arkenv": "https://pkg.pr.new/arkenv@704", + "zod": "^4.3.5" + }, + "devDependencies": { + "tsx": "^4.21.0" + }, + "engines": { + "node": "24" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/arkenv": { + "version": "0.8.3", + "resolved": "https://pkg.pr.new/arkenv@704", + "integrity": "sha512-zrar8aQgA7G+1lO6CKlRTOQI3brEYAiaoN/Tn//n5GtBBkPMus6GHZFuKKpULSC7wXkeAUUJeWGpGL9SbEZZFw==", + "license": "MIT", + "peerDependencies": { + "arktype": "^2.1.22" + }, + "peerDependenciesMeta": { + "arktype": { + "optional": true + } + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/zod": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/apps/playgrounds/no-arktype/package.json b/examples/without-arktype/package.json similarity index 72% rename from apps/playgrounds/no-arktype/package.json rename to examples/without-arktype/package.json index 5ba807288..aa86027e3 100644 --- a/apps/playgrounds/no-arktype/package.json +++ b/examples/without-arktype/package.json @@ -13,12 +13,15 @@ "keywords": [], "author": "", "license": "ISC", - "packageManager": "pnpm@10.27.0", + "engines": { + "node": "24" + }, + "packageManager": "npm@11.6.4", "devDependencies": { - "tsx": "catalog:" + "tsx": "^4.21.0" }, "dependencies": { - "zod": "catalog:", - "arkenv": "workspace:*" + "arkenv": "https://pkg.pr.new/arkenv@704", + "zod": "^4.3.5" } } diff --git a/apps/playgrounds/no-arktype/src/index.ts b/examples/without-arktype/src/index.ts similarity index 100% rename from apps/playgrounds/no-arktype/src/index.ts rename to examples/without-arktype/src/index.ts diff --git a/apps/playgrounds/no-arktype/tsconfig.json b/examples/without-arktype/tsconfig.json similarity index 100% rename from apps/playgrounds/no-arktype/tsconfig.json rename to examples/without-arktype/tsconfig.json diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 27824080d..ac7cf8aa7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -181,19 +181,6 @@ importers: specifier: 'catalog:' version: 2.1.29 - apps/playgrounds/no-arktype: - dependencies: - arkenv: - specifier: workspace:* - version: link:../../../packages/arkenv - zod: - specifier: 'catalog:' - version: 4.3.5 - devDependencies: - tsx: - specifier: 'catalog:' - version: 4.21.0 - apps/playgrounds/node: dependencies: arkenv: From a8f67ad5e7f00db297524311c5089692da1adf59 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Tue, 13 Jan 2026 01:05:03 +0500 Subject: [PATCH 079/117] feat: introduce lazy ArkType loading - Defer ArkType import until validator is used - Make ArkType an optional peer dependency - Refactor keywords to use lazyType from scope - --- packages/arkenv/tsdown.config.ts | 5 +- packages/internal/keywords/src/index.ts | 53 ++++++++-- packages/internal/keywords/tsdown.config.ts | 1 + packages/internal/scope/src/lazy-type.ts | 104 ++++++++++++++++++++ packages/internal/types/src/schema.ts | 9 +- 5 files changed, 158 insertions(+), 14 deletions(-) create mode 100644 packages/internal/scope/src/lazy-type.ts diff --git a/packages/arkenv/tsdown.config.ts b/packages/arkenv/tsdown.config.ts index 8fc08e8db..3400b041f 100644 --- a/packages/arkenv/tsdown.config.ts +++ b/packages/arkenv/tsdown.config.ts @@ -5,6 +5,9 @@ export default defineConfig({ minify: true, fixedExtension: false, dts: { - resolve: ["@repo/types"], + resolve: ["@repo/types", "@repo/scope", "@repo/keywords"], }, + external: ["arktype"], + // Force bundling of workspace packages + noExternal: ["@repo/scope", "@repo/keywords"], }); diff --git a/packages/internal/keywords/src/index.ts b/packages/internal/keywords/src/index.ts index 2b192a78d..87d90ce69 100644 --- a/packages/internal/keywords/src/index.ts +++ b/packages/internal/keywords/src/index.ts @@ -1,7 +1,7 @@ -import { type } from "arktype"; +import { lazyType as type } from "@repo/scope"; /** - * A loose numeric morph. + * A loose numeric morph function (internal use only). * * **In**: `unknown` * @@ -9,7 +9,7 @@ import { type } from "arktype"; * * Useful for coercion in unions where failing on non-numeric strings would block other branches. */ -export const maybeNumber = type("unknown").pipe((s) => { +export const maybeNumberFn = (s: unknown) => { if (typeof s === "number") return s; if (typeof s !== "string") return s; const trimmed = s.trim(); @@ -17,10 +17,21 @@ export const maybeNumber = type("unknown").pipe((s) => { if (trimmed === "NaN") return Number.NaN; const n = Number(trimmed); return Number.isNaN(n) ? s : n; -}); +}; /** - * A loose boolean morph. + * A loose numeric morph. + * + * **In**: `unknown` + * + * **Out**: A `number` if the input is a numeric string; otherwise the original input. + * + * Useful for coercion in unions where failing on non-numeric strings would block other branches. + */ +export const maybeNumber = type("unknown").pipe(maybeNumberFn); + +/** + * A loose boolean morph function (internal use only). * * **In**: `unknown` * @@ -28,11 +39,22 @@ export const maybeNumber = type("unknown").pipe((s) => { * * Useful for coercion in unions where failing on non-boolean strings would block other branches. */ -export const maybeBoolean = type("unknown").pipe((s) => { +export const maybeBooleanFn = (s: unknown) => { if (s === "true") return true; if (s === "false") return false; return s; -}); +}; + +/** + * A loose boolean morph. + * + * **In**: `unknown` + * + * **Out**: `true` for `"true"`, `false` for `"false"`; otherwise the original input. + * + * Useful for coercion in unions where failing on non-boolean strings would block other branches. + */ +export const maybeBoolean = type("unknown").pipe(maybeBooleanFn); /** * A `number` integer between 0 and 65535. @@ -45,7 +67,7 @@ export const port = type("0 <= number.integer <= 65535"); export const host = type("string.ip | 'localhost'"); /** - * A loose JSON morph. + * A loose JSON morph function (internal use only). * * **In**: `unknown` * @@ -53,7 +75,7 @@ export const host = type("string.ip | 'localhost'"); * * Useful for coercion in unions where failing on non-JSON strings would block other branches. */ -export const maybeJson = type("unknown").pipe((s) => { +export const maybeJsonFn = (s: unknown) => { if (typeof s !== "string") return s; const trimmed = s.trim(); // Only attempt to parse if it looks like JSON (starts with { or [) @@ -63,4 +85,15 @@ export const maybeJson = type("unknown").pipe((s) => { } catch { return s; } -}); +}; + +/** + * A loose JSON morph. + * + * **In**: `unknown` + * + * **Out**: A parsed JSON object if the input is a valid JSON string; otherwise the original input. + * + * Useful for coercion in unions where failing on non-JSON strings would block other branches. + */ +export const maybeJson = type("unknown").pipe(maybeJsonFn); diff --git a/packages/internal/keywords/tsdown.config.ts b/packages/internal/keywords/tsdown.config.ts index 8fc08e8db..f99459d2f 100644 --- a/packages/internal/keywords/tsdown.config.ts +++ b/packages/internal/keywords/tsdown.config.ts @@ -7,4 +7,5 @@ export default defineConfig({ dts: { resolve: ["@repo/types"], }, + external: ["arktype"], }); diff --git a/packages/internal/scope/src/lazy-type.ts b/packages/internal/scope/src/lazy-type.ts new file mode 100644 index 000000000..15ec9c902 --- /dev/null +++ b/packages/internal/scope/src/lazy-type.ts @@ -0,0 +1,104 @@ +import { createRequire } from "node:module"; +import type { type as ArkType } from "arktype"; + +const require = createRequire(import.meta.url); + +/** + * A lazy proxy that defers loading ArkType until it is actually needed. + * This allows internal packages to use ArkType syntax without creating + * static import dependencies that would break when ArkType is not installed. + */ + +type LazyTypeProxy = { + def: unknown; + morphs: Array<(value: unknown) => unknown>; + _realized?: any; +}; + +/** + * Create a lazy proxy for an ArkType definition. + * The proxy intercepts property access and method calls, only loading + * ArkType when the validator is actually used or inspected. + */ +function createLazyProxy(state: LazyTypeProxy): any { + return new Proxy( + {}, + { + get(_, prop) { + // Handle .pipe() - chain morphs without realizing the type yet + if (prop === "pipe") { + return (morph: (value: unknown) => unknown) => { + state.morphs.push(morph); + return createLazyProxy(state); + }; + } + + // For any other property access, we need to realize the type + return realizeType(state)[prop]; + }, + apply(_, __, args) { + // If the proxy itself is called as a validator + const realized = realizeType(state); + return realized(...args); + }, + }, + ); +} + +/** + * Realize the actual ArkType type from the lazy proxy state. + * This is where we actually require("arktype") and build the type. + */ +function realizeType(state: LazyTypeProxy): any { + if (state._realized) { + return state._realized; + } + + let at: typeof import("arktype"); + try { + at = require("arktype"); + } catch (e: unknown) { + if ( + e instanceof Error && + "code" in e && + e.code === "MODULE_NOT_FOUND" && + e.message.includes("'arktype'") + ) { + throw new Error( + "ArkType is required when using ArkType-specific schemas (string definitions or type() calls). " + + "Please install 'arktype' as a dependency, or use Standard Schema validators like Zod instead.", + ); + } + throw e; + } + + const { type } = at; + + // Build the type from the definition + let realized = type(state.def as any); + + // Apply any chained morphs + for (const morph of state.morphs) { + realized = realized.pipe(morph); + } + + state._realized = realized; + return realized; +} + +/** + * The lazy type function that mimics ArkType's `type()` API. + * This is typed as ArkType's type function for full DX compatibility, + * but returns a lazy proxy that defers loading ArkType until needed. + */ +export const lazyType = new Proxy(() => {}, { + apply(_, __, [def]) { + return createLazyProxy({ def, morphs: [] }); + }, + get(_, prop) { + // Forward static properties from the real type function + // This ensures things like type.keywords work correctly + const at = require("arktype"); + return at.type[prop]; + }, +}) as typeof ArkType; diff --git a/packages/internal/types/src/schema.ts b/packages/internal/types/src/schema.ts index a8ca5f8fa..28f9226a3 100644 --- a/packages/internal/types/src/schema.ts +++ b/packages/internal/types/src/schema.ts @@ -1,9 +1,12 @@ import type { $ } from "@repo/scope"; -import { type Type, type } from "arktype"; +import type { Type } from "arktype"; import type { StandardSchemaV1 } from "./standard-schema"; -export const SchemaShape = type({ "[string]": "unknown" }); -export type SchemaShape = typeof SchemaShape.infer; +/** + * A schema shape representing a mapping of string keys to unknown values. + * This is a pure TypeScript type and does not require ArkType at runtime. + */ +export type SchemaShape = Record; /** * A schema definition that is either an ArkType Type or a Standard Schema validator. From b201bcc3d2b16885ec80fa820dadc427b4282335 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Tue, 13 Jan 2026 01:47:34 +0500 Subject: [PATCH 080/117] feat(arkenv): add standard schema validation without arktype - Implement validation for standard schema definitions - Allow `createEnv` to use standard schemas directly - Lazily load Ark --- examples/without-arktype/package-lock.json | 934 ++++++++++---------- examples/without-arktype/package.json | 13 +- packages/arkenv/src/create-env.ts | 92 +- packages/arkenv/src/utils/coerce.ts | 24 +- packages/internal/keywords/package.json | 3 + packages/internal/keywords/tsdown.config.ts | 1 + packages/internal/scope/package.json | 4 +- packages/internal/scope/src/index.ts | 7 +- pnpm-lock.yaml | 15 +- 9 files changed, 569 insertions(+), 524 deletions(-) diff --git a/examples/without-arktype/package-lock.json b/examples/without-arktype/package-lock.json index bc78e00b2..31f97854d 100644 --- a/examples/without-arktype/package-lock.json +++ b/examples/without-arktype/package-lock.json @@ -9,7 +9,8 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "arkenv": "https://pkg.pr.new/arkenv@704", + "arkenv": "file:../../packages/arkenv/arkenv-0.8.3.tgz", + "arktype": "^2.1.29", "zod": "^4.3.5" }, "devDependencies": { @@ -19,452 +20,487 @@ "node": "24" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", - "cpu": [ - "x64" - ], - "dev": true, + "../../node_modules/.pnpm/@size-limit+esbuild-why@12.0.0_size-limit@12.0.0_jiti@2.6.1_/node_modules/@size-limit/esbuild-why": { + "version": "12.0.0", + "extraneous": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "esbuild-visualizer": "^0.7.0", + "open": "^11.0.0" + }, + "devDependencies": { + "@size-limit/esbuild": "workspace:*", + "@size-limit/file": "workspace:*", + "esbuild": "^0.27.0", + "redux": "^4.2.1", + "size-limit": "workspace:*" + }, "engines": { - "node": ">=18" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "size-limit": "12.0.0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", - "cpu": [ - "ia32" - ], - "dev": true, + "../../node_modules/.pnpm/@size-limit+preset-small-lib@12.0.0_size-limit@12.0.0_jiti@2.6.1_/node_modules/@size-limit/preset-small-lib": { + "version": "12.0.0", + "extraneous": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@size-limit/esbuild": "12.0.0", + "@size-limit/file": "12.0.0", + "size-limit": "12.0.0" + }, + "peerDependencies": { + "size-limit": "12.0.0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", - "cpu": [ - "loong64" - ], - "dev": true, + "../../node_modules/.pnpm/@types+node@24.10.4/node_modules/@types/node": { + "version": "24.10.4", + "extraneous": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "undici-types": "~7.16.0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", - "cpu": [ - "mips64el" - ], - "dev": true, + "../../node_modules/.pnpm/arktype@2.1.29/node_modules/arktype": { + "version": "2.1.29", + "extraneous": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@ark/schema": "0.56.0", + "@ark/util": "0.56.0", + "arkregex": "0.0.5" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "../../node_modules/.pnpm/rimraf@6.1.2/node_modules/rimraf": { + "version": "6.1.2", + "extraneous": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.0", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "devDependencies": { + "@types/node": "^24.9.2", + "mkdirp": "^3.0.1", + "prettier": "^3.6.2", + "tap": "^21.1.1", + "tshy": "^3.0.3", + "typedoc": "^0.28.14" + }, "engines": { - "node": ">=18" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", - "cpu": [ - "riscv64" - ], - "dev": true, + "../../node_modules/.pnpm/size-limit@12.0.0_jiti@2.6.1/node_modules/size-limit": { + "version": "12.0.0", + "extraneous": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "bytes-iec": "^3.1.1", + "lilconfig": "^3.1.3", + "nanospinner": "^1.2.2", + "picocolors": "^1.1.1", + "tinyglobby": "^0.2.15" + }, + "bin": { + "size-limit": "bin.js" + }, + "devDependencies": { + "@size-limit/esbuild": "workspace:*", + "@size-limit/file": "workspace:*", + "@size-limit/webpack": "workspace:*" + }, "engines": { - "node": ">=18" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "jiti": "^2.0.0" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", - "cpu": [ - "s390x" - ], - "dev": true, + "../../node_modules/.pnpm/tsdown@0.18.4_typescript@5.9.3/node_modules/tsdown": { + "version": "0.18.4", + "extraneous": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "ansis": "^4.2.0", + "cac": "^6.7.14", + "defu": "^6.1.4", + "empathic": "^2.0.0", + "hookable": "^6.0.1", + "import-without-cache": "^0.2.5", + "obug": "^2.1.1", + "picomatch": "^4.0.3", + "rolldown": "1.0.0-beta.57", + "rolldown-plugin-dts": "^0.20.0", + "semver": "^7.7.3", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tree-kill": "^1.2.2", + "unconfig-core": "^7.4.2", + "unrun": "^0.2.21" + }, + "bin": { + "tsdown": "dist/run.mjs" + }, + "devDependencies": { + "@arethetypeswrong/core": "^0.18.2", + "@sxzz/eslint-config": "^7.4.4", + "@sxzz/prettier-config": "^2.2.6", + "@sxzz/test-utils": "^0.5.15", + "@types/node": "^25.0.3", + "@types/picomatch": "^4.0.2", + "@types/semver": "^7.7.1", + "@typescript/native-preview": "7.0.0-dev.20251230.1", + "@unocss/eslint-plugin": "^66.5.12", + "@vitejs/devtools": "^0.0.0-alpha.22", + "@vitest/coverage-v8": "4.0.16", + "@vitest/ui": "^4.0.16", + "@vueuse/core": "^14.1.0", + "bumpp": "^10.3.2", + "dedent": "^1.7.1", + "eslint": "^9.39.2", + "is-in-ci": "^2.0.0", + "lightningcss": "^1.30.2", + "memfs": "^4.51.1", + "pkg-types": "^2.3.0", + "prettier": "^3.7.4", + "publint": "^0.3.16", + "rolldown-plugin-dts-snapshot": "^0.3.2", + "rolldown-plugin-require-cjs": "^0.3.3", + "typescript": "~5.9.3", + "unocss": "^66.5.12", + "unplugin-lightningcss": "^0.4.4", + "unplugin-unused": "^0.5.6", + "vite": "^8.0.0-beta.5", + "vitest": "^4.0.16" + }, "engines": { - "node": ">=18" + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@arethetypeswrong/core": "^0.18.1", + "@vitejs/devtools": "*", + "publint": "^0.3.0", + "typescript": "^5.0.0", + "unplugin-lightningcss": "^0.4.0", + "unplugin-unused": "^0.5.0" + }, + "peerDependenciesMeta": { + "@arethetypeswrong/core": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "publint": { + "optional": true + }, + "typescript": { + "optional": true + }, + "unplugin-lightningcss": { + "optional": true + }, + "unplugin-unused": { + "optional": true + } } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", - "cpu": [ - "x64" - ], + "../../node_modules/.pnpm/tsx@4.21.0/node_modules/tsx": { + "version": "4.21.0", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, "engines": { - "node": ">=18" + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript": { + "version": "5.9.3", + "extraneous": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "devDependencies": { + "@dprint/formatter": "^0.4.1", + "@dprint/typescript": "0.93.4", + "@esfx/canceltoken": "^1.0.0", + "@eslint/js": "^9.20.0", + "@octokit/rest": "^21.1.1", + "@types/chai": "^4.3.20", + "@types/diff": "^7.0.1", + "@types/minimist": "^1.2.5", + "@types/mocha": "^10.0.10", + "@types/ms": "^0.7.34", + "@types/node": "latest", + "@types/source-map-support": "^0.5.10", + "@types/which": "^3.0.4", + "@typescript-eslint/rule-tester": "^8.24.1", + "@typescript-eslint/type-utils": "^8.24.1", + "@typescript-eslint/utils": "^8.24.1", + "azure-devops-node-api": "^14.1.0", + "c8": "^10.1.3", + "chai": "^4.5.0", + "chokidar": "^4.0.3", + "diff": "^7.0.0", + "dprint": "^0.49.0", + "esbuild": "^0.25.0", + "eslint": "^9.20.1", + "eslint-formatter-autolinkable-stylish": "^1.4.0", + "eslint-plugin-regexp": "^2.7.0", + "fast-xml-parser": "^4.5.2", + "glob": "^10.4.5", + "globals": "^15.15.0", + "hereby": "^1.10.0", + "jsonc-parser": "^3.3.1", + "knip": "^5.44.4", + "minimist": "^1.2.8", + "mocha": "^10.8.2", + "mocha-fivemat-progress-reporter": "^0.1.0", + "monocart-coverage-reports": "^2.12.1", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "playwright": "^1.50.1", + "source-map-support": "^0.5.21", + "tslib": "^2.8.1", + "typescript": "^5.7.3", + "typescript-eslint": "^8.24.1", + "which": "^3.0.1" + }, "engines": { - "node": ">=18" + "node": ">=14.17" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", - "cpu": [ - "x64" - ], - "dev": true, + "../../node_modules/.pnpm/vitest@4.0.16_@opentelemetry+api@1.9.0_@types+node@24.10.4_@vitest+ui@4.0.16_jiti@2.6.1_c184208671c790f329b8a6bae22656f2/node_modules/vitest": { + "version": "4.0.16", + "extraneous": true, "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "dependencies": { + "@vitest/expect": "4.0.16", + "@vitest/mocker": "4.0.16", + "@vitest/pretty-format": "4.0.16", + "@vitest/runner": "4.0.16", + "@vitest/snapshot": "4.0.16", + "@vitest/spy": "4.0.16", + "@vitest/utils": "4.0.16", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "devDependencies": { + "@antfu/install-pkg": "^1.1.0", + "@edge-runtime/vm": "^5.0.0", + "@jridgewell/trace-mapping": "0.3.31", + "@opentelemetry/api": "^1.9.0", + "@sinonjs/fake-timers": "14.0.0", + "@types/estree": "^1.0.8", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/jsdom": "^27.0.0", + "@types/node": "^24.10.1", + "@types/picomatch": "^4.0.2", + "@types/prompts": "^2.4.9", + "@types/sinonjs__fake-timers": "^8.1.5", + "acorn-walk": "^8.3.4", + "birpc": "^4.0.0", + "cac": "^6.7.14", + "empathic": "^2.0.0", + "flatted": "^3.3.3", + "happy-dom": "^20.0.11", + "jsdom": "^27.2.0", + "local-pkg": "^1.1.2", + "mime": "^4.1.0", + "prompts": "^2.4.2", + "strip-literal": "^3.1.0", + "ws": "^8.18.3" + }, "engines": { - "node": ">=18" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.16", + "@vitest/browser-preview": "4.0.16", + "@vitest/browser-webdriverio": "4.0.16", + "@vitest/ui": "4.0.16", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", - "cpu": [ - "arm64" - ], - "dev": true, + "../../node_modules/.pnpm/zod@4.3.5/node_modules/zod": { + "version": "4.3.5", "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" + "funding": { + "url": "https://github.com/sponsors/colinhacks" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", - "cpu": [ - "x64" - ], - "dev": true, + "../../packages/arkenv": { + "version": "0.8.3", + "extraneous": true, "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" + "devDependencies": { + "@repo/keywords": "workspace:*", + "@repo/scope": "workspace:*", + "@repo/types": "workspace:*", + "@size-limit/esbuild-why": "catalog:", + "@size-limit/preset-small-lib": "catalog:", + "@types/node": "catalog:", + "arktype": "catalog:", + "rimraf": "catalog:", + "size-limit": "catalog:", + "tsdown": "catalog:", + "typescript": "catalog:", + "vitest": "catalog:", + "zod": "catalog:" + }, + "peerDependencies": { + "arktype": "^2.1.22" + }, + "peerDependenciesMeta": { + "arktype": { + "optional": true + } } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", - "cpu": [ - "arm64" - ], - "dev": true, + "../../packages/internal/keywords": { + "name": "@repo/keywords", + "version": "0.2.1", + "extraneous": true, "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@repo/scope": "workspace:*" + }, + "devDependencies": { + "arktype": "catalog:", + "tsdown": "catalog:", + "typescript": "catalog:" + }, + "peerDependencies": { + "arktype": "catalog:" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", - "cpu": [ - "x64" - ], - "dev": true, + "../../packages/internal/scope": { + "name": "@repo/scope", + "version": "0.1.2", + "extraneous": true, "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" + "devDependencies": { + "@types/node": "catalog:", + "arktype": "catalog:", + "tsdown": "catalog:", + "typescript": "catalog:" + }, + "peerDependencies": { + "arktype": "catalog:" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "../../packages/internal/types": { + "name": "@repo/types", + "version": "0.0.6", + "extraneous": true, + "devDependencies": { + "@repo/scope": "workspace:*", + "arktype": "catalog:", + "typescript": "catalog:" + }, + "peerDependencies": { + "@repo/scope": "workspace:*", + "arktype": "catalog:" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", - "cpu": [ - "ia32" - ], - "dev": true, + "node_modules/@ark/schema": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@ark/schema/-/schema-0.56.0.tgz", + "integrity": "sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@ark/util": "0.56.0" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } + "node_modules/@ark/util": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@ark/util/-/util-0.56.0.tgz", + "integrity": "sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA==", + "license": "MIT" }, "node_modules/arkenv": { "version": "0.8.3", - "resolved": "https://pkg.pr.new/arkenv@704", - "integrity": "sha512-zrar8aQgA7G+1lO6CKlRTOQI3brEYAiaoN/Tn//n5GtBBkPMus6GHZFuKKpULSC7wXkeAUUJeWGpGL9SbEZZFw==", + "resolved": "file:../../packages/arkenv/arkenv-0.8.3.tgz", + "integrity": "sha512-xBFQIngADLi5I//dpkMP4FoowZE50iHzUsVB4TvY3m8kRFPnCOPvWSxjyK/+3yBK7ALjJXoTvvqYC8M8Pa+EKA==", "license": "MIT", "peerDependencies": { "arktype": "^2.1.22" @@ -475,114 +511,34 @@ } } }, - "node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/get-tsconfig": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", - "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", - "dev": true, + "node_modules/arkregex": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/arkregex/-/arkregex-0.0.5.tgz", + "integrity": "sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw==", "license": "MIT", "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + "@ark/util": "0.56.0" } }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, + "node_modules/arktype": { + "version": "2.1.29", + "resolved": "https://registry.npmjs.org/arktype/-/arktype-2.1.29.tgz", + "integrity": "sha512-jyfKk4xIOzvYNayqnD8ZJQqOwcrTOUbIU4293yrzAjA3O1dWh61j71ArMQ6tS/u4pD7vabSPe7nG3RCyoXW6RQ==", "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + "peer": true, + "dependencies": { + "@ark/schema": "0.56.0", + "@ark/util": "0.56.0", + "arkregex": "0.0.5" } }, "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } + "resolved": "../../node_modules/.pnpm/tsx@4.21.0/node_modules/tsx", + "link": true }, "node_modules/zod": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", - "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } + "resolved": "../../node_modules/.pnpm/zod@4.3.5/node_modules/zod", + "link": true } } } diff --git a/examples/without-arktype/package.json b/examples/without-arktype/package.json index aa86027e3..bdfeac5e6 100644 --- a/examples/without-arktype/package.json +++ b/examples/without-arktype/package.json @@ -13,15 +13,16 @@ "keywords": [], "author": "", "license": "ISC", - "engines": { - "node": "24" - }, - "packageManager": "npm@11.6.4", "devDependencies": { "tsx": "^4.21.0" }, "dependencies": { - "arkenv": "https://pkg.pr.new/arkenv@704", + "arkenv": "file:../../packages/arkenv/arkenv-0.8.3.tgz", + "arktype": "^2.1.29", "zod": "^4.3.5" - } + }, + "engines": { + "node": "24" + }, + "packageManager": "npm@11.6.4" } diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 2fd90c264..4da5de2d4 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -1,5 +1,5 @@ import { createRequire } from "node:module"; -import { $ } from "@repo/scope"; +import type { $ } from "@repo/scope"; import type { EnvSchemaWithType, InferType, @@ -85,7 +85,7 @@ function validateArkType( let schema = isCompiledType ? (def as Type) - : ($.type(def as any) as unknown as Type); + : (type(def as any) as unknown as Type); // Apply the `onUndeclaredKey` option, defaulting to "delete" for arkenv compatibility schema = schema.onUndeclaredKey(config.onUndeclaredKey ?? "delete"); @@ -133,6 +133,86 @@ function validateArkType( } } +/** + * Validate a mapping of Standard Schema validators (e.g., Zod) + * without requiring ArkType. + */ +function validateStandardSchemaMapping( + def: SchemaShape, + env: Record, +): { success: true; value: unknown } | { success: false; issues: EnvIssue[] } { + const result: Record = {}; + const issues: EnvIssue[] = []; + + for (const [key, validator] of Object.entries(def)) { + const value = env[key]; + const standardSchema = (validator as StandardSchemaV1)?.["~standard"]; + + if (!standardSchema) { + issues.push({ + path: [key], + message: `Validator for ${key} is not a Standard Schema validator`, + }); + continue; + } + + const validationResult = standardSchema.validate(value); + + // Standard Schema can return Promise or sync result + // For now, we only support sync validation + if (validationResult instanceof Promise) { + issues.push({ + path: [key], + message: `Async validation is not supported for ${key}`, + }); + continue; + } + + if (validationResult.issues) { + for (const issue of validationResult.issues) { + issues.push({ + path: [key, ...(issue.path || []).map(String)], + message: issue.message || "Validation failed", + }); + } + } else { + result[key] = validationResult.value; + } + } + + if (issues.length > 0) { + return { success: false, issues }; + } + + return { success: true, value: result }; +} + +/** + * Detect what type of mapping we have + */ +function detectMappingType( + def: SchemaShape, +): { type: "standard-schema" } | { type: "arktype" } { + let hasStandardSchema = false; + let hasNonStandardSchema = false; + + for (const validator of Object.values(def)) { + if ((validator as StandardSchemaV1)?.["~standard"]) { + hasStandardSchema = true; + } else { + hasNonStandardSchema = true; + } + } + + // If all validators are Standard Schema, use Standard Schema validation + if (hasStandardSchema && !hasNonStandardSchema) { + return { type: "standard-schema" }; + } + + // Otherwise, use ArkType validation (handles strings, mixed, etc.) + return { type: "arktype" }; +} + /** * Validate and distill environment variables based on a schema. * @@ -167,7 +247,13 @@ export function createEnv( ); } - const result = validateArkType(def, config, env); + // Detect what type of mapping we have and route to the appropriate validator + const mappingType = detectMappingType(def as SchemaShape); + + const result = + mappingType.type === "standard-schema" + ? validateStandardSchemaMapping(def as SchemaShape, env) + : validateArkType(def, config, env); if (!result.success) { throw new ArkEnvError(result.issues); diff --git a/packages/arkenv/src/utils/coerce.ts b/packages/arkenv/src/utils/coerce.ts index 664a8ef94..c4a19885c 100644 --- a/packages/arkenv/src/utils/coerce.ts +++ b/packages/arkenv/src/utils/coerce.ts @@ -1,5 +1,5 @@ import { createRequire } from "node:module"; -import { maybeBoolean, maybeJson, maybeNumber } from "@repo/keywords"; +import { maybeBooleanFn, maybeJsonFn, maybeNumberFn } from "@repo/keywords"; import type { JsonSchema, Type } from "arktype"; const require = createRequire(import.meta.url); @@ -168,18 +168,18 @@ const applyCoercion = ( const rootTarget = targets.find((t) => t.path.length === 0); if (rootTarget?.type === "object" && typeof data === "string") { - return maybeJson(data); + return maybeJsonFn(data); } if (rootTarget?.type === "array" && typeof data === "string") { return splitString(data); } - const asNumber = maybeNumber(data); + const asNumber = maybeNumberFn(data); if (typeof asNumber === "number") { return asNumber; } - return maybeBoolean(data); + return maybeBooleanFn(data); } return data; } @@ -209,14 +209,14 @@ const applyCoercion = ( for (let i = 0; i < current.length; i++) { const original = current[i]; if (type === "primitive") { - const asNumber = maybeNumber(original); + const asNumber = maybeNumberFn(original); if (typeof asNumber === "number") { current[i] = asNumber; } else { - current[i] = maybeBoolean(original); + current[i] = maybeBooleanFn(original); } } else if (type === "object") { - current[i] = maybeJson(original); + current[i] = maybeJsonFn(original); } } } @@ -234,7 +234,7 @@ const applyCoercion = ( } if (type === "object" && typeof original === "string") { - record[lastKey] = maybeJson(original); + record[lastKey] = maybeJsonFn(original); return; } @@ -242,22 +242,22 @@ const applyCoercion = ( if (type === "primitive") { for (let i = 0; i < original.length; i++) { const item = original[i]; - const asNumber = maybeNumber(item); + const asNumber = maybeNumberFn(item); if (typeof asNumber === "number") { original[i] = asNumber; } else { - original[i] = maybeBoolean(item); + original[i] = maybeBooleanFn(item); } } } } else { if (type === "primitive") { - const asNumber = maybeNumber(original); + const asNumber = maybeNumberFn(original); // If numeric parsing didn't produce a number, try boolean coercion if (typeof asNumber === "number") { record[lastKey] = asNumber; } else { - record[lastKey] = maybeBoolean(original); + record[lastKey] = maybeBooleanFn(original); } } } diff --git a/packages/internal/keywords/package.json b/packages/internal/keywords/package.json index 61a39717f..042be34d0 100644 --- a/packages/internal/keywords/package.json +++ b/packages/internal/keywords/package.json @@ -33,6 +33,9 @@ "url": "git+https://github.com/yamcodes/arkenv.git" }, "author": "Yam Borodetsky ", + "dependencies": { + "@repo/scope": "workspace:*" + }, "devDependencies": { "arktype": "catalog:", "tsdown": "catalog:", diff --git a/packages/internal/keywords/tsdown.config.ts b/packages/internal/keywords/tsdown.config.ts index f99459d2f..b17832d5e 100644 --- a/packages/internal/keywords/tsdown.config.ts +++ b/packages/internal/keywords/tsdown.config.ts @@ -8,4 +8,5 @@ export default defineConfig({ resolve: ["@repo/types"], }, external: ["arktype"], + // Don't externalize @repo/scope - bundle it }); diff --git a/packages/internal/scope/package.json b/packages/internal/scope/package.json index 5232014c8..959e00dc8 100644 --- a/packages/internal/scope/package.json +++ b/packages/internal/scope/package.json @@ -33,9 +33,7 @@ "url": "git+https://github.com/yamcodes/arkenv.git" }, "author": "Yam Borodetsky ", - "dependencies": { - "@repo/keywords": "workspace:*" - }, + "dependencies": {}, "devDependencies": { "@types/node": "catalog:", "arktype": "catalog:", diff --git a/packages/internal/scope/src/index.ts b/packages/internal/scope/src/index.ts index 2647d290d..e6d97ff85 100644 --- a/packages/internal/scope/src/index.ts +++ b/packages/internal/scope/src/index.ts @@ -1,7 +1,8 @@ import { createRequire } from "node:module"; -import { host, port } from "@repo/keywords"; import type { scope as ArkScope, type as ArkType } from "arktype"; +export { lazyType } from "./lazy-type"; + const require = createRequire(import.meta.url); let _$: any; @@ -27,6 +28,10 @@ export const $: ArkEnvScope = new Proxy( scope: typeof ArkScope; type: typeof ArkType; }; + + // Lazy load keywords to avoid circular dependency + const { host, port } = require("@repo/keywords"); + _$ = scope({ string: type.module({ ...type.keywords.string, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac7cf8aa7..449432d8b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -614,6 +614,10 @@ importers: version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) packages/internal/keywords: + dependencies: + '@repo/scope': + specifier: workspace:* + version: link:../scope devDependencies: arktype: specifier: 'catalog:' @@ -626,10 +630,6 @@ importers: version: 5.9.3 packages/internal/scope: - dependencies: - '@repo/keywords': - specifier: workspace:* - version: link:../keywords devDependencies: '@types/node': specifier: 'catalog:' @@ -3608,9 +3608,6 @@ packages: '@speed-highlight/core@1.2.12': resolution: {integrity: sha512-uilwrK0Ygyri5dToHYdZSjcvpS2ZwX0w5aSt3GCEN9hrjxWCoeV4Z2DTXuxjwbntaLQIEEAlCeNQss5SoHvAEA==} - '@standard-schema/spec@1.0.0': - resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} - '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -11419,8 +11416,6 @@ snapshots: '@speed-highlight/core@1.2.12': {} - '@standard-schema/spec@1.0.0': {} - '@standard-schema/spec@1.1.0': {} '@svta/cml-608@1.0.1': {} @@ -11859,7 +11854,7 @@ snapshots: '@vitest/expect@4.0.16': dependencies: - '@standard-schema/spec': 1.0.0 + '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 '@vitest/spy': 4.0.16 '@vitest/utils': 4.0.16 From 8ae32c83d0c0faef753600fe59fb0443049dab3c Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Tue, 13 Jan 2026 21:19:31 +0500 Subject: [PATCH 081/117] refactor(arkenv): decouple arktype from default build - Move arktype keywords to internal scope package - Lazily load arktype keywords within arkenv - Update arktype --- examples/without-arktype/package-lock.json | 18 +--- examples/without-arktype/package.json | 1 - examples/without-arktype/src/index.ts | 12 +-- packages/arkenv/src/create-env.ts | 63 ++++++------- packages/internal/keywords/src/index.ts | 100 +-------------------- packages/internal/scope/src/index.ts | 9 +- packages/internal/scope/src/keywords.ts | 62 +++++++++++++ packages/internal/scope/src/lazy-type.ts | 37 ++++---- 8 files changed, 123 insertions(+), 179 deletions(-) create mode 100644 packages/internal/scope/src/keywords.ts diff --git a/examples/without-arktype/package-lock.json b/examples/without-arktype/package-lock.json index 31f97854d..f521d9e72 100644 --- a/examples/without-arktype/package-lock.json +++ b/examples/without-arktype/package-lock.json @@ -10,7 +10,6 @@ "license": "ISC", "dependencies": { "arkenv": "file:../../packages/arkenv/arkenv-0.8.3.tgz", - "arktype": "^2.1.29", "zod": "^4.3.5" }, "devDependencies": { @@ -487,6 +486,7 @@ "resolved": "https://registry.npmjs.org/@ark/schema/-/schema-0.56.0.tgz", "integrity": "sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA==", "license": "MIT", + "optional": true, "dependencies": { "@ark/util": "0.56.0" } @@ -495,7 +495,8 @@ "version": "0.56.0", "resolved": "https://registry.npmjs.org/@ark/util/-/util-0.56.0.tgz", "integrity": "sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA==", - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/arkenv": { "version": "0.8.3", @@ -516,22 +517,11 @@ "resolved": "https://registry.npmjs.org/arkregex/-/arkregex-0.0.5.tgz", "integrity": "sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw==", "license": "MIT", + "optional": true, "dependencies": { "@ark/util": "0.56.0" } }, - "node_modules/arktype": { - "version": "2.1.29", - "resolved": "https://registry.npmjs.org/arktype/-/arktype-2.1.29.tgz", - "integrity": "sha512-jyfKk4xIOzvYNayqnD8ZJQqOwcrTOUbIU4293yrzAjA3O1dWh61j71ArMQ6tS/u4pD7vabSPe7nG3RCyoXW6RQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@ark/schema": "0.56.0", - "@ark/util": "0.56.0", - "arkregex": "0.0.5" - } - }, "node_modules/tsx": { "resolved": "../../node_modules/.pnpm/tsx@4.21.0/node_modules/tsx", "link": true diff --git a/examples/without-arktype/package.json b/examples/without-arktype/package.json index bdfeac5e6..7f51a5e00 100644 --- a/examples/without-arktype/package.json +++ b/examples/without-arktype/package.json @@ -18,7 +18,6 @@ }, "dependencies": { "arkenv": "file:../../packages/arkenv/arkenv-0.8.3.tgz", - "arktype": "^2.1.29", "zod": "^4.3.5" }, "engines": { diff --git a/examples/without-arktype/src/index.ts b/examples/without-arktype/src/index.ts index 7be7e468c..4aff19ae5 100644 --- a/examples/without-arktype/src/index.ts +++ b/examples/without-arktype/src/index.ts @@ -1,16 +1,10 @@ import { createEnv } from "arkenv"; import z from "zod"; -// const env = createEnv({ -// TEST_VALUE: z.url(), -// PORT: z.coerce.number(), -// HOST: z.literal("localhost").or(z.url()), -// }); - const env = createEnv({ - TEST_VALUE: "string.url", - PORT: "number", - HOST: "'localhost' | string.url", + TEST_VALUE: z.url(), + PORT: z.coerce.number(), + HOST: z.literal("localhost").or(z.url()), }); console.log(`Value: ${String(env.TEST_VALUE)}`); diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 4da5de2d4..6040aeca9 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -1,5 +1,5 @@ import { createRequire } from "node:module"; -import type { $ } from "@repo/scope"; +import { $ } from "@repo/scope"; import type { EnvSchemaWithType, InferType, @@ -48,27 +48,28 @@ export type ArkEnvConfig = { arrayFormat?: "comma" | "json"; }; -function detectValidatorType(def: unknown) { - const isStandard = !!(def as StandardSchemaV1)?.["~standard"]; - const isObjectLike = - typeof def === "function" || (typeof def === "object" && def !== null); - - if (!isObjectLike) { - return { isStandard, isArkCompiled: false }; +/** + * Helper to identify if a value is an ArkType-compiled type or a lazy proxy. + * We want these to go through the validateArkType path for coercion. + */ +function isArktype(def: unknown): boolean { + if (typeof def !== "function" && (typeof def !== "object" || def === null)) { + return false; } const d = def as Record; - // Check for ArkType's brand/symbol if available, fall back to duck typing - const hasArktypeBrand = - "infer" in d || (typeof d.t === "object" && d.t !== null && "infer" in d.t); - - const isArkCompiled = - hasArktypeBrand || - (("t" in d || "allows" in d) && - ("infer" in d || - "toJsonSchema" in d || - "expression" in d || - ("array" in d && "or" in d && "pipe" in d))); + + // Check for ArkType's brand or our lazy proxy state + return ( + "infer" in d || + (typeof d.t === "object" && d.t !== null && "infer" in d.t) || + (typeof d.allows === "function" && "pipe" in d) + ); +} + +function detectValidatorType(def: unknown) { + const isStandard = !!(def as StandardSchemaV1)?.["~standard"]; + const isArkCompiled = isArktype(def); return { isStandard, isArkCompiled }; } @@ -79,13 +80,11 @@ function validateArkType( env: Record, ): { success: true; value: unknown } | { success: false; issues: EnvIssue[] } { try { - const { type } = require("arktype"); - const { isArkCompiled: isCompiledType } = detectValidatorType(def); let schema = isCompiledType ? (def as Type) - : (type(def as any) as unknown as Type); + : ($.type(def as any) as unknown as Type); // Apply the `onUndeclaredKey` option, defaulting to "delete" for arkenv compatibility schema = schema.onUndeclaredKey(config.onUndeclaredKey ?? "delete"); @@ -93,13 +92,13 @@ function validateArkType( // Apply coercion transformation (Lazy Loaded) if (config.coerce !== false) { schema = coerce(schema, { - ...(config.arrayFormat ? { arrayFormat: config.arrayFormat } : {}), + arrayFormat: config.arrayFormat, }); } const result = schema(env); - if (result instanceof type.errors) { + if (result instanceof ($.type as any).errors) { return { success: false, issues: Object.entries( @@ -194,22 +193,26 @@ function detectMappingType( def: SchemaShape, ): { type: "standard-schema" } | { type: "arktype" } { let hasStandardSchema = false; - let hasNonStandardSchema = false; + let hasArktype = false; for (const validator of Object.values(def)) { - if ((validator as StandardSchemaV1)?.["~standard"]) { + if (isArktype(validator)) { + hasArktype = true; + } else if ((validator as StandardSchemaV1)?.["~standard"]) { hasStandardSchema = true; } else { - hasNonStandardSchema = true; + // If it's a string definition or something else, it's ArkType-path + hasArktype = true; } } - // If all validators are Standard Schema, use Standard Schema validation - if (hasStandardSchema && !hasNonStandardSchema) { + // If all validators are ONLY Standard Schema (and NOT ArkType), use Standard Schema path + // This ensures that ArkType types (which implement Standard Schema) still go through validateArkType + if (hasStandardSchema && !hasArktype) { return { type: "standard-schema" }; } - // Otherwise, use ArkType validation (handles strings, mixed, etc.) + // Otherwise, fallback to ArkType path return { type: "arktype" }; } diff --git a/packages/internal/keywords/src/index.ts b/packages/internal/keywords/src/index.ts index 87d90ce69..76f8fae56 100644 --- a/packages/internal/keywords/src/index.ts +++ b/packages/internal/keywords/src/index.ts @@ -1,99 +1 @@ -import { lazyType as type } from "@repo/scope"; - -/** - * A loose numeric morph function (internal use only). - * - * **In**: `unknown` - * - * **Out**: A `number` if the input is a numeric string; otherwise the original input. - * - * Useful for coercion in unions where failing on non-numeric strings would block other branches. - */ -export const maybeNumberFn = (s: unknown) => { - if (typeof s === "number") return s; - if (typeof s !== "string") return s; - const trimmed = s.trim(); - if (trimmed === "") return s; - if (trimmed === "NaN") return Number.NaN; - const n = Number(trimmed); - return Number.isNaN(n) ? s : n; -}; - -/** - * A loose numeric morph. - * - * **In**: `unknown` - * - * **Out**: A `number` if the input is a numeric string; otherwise the original input. - * - * Useful for coercion in unions where failing on non-numeric strings would block other branches. - */ -export const maybeNumber = type("unknown").pipe(maybeNumberFn); - -/** - * A loose boolean morph function (internal use only). - * - * **In**: `unknown` - * - * **Out**: `true` for `"true"`, `false` for `"false"`; otherwise the original input. - * - * Useful for coercion in unions where failing on non-boolean strings would block other branches. - */ -export const maybeBooleanFn = (s: unknown) => { - if (s === "true") return true; - if (s === "false") return false; - return s; -}; - -/** - * A loose boolean morph. - * - * **In**: `unknown` - * - * **Out**: `true` for `"true"`, `false` for `"false"`; otherwise the original input. - * - * Useful for coercion in unions where failing on non-boolean strings would block other branches. - */ -export const maybeBoolean = type("unknown").pipe(maybeBooleanFn); - -/** - * A `number` integer between 0 and 65535. - */ -export const port = type("0 <= number.integer <= 65535"); - -/** - * An IP address or `"localhost"` - */ -export const host = type("string.ip | 'localhost'"); - -/** - * A loose JSON morph function (internal use only). - * - * **In**: `unknown` - * - * **Out**: A parsed JSON object if the input is a valid JSON string; otherwise the original input. - * - * Useful for coercion in unions where failing on non-JSON strings would block other branches. - */ -export const maybeJsonFn = (s: unknown) => { - if (typeof s !== "string") return s; - const trimmed = s.trim(); - // Only attempt to parse if it looks like JSON (starts with { or [) - if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return s; - try { - return JSON.parse(trimmed); - } catch { - return s; - } -}; - -/** - * A loose JSON morph. - * - * **In**: `unknown` - * - * **Out**: A parsed JSON object if the input is a valid JSON string; otherwise the original input. - * - * Useful for coercion in unions where failing on non-JSON strings would block other branches. - */ -export const maybeJson = type("unknown").pipe(maybeJsonFn); +export * from "@repo/scope"; diff --git a/packages/internal/scope/src/index.ts b/packages/internal/scope/src/index.ts index e6d97ff85..ad2903eb4 100644 --- a/packages/internal/scope/src/index.ts +++ b/packages/internal/scope/src/index.ts @@ -1,9 +1,8 @@ -import { createRequire } from "node:module"; import type { scope as ArkScope, type as ArkType } from "arktype"; +import { host, port } from "./keywords"; export { lazyType } from "./lazy-type"; - -const require = createRequire(import.meta.url); +export * from "./keywords"; let _$: any; @@ -23,15 +22,13 @@ export const $: ArkEnvScope = new Proxy( { get(_, prop) { if (!_$) { + // We don't use require("arktype") statically to avoid load-time errors try { const { scope, type } = require("arktype") as { scope: typeof ArkScope; type: typeof ArkType; }; - // Lazy load keywords to avoid circular dependency - const { host, port } = require("@repo/keywords"); - _$ = scope({ string: type.module({ ...type.keywords.string, diff --git a/packages/internal/scope/src/keywords.ts b/packages/internal/scope/src/keywords.ts new file mode 100644 index 000000000..ef9423db4 --- /dev/null +++ b/packages/internal/scope/src/keywords.ts @@ -0,0 +1,62 @@ +import { lazyType as type } from "./lazy-type"; + +/** + * A loose numeric morph function (internal use only). + */ +export const maybeNumberFn = (s: unknown) => { + if (typeof s === "number") return s; + if (typeof s !== "string") return s; + const trimmed = s.trim(); + if (trimmed === "") return s; + if (trimmed === "NaN") return Number.NaN; + const n = Number(trimmed); + return Number.isNaN(n) ? s : n; +}; + +/** + * A loose numeric morph. + */ +export const maybeNumber = type("unknown").pipe(maybeNumberFn); + +/** + * A loose boolean morph function (internal use only). + */ +export const maybeBooleanFn = (s: unknown) => { + if (s === "true") return true; + if (s === "false") return false; + return s; +}; + +/** + * A loose boolean morph. + */ +export const maybeBoolean = type("unknown").pipe(maybeBooleanFn); + +/** + * A loose JSON morph function (internal use only). + */ +export const maybeJsonFn = (s: unknown) => { + if (typeof s !== "string") return s; + const trimmed = s.trim(); + if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return s; + try { + return JSON.parse(trimmed); + } catch { + return s; + } +}; + +/** + * A loose JSON morph. + */ +export const maybeJson = type("unknown").pipe(maybeJsonFn); + +/** + * A `number` integer between 0 and 65535. + */ +export const port = type("0 <= number.integer <= 65535"); + +/** + * An IP address or `"localhost"` + */ +export const host = type("string.ip | 'localhost'"); diff --git a/packages/internal/scope/src/lazy-type.ts b/packages/internal/scope/src/lazy-type.ts index 15ec9c902..2f34d963c 100644 --- a/packages/internal/scope/src/lazy-type.ts +++ b/packages/internal/scope/src/lazy-type.ts @@ -21,28 +21,25 @@ type LazyTypeProxy = { * ArkType when the validator is actually used or inspected. */ function createLazyProxy(state: LazyTypeProxy): any { - return new Proxy( - {}, - { - get(_, prop) { - // Handle .pipe() - chain morphs without realizing the type yet - if (prop === "pipe") { - return (morph: (value: unknown) => unknown) => { - state.morphs.push(morph); - return createLazyProxy(state); - }; - } + return new Proxy(() => {}, { + get(_, prop) { + // Handle .pipe() - chain morphs without realizing the type yet + if (prop === "pipe") { + return (morph: (value: unknown) => unknown) => { + state.morphs.push(morph); + return createLazyProxy(state); + }; + } - // For any other property access, we need to realize the type - return realizeType(state)[prop]; - }, - apply(_, __, args) { - // If the proxy itself is called as a validator - const realized = realizeType(state); - return realized(...args); - }, + // For any other property access, we need to realize the type + return realizeType(state)[prop]; }, - ); + apply(_, __, args) { + // If the proxy itself is called as a validator + const realized = realizeType(state); + return realized(...args); + }, + }); } /** From c59a4af22662e4a87c36f42779fd0a596532039f Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Tue, 13 Jan 2026 21:22:56 +0500 Subject: [PATCH 082/117] refactor(lazy-type): cache ArkType module loading - Extract ArkType module loading logic - Cache ArkType module for single load - Improve performance by avoiding re-requires - Consolidate module not found error handling --- packages/internal/scope/src/index.ts | 2 +- packages/internal/scope/src/lazy-type.ts | 49 ++++++++++++++---------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/packages/internal/scope/src/index.ts b/packages/internal/scope/src/index.ts index ad2903eb4..6a661e3c5 100644 --- a/packages/internal/scope/src/index.ts +++ b/packages/internal/scope/src/index.ts @@ -1,8 +1,8 @@ import type { scope as ArkScope, type as ArkType } from "arktype"; import { host, port } from "./keywords"; -export { lazyType } from "./lazy-type"; export * from "./keywords"; +export { lazyType } from "./lazy-type"; let _$: any; diff --git a/packages/internal/scope/src/lazy-type.ts b/packages/internal/scope/src/lazy-type.ts index 2f34d963c..1ec17f79e 100644 --- a/packages/internal/scope/src/lazy-type.ts +++ b/packages/internal/scope/src/lazy-type.ts @@ -3,6 +3,32 @@ import type { type as ArkType } from "arktype"; const require = createRequire(import.meta.url); +let _at: typeof import("arktype") | undefined; + +/** + * Load ArkType lazily and cache the result. + */ +function loadArkType(): typeof import("arktype") { + if (_at) return _at; + try { + _at = require("arktype"); + return _at!; + } catch (e: unknown) { + if ( + e instanceof Error && + "code" in e && + e.code === "MODULE_NOT_FOUND" && + e.message.includes("'arktype'") + ) { + throw new Error( + "ArkType is required when using ArkType-specific schemas (string definitions or type() calls). " + + "Please install 'arktype' as a dependency, or use Standard Schema validators like Zod instead.", + ); + } + throw e; + } +} + /** * A lazy proxy that defers loading ArkType until it is actually needed. * This allows internal packages to use ArkType syntax without creating @@ -51,24 +77,7 @@ function realizeType(state: LazyTypeProxy): any { return state._realized; } - let at: typeof import("arktype"); - try { - at = require("arktype"); - } catch (e: unknown) { - if ( - e instanceof Error && - "code" in e && - e.code === "MODULE_NOT_FOUND" && - e.message.includes("'arktype'") - ) { - throw new Error( - "ArkType is required when using ArkType-specific schemas (string definitions or type() calls). " + - "Please install 'arktype' as a dependency, or use Standard Schema validators like Zod instead.", - ); - } - throw e; - } - + const at = loadArkType(); const { type } = at; // Build the type from the definition @@ -95,7 +104,7 @@ export const lazyType = new Proxy(() => {}, { get(_, prop) { // Forward static properties from the real type function // This ensures things like type.keywords work correctly - const at = require("arktype"); - return at.type[prop]; + const at = loadArkType(); + return (at.type as any)[prop]; }, }) as typeof ArkType; From 50be0c50da6177f9dc93c51e9f56602300a845f4 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Tue, 13 Jan 2026 21:59:25 +0500 Subject: [PATCH 083/117] feat(arkenv): make ArkType an optional dependency - Implement lazy loading for ArkType - Introduce `arktypeLoader` and `lazyType` proxy - Refactor core `createEnv --- .gitignore | 1 + .../src/arktype-optional.integration.test.ts | 126 +++++ packages/arkenv/src/create-env.ts | 287 +++++------- packages/arkenv/src/type.ts | 15 +- packages/arkenv/src/utils/coerce.ts | 442 ++++++------------ packages/internal/scope/src/index.ts | 57 +-- packages/internal/scope/src/lazy-type.ts | 88 ++-- 7 files changed, 473 insertions(+), 543 deletions(-) create mode 100644 packages/arkenv/src/arktype-optional.integration.test.ts diff --git a/.gitignore b/.gitignore index 857df7646..2aac385af 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* +*.log # environment variables diff --git a/packages/arkenv/src/arktype-optional.integration.test.ts b/packages/arkenv/src/arktype-optional.integration.test.ts new file mode 100644 index 000000000..b728f8106 --- /dev/null +++ b/packages/arkenv/src/arktype-optional.integration.test.ts @@ -0,0 +1,126 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +describe("ArkType Optional Contract", () => { + afterEach(async () => { + vi.clearAllMocks(); + + const G = globalThis as any; + + // Reset loader via global Symbol + const loader = G[Symbol.for("__ARKENV_ARKTYPE_LOADER__")]; + if (loader) { + loader.reset(); + } + + // Reset scope cache via global Symbol + const scopeCache = G[Symbol.for("__ARKENV_SCOPE_CACHE__")]; + if (scopeCache) { + scopeCache.scope = undefined; + } + + delete process.env.ARKENV_FORCE_MISSING; + }); + + describe("Scenario 1: ArkType Strings + ArkType Present", () => { + it("should validate correctly using ArkType string DSL", async () => { + const { createEnv } = await import("./index"); + const env = createEnv( + { + PORT: "number.port", + }, + { + env: { PORT: "3000" }, + }, + ); + expect(env.PORT).toBe(3000); + }); + }); + + describe("Scenario 2: Compiled ArkType Schema + ArkType Present", () => { + it("should validate correctly using type()", async () => { + const { createEnv, type } = await import("./index"); + const env = createEnv( + { + PORT: type("number.port"), + }, + { + env: { PORT: "8080" }, + }, + ); + expect(env.PORT).toBe(8080); + }); + }); + + describe("Scenario 3: Standard Schema (Zod) + ArkType Missing", () => { + it("should support Zod mapping even if ArkType is unavailable", async () => { + process.env.ARKENV_FORCE_MISSING = "true"; + + const G = globalThis as any; + const scopeCache = G[Symbol.for("__ARKENV_SCOPE_CACHE__")]; + if (scopeCache) scopeCache.scope = undefined; + + const { createEnv } = await import("./index"); + const { z } = await import("zod"); + + const env = createEnv( + { + PORT: z.coerce.number(), + }, + { + env: { PORT: "5432" }, + }, + ); + + expect(env.PORT).toBe(5432); + }); + + it("should not contain ArkType-specific terms in Zod validation errors", async () => { + process.env.ARKENV_FORCE_MISSING = "true"; + + const G = globalThis as any; + const scopeCache = G[Symbol.for("__ARKENV_SCOPE_CACHE__")]; + if (scopeCache) scopeCache.scope = undefined; + + const { createEnv } = await import("./index"); + const { z } = await import("zod"); + + try { + createEnv( + { + PORT: z.number(), + }, + { + env: { PORT: "not-a-number" }, + }, + ); + expect.fail("Should have thrown"); + } catch (e: any) { + expect(e.message).not.toContain("ArkType"); + expect(e.message).toMatch(/expected number/i); + } + }); + }); + + describe("Scenario 4: ArkType Strings + ArkType Missing", () => { + it("should provide a friendly, actionable error message", async () => { + process.env.ARKENV_FORCE_MISSING = "true"; + + const G = globalThis as any; + const scopeCache = G[Symbol.for("__ARKENV_SCOPE_CACHE__")]; + if (scopeCache) scopeCache.scope = undefined; + + const { createEnv } = await import("./index"); + expect(() => + createEnv( + { + // Use unique definition to avoid any potential internal ArkType caching + PORT: "number.port > 0", + }, + { + env: { PORT: "3000" }, + }, + ), + ).toThrow(/ArkType is required/); + }); + }); +}); diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 6040aeca9..e15e6ca57 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -1,77 +1,64 @@ -import { createRequire } from "node:module"; import { $ } from "@repo/scope"; -import type { - EnvSchemaWithType, - InferType, - SchemaShape, - StandardSchemaV1, -} from "@repo/types"; -import type { type as at, Type } from "arktype"; +import type { StandardSchemaV1 } from "@repo/types"; +import type { Type } from "arktype"; import { ArkEnvError, type EnvIssue } from "./errors"; import { coerce } from "./utils/coerce"; -const require = createRequire(import.meta.url); - -export type EnvSchema = at.validate; - /** - * Configuration options for `createEnv` + * The configuration for the ArkEnv library. */ -export type ArkEnvConfig = { - /** - * The environment variables to validate. Defaults to `process.env` - */ +export interface ArkEnvConfig { env?: Record; - /** - * Whether to coerce environment variables to their defined types. - * - * @default true - * Note: Only takes effect when using ArkType. - */ - coerce?: boolean; - /** - * How to handle undeclared keys in the schema. - * - `delete`: Remove undeclared keys. - * - `ignore`: Leave undeclared keys as they are. - * - `reject`: Throw an error if undeclared keys are present. - * @default "delete" - */ - onUndeclaredKey?: "delete" | "ignore" | "reject"; - /** - * For arrays, specify how to coerce the string value. - * - `comma`: Split by commas and trim whitespace. - * - `json`: Strings are coerced as JSON. - * - * Note: only takes effect when `coerce` is enabled. - * @default "comma" - */ - arrayFormat?: "comma" | "json"; -}; + coerce?: + | boolean + | { + numbers?: boolean; + booleans?: boolean; + objects?: boolean; + }; +} + +type SchemaShape = Record; + +function detectValidatorType(def: unknown): { + isArkCompiled: boolean; + isStandard: boolean; +} { + const isArkCompiled = isArktype(def); + const isStandard = + !isArkCompiled && (def as StandardSchemaV1)?.["~standard"] !== undefined; + + return { isArkCompiled, isStandard }; +} -/** - * Helper to identify if a value is an ArkType-compiled type or a lazy proxy. - * We want these to go through the validateArkType path for coercion. - */ function isArktype(def: unknown): boolean { if (typeof def !== "function" && (typeof def !== "object" || def === null)) { return false; } - const d = def as Record; + // ArkType 2.0 markers or our own lazy proxy + if ((def as any).isArktype === true) return true; + + const arkKind = (def as any).arkKind; - // Check for ArkType's brand or our lazy proxy state + // Narrow detection: focus on ArkType-specific metadata return ( - "infer" in d || - (typeof d.t === "object" && d.t !== null && "infer" in d.t) || - (typeof d.allows === "function" && "pipe" in d) + arkKind === "type" || + arkKind === "generic" || + arkKind === "module" || + (typeof (def as any).assert === "function" && + (def as any).infer !== undefined) ); } -function detectValidatorType(def: unknown) { - const isStandard = !!(def as StandardSchemaV1)?.["~standard"]; - const isArkCompiled = isArktype(def); +function isArkErrors(result: unknown): boolean { + if (!result || typeof result !== "object") return false; - return { isStandard, isArkCompiled }; + const arkKind = (result as any).arkKind; + return ( + arkKind === "errors" || + ("byPath" in (result as any) && "count" in (result as any)) + ); } function validateArkType( @@ -79,103 +66,75 @@ function validateArkType( config: ArkEnvConfig, env: Record, ): { success: true; value: unknown } | { success: false; issues: EnvIssue[] } { - try { - const { isArkCompiled: isCompiledType } = detectValidatorType(def); - - let schema = isCompiledType - ? (def as Type) - : ($.type(def as any) as unknown as Type); - - // Apply the `onUndeclaredKey` option, defaulting to "delete" for arkenv compatibility - schema = schema.onUndeclaredKey(config.onUndeclaredKey ?? "delete"); - - // Apply coercion transformation (Lazy Loaded) - if (config.coerce !== false) { - schema = coerce(schema, { - arrayFormat: config.arrayFormat, - }); - } - - const result = schema(env); - - if (result instanceof ($.type as any).errors) { + const { isArkCompiled: isCompiledType } = detectValidatorType(def); + + let schema = isCompiledType + ? (def as Type) + : ($.type(def as any) as unknown as Type); + + const coercionConfig = + config.coerce === false + ? { numbers: false, booleans: false, objects: false } + : { + numbers: + typeof config.coerce === "object" + ? (config.coerce.numbers ?? true) + : true, + booleans: + typeof config.coerce === "object" + ? (config.coerce.booleans ?? true) + : true, + objects: + typeof config.coerce === "object" + ? (config.coerce.objects ?? true) + : true, + }; + + const coercedEnv = coerce(schema, env, coercionConfig); + + const result = schema(coercedEnv); + + if (isArkErrors(result)) { + const issues: EnvIssue[] = (result as any).map((error: any) => { return { - success: false, - issues: Object.entries( - (result as { byPath: Record }).byPath, - ).map(([path, error]) => ({ - path: path ? path.split(".") : [], - message: - typeof error === "object" && error !== null && "message" in error - ? String((error as { message?: string }).message) - : "Validation failed", - })), + path: Array.isArray(error.path) + ? error.path + : [String(error.path || "root")], + message: error.message, }; - } + }); - return { - success: true, - value: result, - }; - } catch (e: unknown) { - if ( - e instanceof Error && - "code" in e && - e.code === "MODULE_NOT_FOUND" && - e.message.includes("'arktype'") - ) { - throw new Error( - "ArkType is required for this schema type. Please install 'arktype' or use a Standard Schema validator like Zod.", - ); - } - throw e; + return { success: false, issues }; } + + return { success: true, value: result }; } -/** - * Validate a mapping of Standard Schema validators (e.g., Zod) - * without requiring ArkType. - */ function validateStandardSchemaMapping( def: SchemaShape, env: Record, ): { success: true; value: unknown } | { success: false; issues: EnvIssue[] } { - const result: Record = {}; + const value: Record = {}; const issues: EnvIssue[] = []; for (const [key, validator] of Object.entries(def)) { - const value = env[key]; - const standardSchema = (validator as StandardSchemaV1)?.["~standard"]; - - if (!standardSchema) { - issues.push({ - path: [key], - message: `Validator for ${key} is not a Standard Schema validator`, - }); - continue; - } - - const validationResult = standardSchema.validate(value); + const result = (validator as StandardSchemaV1)["~standard"].validate( + env[key], + ); - // Standard Schema can return Promise or sync result - // For now, we only support sync validation - if (validationResult instanceof Promise) { - issues.push({ - path: [key], - message: `Async validation is not supported for ${key}`, - }); - continue; + if (result instanceof Promise) { + throw new Error("ArkEnv does not support asynchronous validators"); } - if (validationResult.issues) { - for (const issue of validationResult.issues) { + if (result.issues) { + for (const issue of result.issues) { issues.push({ - path: [key, ...(issue.path || []).map(String)], - message: issue.message || "Validation failed", + path: [key], + message: issue.message, }); } } else { - result[key] = validationResult.value; + value[key] = result.value; } } @@ -183,74 +142,52 @@ function validateStandardSchemaMapping( return { success: false, issues }; } - return { success: true, value: result }; + return { success: true, value }; } -/** - * Detect what type of mapping we have - */ -function detectMappingType( - def: SchemaShape, -): { type: "standard-schema" } | { type: "arktype" } { +function detectMappingType(def: SchemaShape): { + type: "standard-schema" | "arktype"; +} { let hasStandardSchema = false; let hasArktype = false; for (const validator of Object.values(def)) { if (isArktype(validator)) { hasArktype = true; + break; } else if ((validator as StandardSchemaV1)?.["~standard"]) { hasStandardSchema = true; } else { - // If it's a string definition or something else, it's ArkType-path hasArktype = true; + break; } } - // If all validators are ONLY Standard Schema (and NOT ArkType), use Standard Schema path - // This ensures that ArkType types (which implement Standard Schema) still go through validateArkType - if (hasStandardSchema && !hasArktype) { + if (hasArktype) { + return { type: "arktype" }; + } + + if (hasStandardSchema) { return { type: "standard-schema" }; } - // Otherwise, fallback to ArkType path return { type: "arktype" }; } -/** - * Validate and distill environment variables based on a schema. - * - * @param def - The environment variable schema definition. Can be a mapping of keys to validators, - * or a compiled ArkType schema. - * @param config - Optional configuration for validation and coercion. - * @returns The validated and distilled environment variables. - * @throws {ArkEnvError} If validation fails. - */ export function createEnv( - def: T, - config?: ArkEnvConfig, -): InferType; -export function createEnv( - def: T, - config?: ArkEnvConfig, -): InferType; -export function createEnv( def: T, config: ArkEnvConfig = {}, -): InferType { - const { env = process.env } = config; - - const { isStandard, isArkCompiled } = detectValidatorType(def); +): any { + const env = config.env ?? process.env; - // Guardrail: Block top-level Standard Schema (Zod, Valibot, etc.) - // Reusable type() schemas (ArkType) are allowed. - if (isStandard && !isArkCompiled) { - throw new Error( - "ArkEnv: arkenv() expects a mapping of { KEY: validator }, not a top-level Standard Schema (e.g. z.object()). " + - "Standard Schema validators are supported inside the mapping, or you can use ArkType's type() for top-level schemas.", - ); + if (isArktype(def)) { + const result = validateArkType(def, config, env); + if (!result.success) { + throw new ArkEnvError(result.issues); + } + return result.value; } - // Detect what type of mapping we have and route to the appropriate validator const mappingType = detectMappingType(def as SchemaShape); const result = @@ -262,5 +199,7 @@ export function createEnv( throw new ArkEnvError(result.issues); } - return result.value as InferType; + return result.value; } + +export default createEnv; diff --git a/packages/arkenv/src/type.ts b/packages/arkenv/src/type.ts index bd5d747a3..b372d1adc 100644 --- a/packages/arkenv/src/type.ts +++ b/packages/arkenv/src/type.ts @@ -6,10 +6,23 @@ import { $ } from "@repo/scope"; */ export const type: typeof $.type = new Proxy((() => {}) as any, { get(_, prop) { + if (prop === "isArktype") return true; return Reflect.get($.type, prop); }, apply(_, thisArg, args) { - return Reflect.apply($.type, thisArg === type ? $.type : thisArg, args); + const result = Reflect.apply( + $.type, + thisArg === type ? $.type : thisArg, + args, + ); + // If the result is a function/object, ensure it also has the marker + if ( + result && + (typeof result === "object" || typeof result === "function") + ) { + (result as any).isArktype = true; + } + return result; }, }); diff --git a/packages/arkenv/src/utils/coerce.ts b/packages/arkenv/src/utils/coerce.ts index c4a19885c..81986674a 100644 --- a/packages/arkenv/src/utils/coerce.ts +++ b/packages/arkenv/src/utils/coerce.ts @@ -1,344 +1,184 @@ -import { createRequire } from "node:module"; import { maybeBooleanFn, maybeJsonFn, maybeNumberFn } from "@repo/keywords"; +import { arktypeLoader } from "@repo/scope"; import type { JsonSchema, Type } from "arktype"; -const require = createRequire(import.meta.url); - -/** - * A marker used in the coercion path to indicate that the target - * is the *elements* of an array, rather than the array property itself. - */ -const ARRAY_ITEM_MARKER = "*"; - -/** - * @internal - * Information about a path in the schema that requires coercion. - */ -type CoercionTarget = { - path: string[]; - type: "primitive" | "array" | "object"; -}; - -/** - * Options for coercion behavior. - */ -export type CoerceOptions = { - /** - * format to use for array parsing - * @default "comma" - */ - arrayFormat?: "comma" | "json"; -}; - /** - * Recursively find all paths in a JSON Schema that require coercion. - * We prioritize "number", "integer", "boolean", "array", and "object" types. + * Coerce values in a record based on an ArkType schema's JSON representation. */ -const findCoercionPaths = ( - node: JsonSchema, - path: string[] = [], -): CoercionTarget[] => { - const results: CoercionTarget[] = []; - - if (typeof node === "boolean") { - return results; +export function coerce( + schema: Type, + env: Record, + config: { + numbers: boolean; + booleans: boolean; + objects: boolean; + }, +): Record { + const result: Record = {}; + + // Get JSON representation (custom format in ArkType 2.0) + let json: any; + try { + json = (schema as any).json; + } catch { + // No-op } - if ("const" in node) { - if (typeof node.const === "number" || typeof node.const === "boolean") { - results.push({ path: [...path], type: "primitive" }); + if (!json) { + for (const [key, value] of Object.entries(env)) { + if (value === undefined) continue; + let coerced: unknown = value; + if (config.numbers) coerced = maybeNumberFn(coerced); + if (config.booleans) coerced = maybeBooleanFn(coerced); + if (config.objects) coerced = maybeJsonFn(coerced); + result[key] = coerced; } + return result; } - if ("enum" in node && node.enum) { - if ( - node.enum.some((v) => typeof v === "number" || typeof v === "boolean") - ) { - results.push({ path: [...path], type: "primitive" }); + // Build a map of paths that need coercion + const coercionMap = new Map(); + traverseRepresentation(json, "", coercionMap); + + for (const [key, value] of Object.entries(env)) { + if (value === undefined) continue; + + const targetType = coercionMap.get(key); + + if (targetType === "number" && config.numbers) { + result[key] = maybeNumberFn(value); + } else if (targetType === "boolean" && config.booleans) { + result[key] = maybeBooleanFn(value); + } else if (targetType === "object" && config.objects) { + result[key] = maybeJsonFn(value); + } else if (key.includes(".") && config.objects) { + const parts = key.split("."); + let current = result; + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + if (!(part in current)) current[part] = {}; + current = current[part] as Record; + } + const lastPart = parts[parts.length - 1]; + let coerced: unknown = value; + if (config.numbers) coerced = maybeNumberFn(coerced); + if (config.booleans) coerced = maybeBooleanFn(coerced); + current[lastPart] = coerced; + } else { + result[key] = value; } } - if ("type" in node) { - if (node.type === "number" || node.type === "integer") { - results.push({ path: [...path], type: "primitive" }); - } else if (node.type === "boolean") { - results.push({ path: [...path], type: "primitive" }); - } else if (node.type === "object") { - // Check if this object has properties defined - // If it does, we want to coerce the whole object from a JSON string - // But we also want to recursively check nested properties - const hasProperties = - "properties" in node && - node.properties && - Object.keys(node.properties).length > 0; - - if (hasProperties) { - // Mark this path as needing object coercion (JSON parsing) - results.push({ path: [...path], type: "object" }); - } + return result; +} - // Also recursively check nested properties for their own coercions - if ("properties" in node && node.properties) { - for (const [key, prop] of Object.entries(node.properties)) { - results.push( - ...findCoercionPaths(prop as JsonSchema, [...path, key]), - ); +/** + * Traverse ArkType 2.0 custom JSON representation or standard JSON Schema. + */ +function traverseRepresentation( + schema: any, + path: string, + map: Map, +) { + if (!schema || typeof schema !== "object") return; + + // ArkType 2.0 format: { domain: 'object', required: [{ key, value }], optional: [...] } + const domain = schema.domain || schema.type; + + if (domain === "object") { + // Handle required properties + if (Array.isArray(schema.required)) { + for (const entry of schema.required) { + if (entry.key && entry.value) { + const fullPath = path ? `${path}.${entry.key}` : entry.key; + traverseRepresentation(entry.value, fullPath, map); + } else if (typeof entry === "string") { + // Standard JSON Schema path: required is just a list of keys + // but properties are in 'properties' field. } } - } else if (node.type === "array") { - // Mark the array itself as a target for splitting strings - results.push({ path: [...path], type: "array" }); + } - if ("items" in node && node.items) { - if (Array.isArray(node.items)) { - // Tuple traversal - node.items.forEach((item, index) => { - results.push( - ...findCoercionPaths(item as JsonSchema, [...path, `${index}`]), - ); - }); - } else { - // List traversal - results.push( - ...findCoercionPaths(node.items as JsonSchema, [ - ...path, - ARRAY_ITEM_MARKER, - ]), - ); + // Handle optional properties + if (Array.isArray(schema.optional)) { + for (const entry of schema.optional) { + if (entry.key && entry.value) { + const fullPath = path ? `${path}.${entry.key}` : entry.key; + traverseRepresentation(entry.value, fullPath, map); } } } - } - if ("anyOf" in node && node.anyOf) { - for (const branch of node.anyOf) { - results.push(...findCoercionPaths(branch as JsonSchema, path)); + // Handle standard JSON Schema properties + if (schema.properties) { + for (const [prop, propSchema] of Object.entries(schema.properties)) { + const fullPath = path ? `${path}.${prop}` : prop; + traverseRepresentation(propSchema, fullPath, map); + } } } - if ("allOf" in node && node.allOf) { - for (const branch of node.allOf) { - results.push(...findCoercionPaths(branch as JsonSchema, path)); - } + // Leaf nodes + if (domain === "number" || domain === "integer") { + map.set(path, "number"); + } else if (domain === "boolean") { + map.set(path, "boolean"); + } else if ( + domain === "object" && + !schema.required && + !schema.optional && + !schema.properties + ) { + map.set(path, "object"); } - if ("oneOf" in node && node.oneOf) { - for (const branch of node.oneOf) { - results.push(...findCoercionPaths(branch as JsonSchema, path)); + // Handle standard JSON Schema union types + if (Array.isArray(schema.anyOf)) { + for (const s of schema.anyOf) { + traverseRepresentation(s, path, map); } } - - // Deduplicate by path and type combination - const seen = new Set(); - return results.filter((t) => { - const key = JSON.stringify(t.path) + t.type; - if (seen.has(key)) return false; - seen.add(key); - return true; - }); -}; +} /** - * Apply coercion to a data object based on identified paths. + * Coerces a single value based on the inferred type from a schema. */ -const applyCoercion = ( - data: unknown, - targets: CoercionTarget[], - options: CoerceOptions = {}, -) => { - const { arrayFormat = "comma" } = options; - - // Helper to split string to array - const splitString = (val: string) => { - if (arrayFormat === "json") { - try { - return JSON.parse(val); - } catch { - return val; - } - } +export function coerceValue(def: unknown, value: unknown): unknown { + if (typeof value !== "string") return value; - if (!val.trim()) return []; - return val.split(",").map((s) => s.trim()); - }; - - if (typeof data !== "object" || data === null) { - // If root data needs coercion - if (targets.some((t) => t.path.length === 0)) { - const rootTarget = targets.find((t) => t.path.length === 0); - - if (rootTarget?.type === "object" && typeof data === "string") { - return maybeJsonFn(data); - } - - if (rootTarget?.type === "array" && typeof data === "string") { - return splitString(data); - } - - const asNumber = maybeNumberFn(data); - if (typeof asNumber === "number") { - return asNumber; - } - return maybeBooleanFn(data); - } - return data; - } + if ((def as any)?.["~standard"]) return value; - // Sort targets by path length to ensure parent objects/arrays are coerced before their children - const sortedTargets = [...targets].sort( - (a, b) => a.path.length - b.path.length, - ); - - const walk = ( - current: unknown, - targetPath: string[], - type: "primitive" | "array" | "object", - ) => { - if (!current || typeof current !== "object") return; - - if (targetPath.length === 0) { - return; - } - - // If we've reached the last key, apply coercion - if (targetPath.length === 1) { - const lastKey = targetPath[0]; - - if (lastKey === ARRAY_ITEM_MARKER) { - if (Array.isArray(current)) { - for (let i = 0; i < current.length; i++) { - const original = current[i]; - if (type === "primitive") { - const asNumber = maybeNumberFn(original); - if (typeof asNumber === "number") { - current[i] = asNumber; - } else { - current[i] = maybeBooleanFn(original); - } - } else if (type === "object") { - current[i] = maybeJsonFn(original); - } - } - } - return; - } - - const record = current as Record; - // biome-ignore lint/suspicious/noPrototypeBuiltins: ES2020 compatibility - if (Object.prototype.hasOwnProperty.call(record, lastKey)) { - const original = record[lastKey]; - - if (type === "array" && typeof original === "string") { - record[lastKey] = splitString(original); - return; - } - - if (type === "object" && typeof original === "string") { - record[lastKey] = maybeJsonFn(original); - return; - } - - if (Array.isArray(original)) { - if (type === "primitive") { - for (let i = 0; i < original.length; i++) { - const item = original[i]; - const asNumber = maybeNumberFn(item); - if (typeof asNumber === "number") { - original[i] = asNumber; - } else { - original[i] = maybeBooleanFn(item); - } - } - } - } else { - if (type === "primitive") { - const asNumber = maybeNumberFn(original); - // If numeric parsing didn't produce a number, try boolean coercion - if (typeof asNumber === "number") { - record[lastKey] = asNumber; - } else { - record[lastKey] = maybeBooleanFn(original); - } - } - } - } - return; - } - - // Recurse down - const [nextKey, ...rest] = targetPath; - - if (nextKey === ARRAY_ITEM_MARKER) { - if (Array.isArray(current)) { - for (const item of current) { - walk(item, rest, type); - } - } - return; - } - - const record = current as Record; - walk(record[nextKey], rest, type); - }; - - for (const target of sortedTargets) { - walk(data, target.path, target.type); - } - - return data; -}; + const at = arktypeLoader.load(); + const { type } = at; -/** - * Create a coercing wrapper around an ArkType schema using JSON Schema introspection. - * Pre-process input data to coerce string values to numbers/booleans at identified paths - * before validation. - */ -export function coerce(schema: T, options?: CoerceOptions): T { - if ( - schema === null || - (typeof schema !== "object" && typeof schema !== "function") - ) { - throw new Error("coerce: invalid schema - expected an object or function"); - } - if (!schema.in || typeof schema.in.toJsonSchema !== "function") { - throw new Error( - "coerce: invalid schema - missing in.toJsonSchema function", - ); - } - if (typeof schema.pipe !== "function") { - throw new Error("coerce: invalid schema - missing pipe function"); + let t: Type; + try { + t = typeof def === "string" ? type(def as any) : (def as Type); + } catch { + return value; } - let at: typeof import("arktype"); + let json: any; try { - at = require("arktype"); + json = (t as any).json; } catch { - throw new Error( - "coerce: ArkType is required for coercion. Please ensure 'arktype' is installed.", - ); + // No-op } - const { type } = at; - - // Use a fallback to handle unjsonifiable parts of the schema (like predicates) - // by preserving the base schema. This ensures that even if part of the schema - // cannot be fully represented in JSON Schema, we can still perform coercion - // for the parts that can. - const json = schema.in.toJsonSchema({ - fallback: (ctx: { base: unknown }) => ctx.base as any, - }); - const targets = findCoercionPaths(json); - if (targets.length === 0) { - return schema; + if (!json) { + let coerced: unknown = value; + coerced = maybeNumberFn(coerced); + if (typeof coerced === "number") return coerced; + coerced = maybeBooleanFn(coerced); + if (typeof coerced === "boolean") return coerced; + return maybeJsonFn(value); } - /* - * We use `type("unknown")` to start the pipeline, which initializes a default scope. - * Integrating the original `schema` with its custom scope `$` into this pipeline - * creates a scope mismatch in TypeScript ({} vs $). - * We cast to `BaseType` to assert the final contract is maintained. - */ - return type("unknown") - .pipe((data: unknown) => applyCoercion(data, targets, options)) - .pipe(schema) as unknown as T; + const domain = json.domain || json.type; + + if (domain === "number" || domain === "integer") return maybeNumberFn(value); + if (domain === "boolean") return maybeBooleanFn(value); + if (domain === "object") return maybeJsonFn(value); + + return value; } diff --git a/packages/internal/scope/src/index.ts b/packages/internal/scope/src/index.ts index 6a661e3c5..b08423834 100644 --- a/packages/internal/scope/src/index.ts +++ b/packages/internal/scope/src/index.ts @@ -1,10 +1,23 @@ -import type { scope as ArkScope, type as ArkType } from "arktype"; import { host, port } from "./keywords"; +import { arktypeLoader } from "./lazy-type"; export * from "./keywords"; -export { lazyType } from "./lazy-type"; +export { lazyType, arktypeLoader } from "./lazy-type"; -let _$: any; +/** + * Global cache for the realized scope using Symbol.for for cross-module coordination. + */ +const SCOPE_CACHE_SYMBOL = Symbol.for("__ARKENV_SCOPE_CACHE__"); +const G = globalThis as any; +G[SCOPE_CACHE_SYMBOL] ??= { scope: undefined }; + +/** + * Reset the realized scope and loader (for tests). + */ +export function resetScope() { + G[SCOPE_CACHE_SYMBOL].scope = undefined; + arktypeLoader.reset(); +} /** * The root scope for the ArkEnv library, @@ -21,31 +34,21 @@ export const $: ArkEnvScope = new Proxy( {}, { get(_, prop) { - if (!_$) { - // We don't use require("arktype") statically to avoid load-time errors - try { - const { scope, type } = require("arktype") as { - scope: typeof ArkScope; - type: typeof ArkType; - }; - - _$ = scope({ - string: type.module({ - ...type.keywords.string, - host, - }), - number: type.module({ - ...type.keywords.number, - port, - }), - }); - } catch { - throw new Error( - "ArkType is required when using `type()` or ArkType-specific schemas. Please install `arktype`.", - ); - } + if (!G[SCOPE_CACHE_SYMBOL].scope) { + const { scope, type } = arktypeLoader.load(); + + G[SCOPE_CACHE_SYMBOL].scope = scope({ + string: type.module({ + ...type.keywords.string, + host, + }), + number: type.module({ + ...type.keywords.number, + port, + }), + }); } - return (_$ as any)[prop]; + return (G[SCOPE_CACHE_SYMBOL].scope as any)[prop]; }, }, ) as any; diff --git a/packages/internal/scope/src/lazy-type.ts b/packages/internal/scope/src/lazy-type.ts index 1ec17f79e..9df0803c4 100644 --- a/packages/internal/scope/src/lazy-type.ts +++ b/packages/internal/scope/src/lazy-type.ts @@ -1,38 +1,56 @@ import { createRequire } from "node:module"; -import type { type as ArkType } from "arktype"; const require = createRequire(import.meta.url); -let _at: typeof import("arktype") | undefined; - /** - * Load ArkType lazily and cache the result. + * Internal loader state using Symbol.for for cross-module coordination. */ -function loadArkType(): typeof import("arktype") { - if (_at) return _at; - try { - _at = require("arktype"); - return _at!; - } catch (e: unknown) { - if ( - e instanceof Error && - "code" in e && - e.code === "MODULE_NOT_FOUND" && - e.message.includes("'arktype'") - ) { - throw new Error( - "ArkType is required when using ArkType-specific schemas (string definitions or type() calls). " + - "Please install 'arktype' as a dependency, or use Standard Schema validators like Zod instead.", - ); - } - throw e; - } +const LOADER_SYMBOL = Symbol.for("__ARKENV_ARKTYPE_LOADER__"); +const G = globalThis as any; + +const MISSING_ERROR = + "ArkType is required when using ArkType-specific schemas (string definitions or type() calls). " + + "Please install 'arktype' as a dependency, or use Standard Schema validators like Zod instead."; + +if (!G[LOADER_SYMBOL]) { + G[LOADER_SYMBOL] = { + at: undefined, + forceMissing: false, + load: (): typeof import("arktype") => { + if ( + G[LOADER_SYMBOL].forceMissing || + process.env.ARKENV_FORCE_MISSING === "true" + ) { + throw new Error(MISSING_ERROR); + } + if (G[LOADER_SYMBOL].at) return G[LOADER_SYMBOL].at; + try { + G[LOADER_SYMBOL].at = require("arktype"); + return G[LOADER_SYMBOL].at!; + } catch (e: unknown) { + if ( + e instanceof Error && + "code" in e && + e.code === "MODULE_NOT_FOUND" && + e.message.includes("'arktype'") + ) { + throw new Error(MISSING_ERROR); + } + throw e; + } + }, + reset: () => { + G[LOADER_SYMBOL].at = undefined; + G[LOADER_SYMBOL].forceMissing = false; + delete process.env.ARKENV_FORCE_MISSING; + }, + }; } +export const arktypeLoader = G[LOADER_SYMBOL]; + /** * A lazy proxy that defers loading ArkType until it is actually needed. - * This allows internal packages to use ArkType syntax without creating - * static import dependencies that would break when ArkType is not installed. */ type LazyTypeProxy = { @@ -43,25 +61,20 @@ type LazyTypeProxy = { /** * Create a lazy proxy for an ArkType definition. - * The proxy intercepts property access and method calls, only loading - * ArkType when the validator is actually used or inspected. */ function createLazyProxy(state: LazyTypeProxy): any { return new Proxy(() => {}, { get(_, prop) { - // Handle .pipe() - chain morphs without realizing the type yet + if (prop === "isArktype") return true; if (prop === "pipe") { return (morph: (value: unknown) => unknown) => { state.morphs.push(morph); return createLazyProxy(state); }; } - - // For any other property access, we need to realize the type return realizeType(state)[prop]; }, apply(_, __, args) { - // If the proxy itself is called as a validator const realized = realizeType(state); return realized(...args); }, @@ -70,20 +83,17 @@ function createLazyProxy(state: LazyTypeProxy): any { /** * Realize the actual ArkType type from the lazy proxy state. - * This is where we actually require("arktype") and build the type. */ function realizeType(state: LazyTypeProxy): any { if (state._realized) { return state._realized; } - const at = loadArkType(); + const at = arktypeLoader.load(); const { type } = at; - // Build the type from the definition let realized = type(state.def as any); - // Apply any chained morphs for (const morph of state.morphs) { realized = realized.pipe(morph); } @@ -94,17 +104,15 @@ function realizeType(state: LazyTypeProxy): any { /** * The lazy type function that mimics ArkType's `type()` API. - * This is typed as ArkType's type function for full DX compatibility, - * but returns a lazy proxy that defers loading ArkType until needed. */ +import type { type as ArkType } from "arktype"; export const lazyType = new Proxy(() => {}, { apply(_, __, [def]) { return createLazyProxy({ def, morphs: [] }); }, get(_, prop) { - // Forward static properties from the real type function - // This ensures things like type.keywords work correctly - const at = loadArkType(); + if (prop === "isArktype") return true; + const at = arktypeLoader.load(); return (at.type as any)[prop]; }, }) as typeof ArkType; From 830548b3bc87359ee847b039f0238e0d23296d7c Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Tue, 13 Jan 2026 22:00:25 +0500 Subject: [PATCH 084/117] refactor: improve internal type and logic consistency - Change ArkEnvConfig to type alias - Use const for schema variable - Reorder exports for consistency --- packages/arkenv/src/create-env.ts | 9 +++++---- packages/internal/scope/src/index.ts | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index e15e6ca57..66d033ab9 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -7,7 +7,7 @@ import { coerce } from "./utils/coerce"; /** * The configuration for the ArkEnv library. */ -export interface ArkEnvConfig { +export type ArkEnvConfig = { env?: Record; coerce?: | boolean @@ -16,7 +16,7 @@ export interface ArkEnvConfig { booleans?: boolean; objects?: boolean; }; -} +}; type SchemaShape = Record; @@ -68,7 +68,7 @@ function validateArkType( ): { success: true; value: unknown } | { success: false; issues: EnvIssue[] } { const { isArkCompiled: isCompiledType } = detectValidatorType(def); - let schema = isCompiledType + const schema = isCompiledType ? (def as Type) : ($.type(def as any) as unknown as Type); @@ -155,7 +155,8 @@ function detectMappingType(def: SchemaShape): { if (isArktype(validator)) { hasArktype = true; break; - } else if ((validator as StandardSchemaV1)?.["~standard"]) { + } + if ((validator as StandardSchemaV1)?.["~standard"]) { hasStandardSchema = true; } else { hasArktype = true; diff --git a/packages/internal/scope/src/index.ts b/packages/internal/scope/src/index.ts index b08423834..01cafe682 100644 --- a/packages/internal/scope/src/index.ts +++ b/packages/internal/scope/src/index.ts @@ -2,7 +2,7 @@ import { host, port } from "./keywords"; import { arktypeLoader } from "./lazy-type"; export * from "./keywords"; -export { lazyType, arktypeLoader } from "./lazy-type"; +export { arktypeLoader, lazyType } from "./lazy-type"; /** * Global cache for the realized scope using Symbol.for for cross-module coordination. From 906f5372ec18ec69b1f7669e1b304fe4709c4d12 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Tue, 13 Jan 2026 22:29:29 +0500 Subject: [PATCH 085/117] feat(arkenv): add advanced coercion and config options - Introduce onUndeclaredKey config for schema behavior - Add arrayFormat option for parsing array strings - Refactor coercion --- packages/arkenv/src/create-env.ts | 113 ++++++---- packages/arkenv/src/utils/coerce.ts | 339 ++++++++++++++++++++-------- 2 files changed, 324 insertions(+), 128 deletions(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 66d033ab9..34f8b6d4e 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -16,6 +16,8 @@ export type ArkEnvConfig = { booleans?: boolean; objects?: boolean; }; + onUndeclaredKey?: "ignore" | "reject" | "delete"; + arrayFormat?: "comma" | "json"; }; type SchemaShape = Record; @@ -36,18 +38,23 @@ function isArktype(def: unknown): boolean { return false; } - // ArkType 2.0 markers or our own lazy proxy - if ((def as any).isArktype === true) return true; + const defAny = def as any; - const arkKind = (def as any).arkKind; + // ArkType 2.0 markers or our own lazy proxy + if ( + defAny.isArktype === true || + defAny.arkKind === "type" || + defAny.arkKind === "generic" || + defAny.arkKind === "module" + ) { + return true; + } - // Narrow detection: focus on ArkType-specific metadata + // ArkType 2.0 often has these internal ones return ( - arkKind === "type" || - arkKind === "generic" || - arkKind === "module" || - (typeof (def as any).assert === "function" && - (def as any).infer !== undefined) + typeof defAny.traverseApply === "function" && + typeof defAny.traverseAllows === "function" && + defAny.json !== undefined ); } @@ -68,41 +75,58 @@ function validateArkType( ): { success: true; value: unknown } | { success: false; issues: EnvIssue[] } { const { isArkCompiled: isCompiledType } = detectValidatorType(def); - const schema = isCompiledType + let schema = isCompiledType ? (def as Type) : ($.type(def as any) as unknown as Type); - const coercionConfig = - config.coerce === false - ? { numbers: false, booleans: false, objects: false } - : { - numbers: - typeof config.coerce === "object" - ? (config.coerce.numbers ?? true) - : true, - booleans: - typeof config.coerce === "object" - ? (config.coerce.booleans ?? true) - : true, - objects: - typeof config.coerce === "object" - ? (config.coerce.objects ?? true) - : true, - }; - - const coercedEnv = coerce(schema, env, coercionConfig); + // Apply undeclared key policy if specified or needed + if (config.onUndeclaredKey) { + schema = (schema as any).onUndeclaredKey(config.onUndeclaredKey); + } else if (!isCompiledType) { + // Default behavior for mappings: strip extra keys unless explicitly ignored + schema = (schema as any).onUndeclaredKey("delete"); + } + + const coercionConfig = { + numbers: + config.coerce === false + ? false + : typeof config.coerce === "object" + ? (config.coerce.numbers ?? true) + : true, + booleans: + config.coerce === false + ? false + : typeof config.coerce === "object" + ? (config.coerce.booleans ?? true) + : true, + objects: + config.coerce === false + ? false + : typeof config.coerce === "object" + ? (config.coerce.objects ?? true) + : true, + arrayFormat: config.arrayFormat ?? "comma", + }; + + const coercedEnv = coerce(schema, env, coercionConfig as any); const result = schema(coercedEnv); if (isArkErrors(result)) { - const issues: EnvIssue[] = (result as any).map((error: any) => { - return { - path: Array.isArray(error.path) - ? error.path - : [String(error.path || "root")], - message: error.message, - }; - }); + const issues: EnvIssue[] = []; + const errors = result as any; + if (errors.byPath) { + for (const [path, error] of Object.entries(errors.byPath)) { + issues.push({ + path: path.split("."), + message: (error as any).message, + }); + } + } else { + // Fallback + issues.push({ path: ["root"], message: String(result) }); + } return { success: false, issues }; } @@ -181,14 +205,22 @@ export function createEnv( ): any { const env = config.env ?? process.env; - if (isArktype(def)) { + const { isArkCompiled, isStandard } = detectValidatorType(def); + + if (isArkCompiled) { const result = validateArkType(def, config, env); if (!result.success) { - throw new ArkEnvError(result.issues); + throw new ArkEnvError(result.issues as any); } return result.value; } + if (isStandard) { + throw new Error( + "ArkEnv expects a mapping of environment variables to validators, not a top-level Standard Schema (like a Zod object). Please pass an object instead, e.g. createEnv({ PORT: z.string() }).", + ); + } + const mappingType = detectMappingType(def as SchemaShape); const result = @@ -197,7 +229,8 @@ export function createEnv( : validateArkType(def, config, env); if (!result.success) { - throw new ArkEnvError(result.issues); + // Use result.issues which is mapped from ArkErrors if needed + throw new ArkEnvError(result.issues as any); } return result.value; diff --git a/packages/arkenv/src/utils/coerce.ts b/packages/arkenv/src/utils/coerce.ts index 81986674a..f6e269d37 100644 --- a/packages/arkenv/src/utils/coerce.ts +++ b/packages/arkenv/src/utils/coerce.ts @@ -1,20 +1,36 @@ import { maybeBooleanFn, maybeJsonFn, maybeNumberFn } from "@repo/keywords"; import { arktypeLoader } from "@repo/scope"; -import type { JsonSchema, Type } from "arktype"; +import type { Type } from "arktype"; + +export type CoerceConfig = { + numbers: boolean; + booleans: boolean; + objects: boolean; + arrayFormat: "comma" | "json"; +}; + +const DEFAULT_CONFIG: CoerceConfig = { + numbers: true, + booleans: true, + objects: true, + arrayFormat: "comma", +}; /** * Coerce values in a record based on an ArkType schema's JSON representation. */ export function coerce( schema: Type, - env: Record, - config: { - numbers: boolean; - booleans: boolean; - objects: boolean; - }, -): Record { - const result: Record = {}; + env?: Record | string, + config: CoerceConfig = DEFAULT_CONFIG, +): any { + // If env is not provided, return a wrapper function (compat with older tests) + if (arguments.length === 1) { + return (input: any) => { + const coerced = coerce(schema, input, config); + return schema(coerced); + }; + } // Get JSON representation (custom format in ArkType 2.0) let json: any; @@ -24,52 +40,164 @@ export function coerce( // No-op } - if (!json) { - for (const [key, value] of Object.entries(env)) { - if (value === undefined) continue; - let coerced: unknown = value; - if (config.numbers) coerced = maybeNumberFn(coerced); - if (config.booleans) coerced = maybeBooleanFn(coerced); - if (config.objects) coerced = maybeJsonFn(coerced); - result[key] = coerced; + // Handle primitive root schema + if (typeof env !== "object" || env === null) { + if (env === undefined) return undefined; + return coerceSingleValue(json, env, config); + } + + const coercionMap = new Map(); + if (json) { + traverseRepresentation(json, "", coercionMap); + } + + const result = coerceInternal(env, "", coercionMap, config); + + return result; +} + +function coerceInternal( + input: any, + path: string, + coercionMap: Map, + config: CoerceConfig, +): any { + if (input === undefined || input === null) return input; + + const getTargetType = (p: string) => { + if (coercionMap.has(p)) return coercionMap.get(p); + // More aggressive wildcard: replace all numeric segments with '*' + // e.g. SERVICES.0.NAME -> SERVICES.*.NAME, or 0 -> * + const wildcard = p + .split(".") + .map((s) => (/^\d+$/.test(s) ? "*" : s)) + .join("."); + if (wildcard !== p && coercionMap.has(wildcard)) { + return coercionMap.get(wildcard); + } + return undefined; + }; + + if (Array.isArray(input)) { + return input.map((v, i) => { + const fullPath = path ? `${path}.${i}` : `${i}`; + const targetType = getTargetType(fullPath); + + let coerced = v; + if (typeof v !== "object" || v === null) { + coerced = applyCoercion(v, targetType, config); + } + + if (typeof coerced === "object" && coerced !== null) { + return coerceInternal(coerced, fullPath, coercionMap, config); + } + return coerced; + }); + } + + if (typeof input === "object") { + const result: any = {}; + for (const [key, value] of Object.entries(input)) { + const fullPath = path ? `${path}.${key}` : key; + const targetType = getTargetType(fullPath); + + let coerced = value; + if (typeof value !== "object" || value === null) { + coerced = applyCoercion(value, targetType, config); + } + + if (typeof coerced === "object" && coerced !== null) { + result[key] = coerceInternal(coerced, fullPath, coercionMap, config); + } else if ( + typeof key === "string" && + key.includes(".") && + config.objects + ) { + const parts = key.split("."); + let current = result; + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + if (!(part in current)) current[part] = {}; + current = current[part] as Record; + } + const lastPart = parts[parts.length - 1]; + const inferredTargetType = getTargetType(key); + current[lastPart] = applyCoercion(value, inferredTargetType, config); + } else { + result[key] = coerced; + } } return result; } - // Build a map of paths that need coercion - const coercionMap = new Map(); - traverseRepresentation(json, "", coercionMap); + return applyCoercion(input, getTargetType(path), config); +} - for (const [key, value] of Object.entries(env)) { - if (value === undefined) continue; - - const targetType = coercionMap.get(key); - - if (targetType === "number" && config.numbers) { - result[key] = maybeNumberFn(value); - } else if (targetType === "boolean" && config.booleans) { - result[key] = maybeBooleanFn(value); - } else if (targetType === "object" && config.objects) { - result[key] = maybeJsonFn(value); - } else if (key.includes(".") && config.objects) { - const parts = key.split("."); - let current = result; - for (let i = 0; i < parts.length - 1; i++) { - const part = parts[i]; - if (!(part in current)) current[part] = {}; - current = current[part] as Record; +function applyCoercion( + value: any, + targetType: string | undefined, + config: CoerceConfig, +): any { + if (!targetType) return value; + + let coerced = value; + const types = targetType.split("|"); + + // Priority: object (JSON Parsing) > array (Splitting) > number > boolean + if (types.includes("object") && config.objects) { + coerced = maybeJsonFn(coerced); + } + + if (typeof coerced === "string") { + if (types.includes("array")) { + if ( + config.arrayFormat === "json" || + coerced.startsWith("[") || + coerced.startsWith("{") + ) { + const parsed = maybeJsonFn(coerced); + if (Array.isArray(parsed)) return parsed; } - const lastPart = parts[parts.length - 1]; - let coerced: unknown = value; - if (config.numbers) coerced = maybeNumberFn(coerced); - if (config.booleans) coerced = maybeBooleanFn(coerced); - current[lastPart] = coerced; - } else { - result[key] = value; + + if (config.arrayFormat === "comma" || config.arrayFormat === undefined) { + return coerced === "" + ? [] + : coerced + .split(",") + .map((x) => x.trim()) + .filter((x) => x !== ""); + } + } + + if (types.includes("number") && config.numbers) { + coerced = maybeNumberFn(coerced); + } + if ( + typeof coerced === "string" && + types.includes("boolean") && + config.booleans + ) { + coerced = maybeBooleanFn(coerced); } } - return result; + return coerced; +} + +function coerceSimpleValue(value: any, config: CoerceConfig): any { + let coerced = value; + if (config.numbers) coerced = maybeNumberFn(coerced); + if (config.booleans) coerced = maybeBooleanFn(coerced); + if (config.objects) coerced = maybeJsonFn(coerced); + return coerced; +} + +function coerceSingleValue(json: any, value: any, config: CoerceConfig): any { + if (!json) return coerceSimpleValue(value, config); + + const coercionMap = new Map(); + traverseRepresentation(json, "", coercionMap); + return applyCoercion(value, coercionMap.get(""), config); } /** @@ -78,28 +206,86 @@ export function coerce( function traverseRepresentation( schema: any, path: string, - map: Map, + map: Map, ) { - if (!schema || typeof schema !== "object") return; + if (schema === undefined || schema === null) return; + + const recordType = (type: string, p = path) => { + const existing = map.get(p); + if (!existing) { + map.set(p, type); + } else if (!existing.includes(type)) { + map.set(p, `${existing}|${type}`); + } + }; + + // Handle string shorthand for domains + if (typeof schema === "string") { + if (schema === "number" || schema === "integer") recordType("number"); + else if (schema === "boolean") recordType("boolean"); + else if (schema === "object") recordType("object"); + return; + } + + // Handle Unions represented as arrays + if (Array.isArray(schema)) { + for (const branch of schema) { + traverseRepresentation(branch, path, map); + } + return; + } + + if (typeof schema !== "object") return; + + // Leaf nodes + if ( + schema.unit === "NaN" || + schema.unit === "Infinity" || + schema.unit === "-Infinity" || + typeof schema.unit === "number" + ) { + recordType("number"); + } else if (typeof schema.unit === "boolean") { + recordType("boolean"); + } + + // Nested domains (e.g. number.epoch) + let domain = schema.domain || schema.type; + if (domain && typeof domain === "object") { + domain = domain.domain || domain.type; + } - // ArkType 2.0 format: { domain: 'object', required: [{ key, value }], optional: [...] } - const domain = schema.domain || schema.type; + // Handle Arrays + if ( + schema.proto === "Array" || + domain === "Array" || + schema.sequence || + schema.items + ) { + recordType("array"); + recordType("object"); + const elementSchema = schema.sequence || schema.items; + if (elementSchema) { + const elementPath = path ? `${path}.*` : "*"; + traverseRepresentation(elementSchema, elementPath, map); + } + return; + } + + // Mark current path as object if it's an object domain + if (domain === "object") { + recordType("object"); + } if (domain === "object") { - // Handle required properties if (Array.isArray(schema.required)) { for (const entry of schema.required) { if (entry.key && entry.value) { const fullPath = path ? `${path}.${entry.key}` : entry.key; traverseRepresentation(entry.value, fullPath, map); - } else if (typeof entry === "string") { - // Standard JSON Schema path: required is just a list of keys - // but properties are in 'properties' field. } } } - - // Handle optional properties if (Array.isArray(schema.optional)) { for (const entry of schema.optional) { if (entry.key && entry.value) { @@ -108,8 +294,6 @@ function traverseRepresentation( } } } - - // Handle standard JSON Schema properties if (schema.properties) { for (const [prop, propSchema] of Object.entries(schema.properties)) { const fullPath = path ? `${path}.${prop}` : prop; @@ -118,23 +302,18 @@ function traverseRepresentation( } } - // Leaf nodes + // Base leaf domains if (domain === "number" || domain === "integer") { - map.set(path, "number"); + recordType("number"); } else if (domain === "boolean") { - map.set(path, "boolean"); - } else if ( - domain === "object" && - !schema.required && - !schema.optional && - !schema.properties - ) { - map.set(path, "object"); + recordType("boolean"); } - // Handle standard JSON Schema union types - if (Array.isArray(schema.anyOf)) { - for (const s of schema.anyOf) { + // Handle standard JSON Schema union types (anyOf / etc) + const unions = + schema.anyOf || schema.oneOf || schema.allOf || schema.branches; + if (Array.isArray(unions)) { + for (const s of unions) { traverseRepresentation(s, path, map); } } @@ -145,7 +324,6 @@ function traverseRepresentation( */ export function coerceValue(def: unknown, value: unknown): unknown { if (typeof value !== "string") return value; - if ((def as any)?.["~standard"]) return value; const at = arktypeLoader.load(); @@ -165,20 +343,5 @@ export function coerceValue(def: unknown, value: unknown): unknown { // No-op } - if (!json) { - let coerced: unknown = value; - coerced = maybeNumberFn(coerced); - if (typeof coerced === "number") return coerced; - coerced = maybeBooleanFn(coerced); - if (typeof coerced === "boolean") return coerced; - return maybeJsonFn(value); - } - - const domain = json.domain || json.type; - - if (domain === "number" || domain === "integer") return maybeNumberFn(value); - if (domain === "boolean") return maybeBooleanFn(value); - if (domain === "object") return maybeJsonFn(value); - - return value; + return coerceSingleValue(json, value, DEFAULT_CONFIG); } From 24a3722edfff9ac6850111a60121f06c1dbc77f3 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Tue, 13 Jan 2026 22:38:57 +0500 Subject: [PATCH 086/117] refactor: Improve internal code clarity and maintainability - Add maintainer notes to key internal functions - Refine ArkType lazy proxy type assertion - Correct internal import path for keywords --- packages/arkenv/src/create-env.ts | 8 ++++++++ packages/arkenv/src/index.ts | 2 -- packages/arkenv/src/utils/coerce.ts | 6 ++++++ packages/internal/scope/src/lazy-type.ts | 8 +++++++- packages/internal/scope/src/scope-def.ts | 2 +- 5 files changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 34f8b6d4e..9ccbfecaf 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -33,6 +33,14 @@ function detectValidatorType(def: unknown): { return { isArkCompiled, isStandard }; } +/** + * Detects if a definition is an ArkType validator. + * + * MAINTAINER NOTE: This is best-effort detection for ArkType 2.0. + * Future ArkType internals could shift; fallback behavior should ensure + * we error cleanly if detection fails but a validator is actually required. + * Do not promise this logic as public contract. + */ function isArktype(def: unknown): boolean { if (typeof def !== "function" && (typeof def !== "object" || def === null)) { return false; diff --git a/packages/arkenv/src/index.ts b/packages/arkenv/src/index.ts index 72677ad9b..d80d97165 100644 --- a/packages/arkenv/src/index.ts +++ b/packages/arkenv/src/index.ts @@ -1,7 +1,5 @@ import { createEnv } from "./create-env"; -export type { EnvSchema } from "./create-env"; - /** * `arkenv`'s main export, an alias for {@link createEnv} * diff --git a/packages/arkenv/src/utils/coerce.ts b/packages/arkenv/src/utils/coerce.ts index f6e269d37..9cce968a1 100644 --- a/packages/arkenv/src/utils/coerce.ts +++ b/packages/arkenv/src/utils/coerce.ts @@ -18,6 +18,12 @@ const DEFAULT_CONFIG: CoerceConfig = { /** * Coerce values in a record based on an ArkType schema's JSON representation. + * + * MAINTAINER NOTE: This is a mini-coercion engine. + * 1. Keep it isolated to arkenv's internal needs. + * 2. Resist feature creep (do not add specialized rules for every possible type). + * 3. Standard Schema validators (Zod, etc.) OWN their own coercion; arkenv only + * applies this engine to ArkType or raw object mappings. */ export function coerce( schema: Type, diff --git a/packages/internal/scope/src/lazy-type.ts b/packages/internal/scope/src/lazy-type.ts index 9df0803c4..38bdd96db 100644 --- a/packages/internal/scope/src/lazy-type.ts +++ b/packages/internal/scope/src/lazy-type.ts @@ -104,6 +104,12 @@ function realizeType(state: LazyTypeProxy): any { /** * The lazy type function that mimics ArkType's `type()` API. + * This is typed as ArkType's type function for full DX compatibility, + * but returns a lazy proxy that defers loading ArkType until needed. + * + * MAINTAINER NOTE: This is a best-effort internal proxy. + * If you use this in tests, you MUST call resetScope() in beforeEach/afterEach + * to avoid leaking state between test cases. */ import type { type as ArkType } from "arktype"; export const lazyType = new Proxy(() => {}, { @@ -115,4 +121,4 @@ export const lazyType = new Proxy(() => {}, { const at = arktypeLoader.load(); return (at.type as any)[prop]; }, -}) as typeof ArkType; +}) as unknown as typeof ArkType; diff --git a/packages/internal/scope/src/scope-def.ts b/packages/internal/scope/src/scope-def.ts index e03006f49..0a0179be5 100644 --- a/packages/internal/scope/src/scope-def.ts +++ b/packages/internal/scope/src/scope-def.ts @@ -1,4 +1,4 @@ -import { host, port } from "@repo/keywords"; +import { host, port } from "./keywords"; import { scope, type } from "arktype"; /** From da5ca1f9372c7562448843b74235a37ed7f9db19 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Tue, 13 Jan 2026 22:49:27 +0500 Subject: [PATCH 087/117] feat: improve createEnv type inference and schema flexibility - Enhance `createEnv` return type with `InferType` - Broaden `SchemaShape` to allow more schema definitions - --- packages/arkenv/src/create-env.ts | 92 +++++++++++++----------- packages/bun-plugin/src/index.ts | 9 +-- packages/internal/scope/src/scope-def.ts | 2 +- packages/vite-plugin/src/index.ts | 22 ++---- 4 files changed, 57 insertions(+), 68 deletions(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 9ccbfecaf..a93262d12 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -1,5 +1,5 @@ import { $ } from "@repo/scope"; -import type { StandardSchemaV1 } from "@repo/types"; +import type { InferType, StandardSchemaV1 } from "@repo/types"; import type { Type } from "arktype"; import { ArkEnvError, type EnvIssue } from "./errors"; import { coerce } from "./utils/coerce"; @@ -20,7 +20,11 @@ export type ArkEnvConfig = { arrayFormat?: "comma" | "json"; }; -type SchemaShape = Record; +/** + * A flexible schema definition that can be a raw object, compiled ArkType, + * or a Standard Schema validator. + */ +type SchemaShape = any; function detectValidatorType(def: unknown): { isArkCompiled: boolean; @@ -139,78 +143,80 @@ function validateArkType( return { success: false, issues }; } - return { success: true, value: result }; + return { success: true, value: result as any }; } function validateStandardSchemaMapping( - def: SchemaShape, + mapping: Record, env: Record, ): { success: true; value: unknown } | { success: false; issues: EnvIssue[] } { - const value: Record = {}; - const issues: EnvIssue[] = []; + const result: Record = {}; + const allIssues: EnvIssue[] = []; - for (const [key, validator] of Object.entries(def)) { - const result = (validator as StandardSchemaV1)["~standard"].validate( - env[key], - ); + for (const [key, validator] of Object.entries(mapping)) { + const value = env[key]; + const validationResult = validator["~standard"].validate(value); - if (result instanceof Promise) { - throw new Error("ArkEnv does not support asynchronous validators"); + if (validationResult instanceof Promise) { + throw new Error("Async validation is not supported in createEnv."); } - if (result.issues) { - for (const issue of result.issues) { - issues.push({ - path: [key], - message: issue.message, + if (validationResult.issues) { + for (const stdIssue of validationResult.issues) { + allIssues.push({ + path: [key, ...(stdIssue.path || [])].map(String), + message: stdIssue.message, }); } } else { - value[key] = result.value; + result[key] = validationResult.value; } } - if (issues.length > 0) { - return { success: false, issues }; + if (allIssues.length > 0) { + return { success: false, issues: allIssues }; } - return { success: true, value }; + return { success: true, value: result }; } -function detectMappingType(def: SchemaShape): { - type: "standard-schema" | "arktype"; +function detectMappingType(mapping: SchemaShape): { + type: "arktype" | "standard-schema"; } { - let hasStandardSchema = false; + let hasStandard = false; let hasArktype = false; - for (const validator of Object.values(def)) { - if (isArktype(validator)) { + for (const value of Object.values(mapping)) { + if (typeof value === "string" || isArktype(value)) { hasArktype = true; - break; + } else if ((value as any)?.["~standard"]) { + hasStandard = true; } - if ((validator as StandardSchemaV1)?.["~standard"]) { - hasStandardSchema = true; - } else { - hasArktype = true; - break; - } - } - - if (hasArktype) { - return { type: "arktype" }; } - if (hasStandardSchema) { - return { type: "standard-schema" }; - } + // Prioritize ArkType detection: if anything identifies as ArkType, + // we use the ArkType path which handles both ArkType and Standard Schema + // (via $.type wrapping). + if (hasArktype) return { type: "arktype" }; + if (hasStandard) return { type: "standard-schema" }; return { type: "arktype" }; } +/** + * Validates environment variables against a schema and returns the parsed result. + * + * {@link https://arkenv.js.org | ArkEnv} is a typesafe environment variables validator from editor to runtime. + * + * @param def - The schema definition (ArkType mapping, compiled ArkType, or Standard Schema mapping) + * @param config - Optional configuration for validation and coercion + * @returns The validated and parsed environment variables + * @throws {ArkEnvError} If validation fails + */ export function createEnv( def: T, config: ArkEnvConfig = {}, -): any { +): InferType { const env = config.env ?? process.env; const { isArkCompiled, isStandard } = detectValidatorType(def); @@ -220,7 +226,7 @@ export function createEnv( if (!result.success) { throw new ArkEnvError(result.issues as any); } - return result.value; + return result.value as InferType; } if (isStandard) { @@ -241,7 +247,7 @@ export function createEnv( throw new ArkEnvError(result.issues as any); } - return result.value; + return result.value as InferType; } export default createEnv; diff --git a/packages/bun-plugin/src/index.ts b/packages/bun-plugin/src/index.ts index 4d9286101..aea352eb9 100644 --- a/packages/bun-plugin/src/index.ts +++ b/packages/bun-plugin/src/index.ts @@ -1,6 +1,5 @@ import { join } from "node:path"; import type { EnvSchemaWithType, SchemaShape } from "@repo/types"; -import type { EnvSchema } from "arkenv"; import { createEnv } from "arkenv"; import type { BunPlugin, Loader, PluginBuilder } from "bun"; @@ -10,7 +9,7 @@ export type { ProcessEnvAugmented } from "./types"; * Helper to process env schema and return envMap */ export function processEnvSchema( - options: EnvSchema | EnvSchemaWithType, + options: T | EnvSchemaWithType, ): Map { // Validate environment variables @@ -125,11 +124,9 @@ function registerLoader(build: PluginBuilder, envMap: Map) { * ``` */ export function arkenv(options: EnvSchemaWithType): BunPlugin; +export function arkenv(options: T): BunPlugin; export function arkenv( - options: EnvSchema, -): BunPlugin; -export function arkenv( - options: EnvSchema | EnvSchemaWithType, + options: T | EnvSchemaWithType, ): BunPlugin { const envMap = processEnvSchema(options); diff --git a/packages/internal/scope/src/scope-def.ts b/packages/internal/scope/src/scope-def.ts index 0a0179be5..10a5f0409 100644 --- a/packages/internal/scope/src/scope-def.ts +++ b/packages/internal/scope/src/scope-def.ts @@ -1,5 +1,5 @@ -import { host, port } from "./keywords"; import { scope, type } from "arktype"; +import { host, port } from "./keywords"; /** * Definition of the scope for type inference purposes only. diff --git a/packages/vite-plugin/src/index.ts b/packages/vite-plugin/src/index.ts index 83f783907..0e4fd4cdb 100644 --- a/packages/vite-plugin/src/index.ts +++ b/packages/vite-plugin/src/index.ts @@ -1,14 +1,9 @@ import type { EnvSchemaWithType, SchemaShape } from "@repo/types"; -import { createEnv, type EnvSchema } from "arkenv"; +import { createEnv } from "arkenv"; import { loadEnv, type Plugin } from "vite"; export type { ImportMetaEnvAugmented } from "./types"; -/** - * TODO: If possible, find a better type than "const T extends SchemaShape", - * and be as close as possible to the type accepted by ArkType's `type`. - */ - /** * Vite plugin to validate environment variables using ArkEnv and expose them to client code. * @@ -16,8 +11,7 @@ export type { ImportMetaEnvAugmented } from "./types"; * automatically filters them based on Vite's `envPrefix` configuration (defaults to `"VITE_"`). * Only environment variables matching the prefix are exposed to client code via `import.meta.env.*`. * - * @param options - The environment variable schema definition. Can be an `EnvSchema` object - * for typesafe validation or an ArkType `EnvSchemaWithType` for dynamic schemas. + * @param options - The environment variable schema definition. * @returns A Vite plugin that validates environment variables and exposes them to the client. * * @example @@ -35,19 +29,11 @@ export type { ImportMetaEnvAugmented } from "./types"; * ], * }); * ``` - * - * @example - * ```ts - * // In your client code - * console.log(import.meta.env.VITE_API_URL); // Typesafe access - * ``` */ export default function arkenv(options: EnvSchemaWithType): Plugin; +export default function arkenv(options: T): Plugin; export default function arkenv( - options: EnvSchema, -): Plugin; -export default function arkenv( - options: EnvSchema | EnvSchemaWithType, + options: T | EnvSchemaWithType, ): Plugin { return { name: "@arkenv/vite-plugin", From 8c28508e459b37508f63d20d3550744c49d61c08 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Tue, 13 Jan 2026 23:11:46 +0500 Subject: [PATCH 088/117] fix(lazy-type): prevent piping after type realization - Add validation for pipe() method - Ensure pipe() called before type is realized - Add tests for pipe() realization behavior --- packages/arkenv/src/lazy-type.test.ts | 44 ++++++++++++++++++++++++ packages/internal/scope/src/lazy-type.ts | 5 +++ 2 files changed, 49 insertions(+) create mode 100644 packages/arkenv/src/lazy-type.test.ts diff --git a/packages/arkenv/src/lazy-type.test.ts b/packages/arkenv/src/lazy-type.test.ts new file mode 100644 index 000000000..1ed68c18a --- /dev/null +++ b/packages/arkenv/src/lazy-type.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it, afterEach } from "vitest"; +import { lazyType, resetScope } from "@repo/scope"; + +describe("LazyType Proxy", () => { + afterEach(() => { + resetScope(); + }); + + it("should throw an error if .pipe() is called after realization via property access", () => { + const schema = lazyType("string"); + + // Accessing 'json' realizes the type + const _ = (schema as any).json; + + // Now '.pipe()' should throw + expect(() => { + (schema as any).pipe((v: any) => v + "!"); + }).toThrow("Cannot pipe after the lazy type has been realized"); + }); + + it("should throw an error if .pipe() is called after realization via apply", () => { + const schema = lazyType("string"); + + // Calling it realizes the type + schema("test"); + + // Now '.pipe()' should throw + expect(() => { + (schema as any).pipe((v: any) => v + "!"); + }).toThrow("Cannot pipe after the lazy type has been realized"); + }); + + it("should allow chaining .pipe() calls before realization", () => { + const schema = lazyType("string") + .pipe((v: any) => v + "!") + .pipe((v: any) => v + "?") as any; + + // Still not realized + expect(schema.isArktype).toBe(true); + + const result = schema("test"); + expect(result).toBe("test!?"); + }); +}); diff --git a/packages/internal/scope/src/lazy-type.ts b/packages/internal/scope/src/lazy-type.ts index 38bdd96db..d989694cf 100644 --- a/packages/internal/scope/src/lazy-type.ts +++ b/packages/internal/scope/src/lazy-type.ts @@ -68,6 +68,11 @@ function createLazyProxy(state: LazyTypeProxy): any { if (prop === "isArktype") return true; if (prop === "pipe") { return (morph: (value: unknown) => unknown) => { + if (state._realized) { + throw new Error( + "Cannot pipe after the lazy type has been realized", + ); + } state.morphs.push(morph); return createLazyProxy(state); }; From 020690404d64aa7206d14a6a6c2ee517a836ef9d Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Tue, 13 Jan 2026 23:17:13 +0500 Subject: [PATCH 089/117] fix(types): update InferType for StandardSchemaV1 - Extract output type (O) from StandardSchemaV1 - Add MIT license and copyright to standard-schema.ts --- packages/internal/types/src/infer-type.ts | 4 ++-- packages/internal/types/src/standard-schema.ts | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/internal/types/src/infer-type.ts b/packages/internal/types/src/infer-type.ts index c6bb10cec..0298ac7ba 100644 --- a/packages/internal/types/src/infer-type.ts +++ b/packages/internal/types/src/infer-type.ts @@ -8,8 +8,8 @@ import type { StandardSchemaV1 } from "./standard-schema"; * @template T - The schema definition to infer from */ export type InferType = - T extends StandardSchemaV1 - ? U + T extends StandardSchemaV1 + ? O : T extends Type ? distill.Out : T extends { t: infer U } diff --git a/packages/internal/types/src/standard-schema.ts b/packages/internal/types/src/standard-schema.ts index 051465040..7d85f3948 100644 --- a/packages/internal/types/src/standard-schema.ts +++ b/packages/internal/types/src/standard-schema.ts @@ -1,5 +1,8 @@ /** * @see https://github.com/standard-schema/standard-schema/tree/3130ce43fdd848d9ab49dbb0458d04f18459961c/packages/spec + * + * Copied from standard-schema (MIT License) + * Copyright (c) 2024 Colin McDannell */ // biome-ignore-all lint/style/useConsistentTypeDefinitions: this file is copied from the standard-schema repo From f5b5b3d646a001d25dcaeaad53f3d3b40aaab551 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Tue, 13 Jan 2026 23:20:23 +0500 Subject: [PATCH 090/117] docs(arkenv): update JSDoc for createEnv and deps - Clarify @param and @throws in createEnv - Update arktype peer dependency to ^2.1.2 --- packages/arkenv/package.json | 2 +- packages/arkenv/src/create-env.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/arkenv/package.json b/packages/arkenv/package.json index e61d79e89..c682ef167 100644 --- a/packages/arkenv/package.json +++ b/packages/arkenv/package.json @@ -55,7 +55,7 @@ "zod": "catalog:" }, "peerDependencies": { - "arktype": "^2.1.22" + "arktype": "^2.1.28" }, "peerDependenciesMeta": { "arktype": { diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index a93262d12..46b273d5e 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -208,10 +208,10 @@ function detectMappingType(mapping: SchemaShape): { * * {@link https://arkenv.js.org | ArkEnv} is a typesafe environment variables validator from editor to runtime. * - * @param def - The schema definition (ArkType mapping, compiled ArkType, or Standard Schema mapping) + * @param def - The environment variable schema definition. Can be a mapping of keys to validators, or a compiled ArkType schema. * @param config - Optional configuration for validation and coercion * @returns The validated and parsed environment variables - * @throws {ArkEnvError} If validation fails + * @throws An {@link ArkEnvError | error} If validation fails. */ export function createEnv( def: T, From 54815b14fea807b0cffbf3977ac201ba226ddd98 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Tue, 13 Jan 2026 23:23:02 +0500 Subject: [PATCH 091/117] refactor(arkenv): improve JSDoc for config and fn - Add JSDoc to ArkEnvConfig properties - Update JSDoc for createEnv function - Clar --- packages/arkenv/src/create-env.ts | 35 ++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 46b273d5e..29266947e 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -8,15 +8,44 @@ import { coerce } from "./utils/coerce"; * The configuration for the ArkEnv library. */ export type ArkEnvConfig = { + /** + * The environment variables to validate. Defaults to `process.env`. + */ env?: Record; + /** + * Whether to coerce environment variables to their expected types. + * Defaults to `true`. + */ coerce?: | boolean | { + /** + * Whether to coerce environment variables to numbers. + * Defaults to `true`. + */ numbers?: boolean; + /** + * Whether to coerce environment variables to booleans. + * Defaults to `true`. + */ booleans?: boolean; + /** + * Whether to coerce environment variables to objects. + * Defaults to `true`. + */ objects?: boolean; }; + /** + * The policy for undeclared environment variables. + * + * @default "delete" + */ onUndeclaredKey?: "ignore" | "reject" | "delete"; + /** + * The format for array environment variables. + * + * @default "comma" + */ arrayFormat?: "comma" | "json"; }; @@ -204,13 +233,13 @@ function detectMappingType(mapping: SchemaShape): { } /** - * Validates environment variables against a schema and returns the parsed result. + * Validate environment variables against a schema and return the parsed result. * * {@link https://arkenv.js.org | ArkEnv} is a typesafe environment variables validator from editor to runtime. * * @param def - The environment variable schema definition. Can be a mapping of keys to validators, or a compiled ArkType schema. - * @param config - Optional configuration for validation and coercion - * @returns The validated and parsed environment variables + * @param config - Optional {@link ArkEnvConfig | configuration} for validation and coercion. + * @returns The validated and parsed environment variables. * @throws An {@link ArkEnvError | error} If validation fails. */ export function createEnv( From 2363ddfbe32fbf66a6a1f6d9f5b19748da9644e8 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:24:29 +0000 Subject: [PATCH 092/117] [autofix.ci] apply automated fixes --- packages/arkenv/src/lazy-type.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/arkenv/src/lazy-type.test.ts b/packages/arkenv/src/lazy-type.test.ts index 1ed68c18a..18313cf3b 100644 --- a/packages/arkenv/src/lazy-type.test.ts +++ b/packages/arkenv/src/lazy-type.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it, afterEach } from "vitest"; import { lazyType, resetScope } from "@repo/scope"; +import { afterEach, describe, expect, it } from "vitest"; describe("LazyType Proxy", () => { afterEach(() => { From 135c17c78896e1e9d6f585921f82041bdf78e17e Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Tue, 13 Jan 2026 23:37:37 +0500 Subject: [PATCH 093/117] build(keywords): bundle @repo/scope in config --- packages/internal/keywords/tsdown.config.ts | 1 + packages/vite-plugin/src/index.test.ts.tmp | 33 --------------------- 2 files changed, 1 insertion(+), 33 deletions(-) delete mode 100644 packages/vite-plugin/src/index.test.ts.tmp diff --git a/packages/internal/keywords/tsdown.config.ts b/packages/internal/keywords/tsdown.config.ts index b17832d5e..1d46d5bdb 100644 --- a/packages/internal/keywords/tsdown.config.ts +++ b/packages/internal/keywords/tsdown.config.ts @@ -8,5 +8,6 @@ export default defineConfig({ resolve: ["@repo/types"], }, external: ["arktype"], + noExternal: ["@repo/scope"], // Don't externalize @repo/scope - bundle it }); diff --git a/packages/vite-plugin/src/index.test.ts.tmp b/packages/vite-plugin/src/index.test.ts.tmp deleted file mode 100644 index a465bbd98..000000000 --- a/packages/vite-plugin/src/index.test.ts.tmp +++ /dev/null @@ -1,33 +0,0 @@ -import { readdirSync, readFileSync } from "node:fs"; -import { join } from "node:path"; -import * as vite from "vite"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { z } from "zod"; -import arkenvPlugin from "./index"; - -// Mock the arkenv module to capture calls -// Mock the arkenv module with a spy that calls the real implementation by default -vi.mock("arkenv", async (importActual) => { - const actual = await importActual(); - return { - ...actual, - default: vi.fn(actual.default), - createEnv: vi.fn(actual.createEnv), - }; -}); - -// Mock the vite module to capture loadEnv calls -vi.mock("vite", async (importActual) => { - const actual = await importActual(); - return { - ...actual, - loadEnv: vi.fn(actual.loadEnv), - }; -}); - -const fixturesDir = join(__dirname, "__fixtures__"); - -// Get the mocked functions -const { createEnv: mockCreateEnv } = vi.mocked(await import("arkenv")); - -const mockLoadEnv = vi.mocked(vite.loadEnv); From 950323d11b49352113bc0c5a417db85e70059625 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Tue, 13 Jan 2026 23:38:08 +0500 Subject: [PATCH 094/117] chore(vite-plugin): remove duplicate keyword --- packages/vite-plugin/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/vite-plugin/package.json b/packages/vite-plugin/package.json index d1727ceb0..b792bc776 100644 --- a/packages/vite-plugin/package.json +++ b/packages/vite-plugin/package.json @@ -42,7 +42,6 @@ "keywords": [ "arktype", "arkenv", - "arkenv", "environment", "variables", "vite", From 0f387cd9fa1377f84917dd4387ecf1724a2b9bba Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Wed, 14 Jan 2026 00:12:01 +0500 Subject: [PATCH 095/117] refactor(arkenv): relocate internal morph fns - Move maybeNumberFn, maybeBooleanFn, maybeJsonFn - Functions moved from @repo/keywords to arkenv - --- packages/arkenv/src/morphs.ts | 35 +++++++++++++++++ packages/arkenv/src/utils/coerce.ts | 2 +- packages/arkenv/tsconfig.json | 6 ++- packages/internal/scope/src/keywords.ts | 51 ------------------------- 4 files changed, 41 insertions(+), 53 deletions(-) create mode 100644 packages/arkenv/src/morphs.ts diff --git a/packages/arkenv/src/morphs.ts b/packages/arkenv/src/morphs.ts new file mode 100644 index 000000000..551432763 --- /dev/null +++ b/packages/arkenv/src/morphs.ts @@ -0,0 +1,35 @@ +/** + * A loose numeric morph function (internal use only). + */ +export const maybeNumberFn = (s: unknown) => { + if (typeof s === "number") return s; + if (typeof s !== "string") return s; + const trimmed = s.trim(); + if (trimmed === "") return s; + if (trimmed === "NaN") return Number.NaN; + const n = Number(trimmed); + return Number.isNaN(n) ? s : n; +}; + +/** + * A loose boolean morph function (internal use only). + */ +export const maybeBooleanFn = (s: unknown) => { + if (s === "true") return true; + if (s === "false") return false; + return s; +}; + +/** + * A loose JSON morph function (internal use only). + */ +export const maybeJsonFn = (s: unknown) => { + if (typeof s !== "string") return s; + const trimmed = s.trim(); + if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return s; + try { + return JSON.parse(trimmed); + } catch { + return s; + } +}; diff --git a/packages/arkenv/src/utils/coerce.ts b/packages/arkenv/src/utils/coerce.ts index 9cce968a1..91c329c69 100644 --- a/packages/arkenv/src/utils/coerce.ts +++ b/packages/arkenv/src/utils/coerce.ts @@ -1,6 +1,6 @@ -import { maybeBooleanFn, maybeJsonFn, maybeNumberFn } from "@repo/keywords"; import { arktypeLoader } from "@repo/scope"; import type { Type } from "arktype"; +import { maybeBooleanFn, maybeJsonFn, maybeNumberFn } from "@/morphs"; export type CoerceConfig = { numbers: boolean; diff --git a/packages/arkenv/tsconfig.json b/packages/arkenv/tsconfig.json index 56793c550..02abaf57a 100644 --- a/packages/arkenv/tsconfig.json +++ b/packages/arkenv/tsconfig.json @@ -13,7 +13,11 @@ "resolveJsonModule": true, "rootDir": "src", "declaration": true, - "declarationMap": true + "declarationMap": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } }, "include": ["src"] } diff --git a/packages/internal/scope/src/keywords.ts b/packages/internal/scope/src/keywords.ts index ef9423db4..4e672a2dc 100644 --- a/packages/internal/scope/src/keywords.ts +++ b/packages/internal/scope/src/keywords.ts @@ -1,56 +1,5 @@ import { lazyType as type } from "./lazy-type"; -/** - * A loose numeric morph function (internal use only). - */ -export const maybeNumberFn = (s: unknown) => { - if (typeof s === "number") return s; - if (typeof s !== "string") return s; - const trimmed = s.trim(); - if (trimmed === "") return s; - if (trimmed === "NaN") return Number.NaN; - const n = Number(trimmed); - return Number.isNaN(n) ? s : n; -}; - -/** - * A loose numeric morph. - */ -export const maybeNumber = type("unknown").pipe(maybeNumberFn); - -/** - * A loose boolean morph function (internal use only). - */ -export const maybeBooleanFn = (s: unknown) => { - if (s === "true") return true; - if (s === "false") return false; - return s; -}; - -/** - * A loose boolean morph. - */ -export const maybeBoolean = type("unknown").pipe(maybeBooleanFn); - -/** - * A loose JSON morph function (internal use only). - */ -export const maybeJsonFn = (s: unknown) => { - if (typeof s !== "string") return s; - const trimmed = s.trim(); - if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return s; - try { - return JSON.parse(trimmed); - } catch { - return s; - } -}; - -/** - * A loose JSON morph. - */ -export const maybeJson = type("unknown").pipe(maybeJsonFn); - /** * A `number` integer between 0 and 65535. */ From 77539a0d129b1614598944af9191b7a67223559b Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Wed, 14 Jan 2026 00:12:23 +0500 Subject: [PATCH 096/117] test(arkenv/morphs): add unit tests for morph functions - Cover maybeNumberFn conversions - Validate maybeBooleanFn logic - Ensure maybeJsonFn parsing is correct - Test various valid and invalid inputs - Use vitest for the test suite --- packages/arkenv/src/morphs.test.ts | 96 ++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 packages/arkenv/src/morphs.test.ts diff --git a/packages/arkenv/src/morphs.test.ts b/packages/arkenv/src/morphs.test.ts new file mode 100644 index 000000000..b20ff1814 --- /dev/null +++ b/packages/arkenv/src/morphs.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from "vitest"; +import { maybeBooleanFn, maybeJsonFn, maybeNumberFn } from "./morphs"; + +describe("Morph Functions", () => { + describe("maybeNumberFn", () => { + it("should return numbers as-is", () => { + expect(maybeNumberFn(42)).toBe(42); + expect(maybeNumberFn(0)).toBe(0); + expect(maybeNumberFn(-5.5)).toBe(-5.5); + }); + + it("should convert numeric strings to numbers", () => { + expect(maybeNumberFn("42")).toBe(42); + expect(maybeNumberFn("0")).toBe(0); + expect(maybeNumberFn("-5.5")).toBe(-5.5); + expect(maybeNumberFn(" 123 ")).toBe(123); + }); + + it("should handle NaN string", () => { + expect(maybeNumberFn("NaN")).toBeNaN(); + }); + + it("should return non-numeric strings as-is", () => { + expect(maybeNumberFn("hello")).toBe("hello"); + expect(maybeNumberFn("not a number")).toBe("not a number"); + }); + + it("should return empty strings as-is", () => { + expect(maybeNumberFn("")).toBe(""); + expect(maybeNumberFn(" ")).toBe(" "); + }); + + it("should return non-string, non-number values as-is", () => { + const obj = { foo: "bar" }; + expect(maybeNumberFn(obj)).toBe(obj); + expect(maybeNumberFn(null)).toBe(null); + expect(maybeNumberFn(undefined)).toBe(undefined); + }); + }); + + describe("maybeBooleanFn", () => { + it("should convert 'true' string to boolean true", () => { + expect(maybeBooleanFn("true")).toBe(true); + }); + + it("should convert 'false' string to boolean false", () => { + expect(maybeBooleanFn("false")).toBe(false); + }); + + it("should return other values as-is", () => { + expect(maybeBooleanFn("yes")).toBe("yes"); + expect(maybeBooleanFn("no")).toBe("no"); + expect(maybeBooleanFn("1")).toBe("1"); + expect(maybeBooleanFn("0")).toBe("0"); + expect(maybeBooleanFn(true)).toBe(true); + expect(maybeBooleanFn(false)).toBe(false); + expect(maybeBooleanFn(42)).toBe(42); + }); + }); + + describe("maybeJsonFn", () => { + it("should parse valid JSON objects", () => { + expect(maybeJsonFn('{"foo":"bar"}')).toEqual({ foo: "bar" }); + expect(maybeJsonFn(' {"nested":{"value":123}} ')).toEqual({ + nested: { value: 123 }, + }); + }); + + it("should parse valid JSON arrays", () => { + expect(maybeJsonFn("[1,2,3]")).toEqual([1, 2, 3]); + expect(maybeJsonFn('["a","b","c"]')).toEqual(["a", "b", "c"]); + }); + + it("should return non-JSON strings as-is", () => { + expect(maybeJsonFn("hello")).toBe("hello"); + expect(maybeJsonFn("not json")).toBe("not json"); + }); + + it("should return strings that don't start with { or [ as-is", () => { + expect(maybeJsonFn("plain text")).toBe("plain text"); + expect(maybeJsonFn(" plain text ")).toBe(" plain text "); + }); + + it("should return invalid JSON as-is", () => { + expect(maybeJsonFn("{invalid}")).toBe("{invalid}"); + expect(maybeJsonFn("[broken")).toBe("[broken"); + }); + + it("should return non-string values as-is", () => { + expect(maybeJsonFn(42)).toBe(42); + expect(maybeJsonFn(true)).toBe(true); + const obj = { foo: "bar" }; + expect(maybeJsonFn(obj)).toBe(obj); + }); + }); +}); From bf1f78838defe702a7712a31b2fe49d78f118291 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Wed, 14 Jan 2026 00:14:44 +0500 Subject: [PATCH 097/117] build(vitest): enable TS path aliases - Add vite-tsconfig-paths dev dependency - Configure Vitest with tsconfigPaths plugin - Enable TypeScript path aliases for tests --- packages/arkenv/package.json | 1 + packages/arkenv/vitest.config.ts | 2 ++ pnpm-lock.yaml | 3 +++ 3 files changed, 6 insertions(+) diff --git a/packages/arkenv/package.json b/packages/arkenv/package.json index c682ef167..a7e5263cd 100644 --- a/packages/arkenv/package.json +++ b/packages/arkenv/package.json @@ -51,6 +51,7 @@ "size-limit": "catalog:", "tsdown": "catalog:", "typescript": "catalog:", + "vite-tsconfig-paths": "catalog:", "vitest": "catalog:", "zod": "catalog:" }, diff --git a/packages/arkenv/vitest.config.ts b/packages/arkenv/vitest.config.ts index ac7bda0bc..cf7c3bd2f 100644 --- a/packages/arkenv/vitest.config.ts +++ b/packages/arkenv/vitest.config.ts @@ -1,7 +1,9 @@ import { defineProject } from "vitest/config"; +import tsconfigPaths from "vite-tsconfig-paths"; export default defineProject({ test: { name: "arkenv", }, + plugins: [tsconfigPaths()], }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 449432d8b..bd2c297e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -566,6 +566,9 @@ importers: typescript: specifier: 'catalog:' version: 5.9.3 + vite-tsconfig-paths: + specifier: 'catalog:' + version: 6.0.3(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) vitest: specifier: 'catalog:' version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) From 4ab9ecfb2e335015270d6be83d214be87c1bc92b Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Wed, 14 Jan 2026 00:16:52 +0500 Subject: [PATCH 098/117] style(arkenv): reorder imports --- packages/arkenv/vitest.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/arkenv/vitest.config.ts b/packages/arkenv/vitest.config.ts index cf7c3bd2f..6d38c3bb2 100644 --- a/packages/arkenv/vitest.config.ts +++ b/packages/arkenv/vitest.config.ts @@ -1,5 +1,5 @@ -import { defineProject } from "vitest/config"; import tsconfigPaths from "vite-tsconfig-paths"; +import { defineProject } from "vitest/config"; export default defineProject({ test: { From b32c1d503103d33e8ac3624fdf651593f917fe7f Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Wed, 14 Jan 2026 00:21:41 +0500 Subject: [PATCH 099/117] refactor: remove @repo/keywords package - Consolidate keywords into @repo/scope - Remove unused maybeJson keyword - Update workspace and package dependencies --- arkenv.code-workspace | 4 - examples/without-arktype/package-lock.json | 1 - packages/arkenv/package.json | 1 - packages/internal/keywords/CHANGELOG.md | 39 ---------- packages/internal/keywords/README.md | 3 - packages/internal/keywords/package.json | 47 ------------ packages/internal/keywords/src/index.test.ts | 80 -------------------- packages/internal/keywords/src/index.ts | 1 - packages/internal/keywords/tsconfig.json | 19 ----- packages/internal/keywords/tsdown.config.ts | 13 ---- packages/internal/keywords/vitest.config.ts | 7 -- packages/internal/scope/src/keywords.test.ts | 37 +++++++++ 12 files changed, 37 insertions(+), 215 deletions(-) delete mode 100644 packages/internal/keywords/CHANGELOG.md delete mode 100644 packages/internal/keywords/README.md delete mode 100644 packages/internal/keywords/package.json delete mode 100644 packages/internal/keywords/src/index.test.ts delete mode 100644 packages/internal/keywords/src/index.ts delete mode 100644 packages/internal/keywords/tsconfig.json delete mode 100644 packages/internal/keywords/tsdown.config.ts delete mode 100644 packages/internal/keywords/vitest.config.ts create mode 100644 packages/internal/scope/src/keywords.test.ts diff --git a/arkenv.code-workspace b/arkenv.code-workspace index 80b8d6810..129c6d7b2 100644 --- a/arkenv.code-workspace +++ b/arkenv.code-workspace @@ -45,10 +45,6 @@ "path": "packages/internal/scope", "name": " @repo/scope" }, - { - "path": "packages/internal/keywords", - "name": " @repo/keywords" - }, { "path": "tooling", "name": "tooling" diff --git a/examples/without-arktype/package-lock.json b/examples/without-arktype/package-lock.json index f521d9e72..690a30d45 100644 --- a/examples/without-arktype/package-lock.json +++ b/examples/without-arktype/package-lock.json @@ -412,7 +412,6 @@ "extraneous": true, "license": "MIT", "devDependencies": { - "@repo/keywords": "workspace:*", "@repo/scope": "workspace:*", "@repo/types": "workspace:*", "@size-limit/esbuild-why": "catalog:", diff --git a/packages/arkenv/package.json b/packages/arkenv/package.json index a7e5263cd..1242a3dff 100644 --- a/packages/arkenv/package.json +++ b/packages/arkenv/package.json @@ -40,7 +40,6 @@ "bugs": "https://github.com/yamcodes/arkenv/labels/arkenv", "author": "Yam Borodetsky ", "devDependencies": { - "@repo/keywords": "workspace:*", "@repo/scope": "workspace:*", "@repo/types": "workspace:*", "@size-limit/esbuild-why": "catalog:", diff --git a/packages/internal/keywords/CHANGELOG.md b/packages/internal/keywords/CHANGELOG.md deleted file mode 100644 index c5175d487..000000000 --- a/packages/internal/keywords/CHANGELOG.md +++ /dev/null @@ -1,39 +0,0 @@ -# @repo/keywords - -## 0.2.1 - -### Patch Changes - -- #### Add `maybeJson` keyword _[`#694`](https://github.com/yamcodes/arkenv/pull/694) [`01c1704`](https://github.com/yamcodes/arkenv/commit/01c17041029a41f2dfcacd7dd7ed2d1cd5a8c058) [@copilot-swe-agent](https://github.com/apps/copilot-swe-agent)_ - - JSDoc: - - ```ts - /** - * A loose JSON morph. - * - * **In**: `unknown` - * - * **Out**: A parsed JSON object if the input is a valid JSON string; otherwise the original input. - * - * Useful for coercion in unions where failing on non-JSON strings would block other branches. - */ - ``` - -## 0.2.0 - -### Minor Changes - -- #### Rename `maybeParsedNumber`/`maybeParsedBoolean` to `maybeNumber`/`maybeBoolean` _[`#693`](https://github.com/yamcodes/arkenv/pull/693) [`7919b6d`](https://github.com/yamcodes/arkenv/commit/7919b6dcd171553d0e6e6e819a862408284e1f71) [@yamcodes](https://github.com/yamcodes)_ - - Rename these types for brevity and clarity. - -## 0.1.0 - -### Minor Changes - -- #### Simplify keywords for central coercion _[`#569`](https://github.com/yamcodes/arkenv/pull/569) [`adaada4`](https://github.com/yamcodes/arkenv/commit/adaada4d214c152e8d23c983aea1747d81a0e539) [@yamcodes](https://github.com/yamcodes)_ - - - **BREAKING**: The `boolean` keyword has been removed. Universal boolean coercion is now handled by the `arkenv` package. - - **BREAKING**: The `port` keyword has been changed from a `string -> number` morph to a pure `number` refinement. Numeric coercion is now handled centrally. - - Added `maybeParsedNumber` and `maybeParsedBoolean` internal morphs to support central coercion (including specific "NaN" support). diff --git a/packages/internal/keywords/README.md b/packages/internal/keywords/README.md deleted file mode 100644 index 9500dfd47..000000000 --- a/packages/internal/keywords/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `@repo/keywords` - -Internal keywords for ArkEnv. diff --git a/packages/internal/keywords/package.json b/packages/internal/keywords/package.json deleted file mode 100644 index 042be34d0..000000000 --- a/packages/internal/keywords/package.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "@repo/keywords", - "private": true, - "type": "module", - "version": "0.2.1", - "main": "./dist/index.cjs", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "description": "ArkEnv keywords (internal usage)", - "exports": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "require": "./dist/index.cjs" - }, - "scripts": { - "build": "tsdown", - "size": "size-limit", - "typecheck": "tsc --noEmit", - "clean": "rimraf dist node_modules", - "test": "vitest", - "fix": "pnpm -w run fix" - }, - "files": [ - "dist" - ], - "keywords": [ - "keywords" - ], - "license": "MIT", - "homepage": "https://arkenv.js.org", - "repository": { - "type": "git", - "url": "git+https://github.com/yamcodes/arkenv.git" - }, - "author": "Yam Borodetsky ", - "dependencies": { - "@repo/scope": "workspace:*" - }, - "devDependencies": { - "arktype": "catalog:", - "tsdown": "catalog:", - "typescript": "catalog:" - }, - "peerDependencies": { - "arktype": "catalog:" - } -} diff --git a/packages/internal/keywords/src/index.test.ts b/packages/internal/keywords/src/index.test.ts deleted file mode 100644 index 4aeffb010..000000000 --- a/packages/internal/keywords/src/index.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { host, maybeJson, port } from "./index"; - -describe("port", () => { - it("should validate a valid port number", () => { - const result = port.assert(8080); - expect(result).toBe(8080); - }); - - it("should throw when the port is not a number", () => { - expect(() => port.assert("invalid")).toThrow(); - }); - - it("should throw when the port is negative", () => { - expect(() => port.assert("-2")).toThrow(); - }); - - it("should throw when the port is too large", () => { - expect(() => port.assert("65536")).toThrow(); - }); -}); - -describe("host", () => { - it("should validate a valid IP address", () => { - const result = host.assert("127.0.0.1"); - expect(result).toBe("127.0.0.1"); - }); - - it("should validate localhost", () => { - const result = host.assert("localhost"); - expect(result).toBe("localhost"); - }); - - it("should throw when the host is invalid", () => { - expect(() => host.assert("invalid")).toThrow(); - }); -}); - -describe("maybeJson", () => { - it("should parse a valid JSON object string", () => { - const result = maybeJson('{"key": "value"}'); - expect(result).toEqual({ key: "value" }); - }); - - it("should parse a valid JSON array string", () => { - const result = maybeJson("[1, 2, 3]"); - expect(result).toEqual([1, 2, 3]); - }); - - it("should parse nested JSON objects", () => { - const result = maybeJson('{"nested": {"key": "value"}}'); - expect(result).toEqual({ nested: { key: "value" } }); - }); - - it("should return the original string if not valid JSON", () => { - const result = maybeJson("not json"); - expect(result).toBe("not json"); - }); - - it("should return the original string if it doesn't look like JSON", () => { - const result = maybeJson("simple string"); - expect(result).toBe("simple string"); - }); - - it("should return the original value if not a string", () => { - const result = maybeJson(42); - expect(result).toBe(42); - }); - - it("should return the original object if already an object", () => { - const obj = { key: "value" }; - const result = maybeJson(obj); - expect(result).toStrictEqual(obj); - }); - - it("should handle JSON strings with whitespace", () => { - const result = maybeJson(' {"key": "value"} '); - expect(result).toEqual({ key: "value" }); - }); -}); diff --git a/packages/internal/keywords/src/index.ts b/packages/internal/keywords/src/index.ts deleted file mode 100644 index 76f8fae56..000000000 --- a/packages/internal/keywords/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@repo/scope"; diff --git a/packages/internal/keywords/tsconfig.json b/packages/internal/keywords/tsconfig.json deleted file mode 100644 index 56793c550..000000000 --- a/packages/internal/keywords/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "target": "es2020", - "module": "preserve", - "strict": true, - "esModuleInterop": true, - "moduleResolution": "bundler", - "skipLibCheck": true, - "exactOptionalPropertyTypes": true, - "noImplicitAny": true, - "noEmit": true, - "outDir": "dist", - "resolveJsonModule": true, - "rootDir": "src", - "declaration": true, - "declarationMap": true - }, - "include": ["src"] -} diff --git a/packages/internal/keywords/tsdown.config.ts b/packages/internal/keywords/tsdown.config.ts deleted file mode 100644 index 1d46d5bdb..000000000 --- a/packages/internal/keywords/tsdown.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineConfig } from "tsdown"; - -export default defineConfig({ - format: ["esm", "cjs"], - minify: true, - fixedExtension: false, - dts: { - resolve: ["@repo/types"], - }, - external: ["arktype"], - noExternal: ["@repo/scope"], - // Don't externalize @repo/scope - bundle it -}); diff --git a/packages/internal/keywords/vitest.config.ts b/packages/internal/keywords/vitest.config.ts deleted file mode 100644 index d3f85876b..000000000 --- a/packages/internal/keywords/vitest.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineProject } from "vitest/config"; - -export default defineProject({ - test: { - name: "@repo/keywords", - }, -}); diff --git a/packages/internal/scope/src/keywords.test.ts b/packages/internal/scope/src/keywords.test.ts new file mode 100644 index 000000000..a948bc446 --- /dev/null +++ b/packages/internal/scope/src/keywords.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; +import { host, port } from "./keywords"; + +describe("port", () => { + it("should validate a valid port number", () => { + const result = port.assert(8080); + expect(result).toBe(8080); + }); + + it("should throw when the port is not a number", () => { + expect(() => port.assert("invalid")).toThrow(); + }); + + it("should throw when the port is negative", () => { + expect(() => port.assert("-2")).toThrow(); + }); + + it("should throw when the port is too large", () => { + expect(() => port.assert("65536")).toThrow(); + }); +}); + +describe("host", () => { + it("should validate a valid IP address", () => { + const result = host.assert("127.0.0.1"); + expect(result).toBe("127.0.0.1"); + }); + + it("should validate localhost", () => { + const result = host.assert("localhost"); + expect(result).toBe("localhost"); + }); + + it("should throw when the host is invalid", () => { + expect(() => host.assert("invalid")).toThrow(); + }); +}); From 4a8ee5ba91a0cae3d1d4ed2c7644d96edfeec316 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Wed, 14 Jan 2026 00:21:45 +0500 Subject: [PATCH 100/117] refactor(internal): remove keywords package - Migrate host and port keywords to @repo/scope - Delete @repo/keywords package and its files - Remove all references to @ --- pnpm-lock.yaml | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd2c297e2..1a3879ecc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -533,9 +533,6 @@ importers: packages/arkenv: devDependencies: - '@repo/keywords': - specifier: workspace:* - version: link:../internal/keywords '@repo/scope': specifier: workspace:* version: link:../internal/scope @@ -616,22 +613,6 @@ importers: specifier: 'catalog:' version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) - packages/internal/keywords: - dependencies: - '@repo/scope': - specifier: workspace:* - version: link:../scope - devDependencies: - arktype: - specifier: 'catalog:' - version: 2.1.29 - tsdown: - specifier: 'catalog:' - version: 0.18.4(typescript@5.9.3) - typescript: - specifier: 'catalog:' - version: 5.9.3 - packages/internal/scope: devDependencies: '@types/node': From 7bddd0995f7841bdba0b02bc30984c1dc770943d Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Wed, 14 Jan 2026 00:23:27 +0500 Subject: [PATCH 101/117] test(vitest): enable tsconfig paths plugin - Add vite-tsconfig-paths dependency - Apply plugin to bun-plugin Vitest config - Apply plugin to vite-plugin Vitest config --- packages/bun-plugin/package.json | 1 + packages/bun-plugin/vitest.config.ts | 2 ++ packages/vite-plugin/vitest.config.ts | 2 ++ pnpm-lock.yaml | 3 +++ 4 files changed, 8 insertions(+) diff --git a/packages/bun-plugin/package.json b/packages/bun-plugin/package.json index 5cb7c40a4..b4aa20330 100644 --- a/packages/bun-plugin/package.json +++ b/packages/bun-plugin/package.json @@ -22,6 +22,7 @@ "size-limit": "catalog:", "tsdown": "catalog:", "typescript": "catalog:", + "vite-tsconfig-paths": "catalog:", "vitest": "catalog:" }, "peerDependencies": { diff --git a/packages/bun-plugin/vitest.config.ts b/packages/bun-plugin/vitest.config.ts index 53978c91b..18f01b5fd 100644 --- a/packages/bun-plugin/vitest.config.ts +++ b/packages/bun-plugin/vitest.config.ts @@ -1,3 +1,4 @@ +import tsconfigPaths from "vite-tsconfig-paths"; import { defineConfig } from "vitest/config"; export default defineConfig({ @@ -11,4 +12,5 @@ export default defineConfig({ arkenv: new URL("../arkenv/src/index.ts", import.meta.url).pathname, }, }, + plugins: [tsconfigPaths()], }); diff --git a/packages/vite-plugin/vitest.config.ts b/packages/vite-plugin/vitest.config.ts index c4b099d0d..2c973f25c 100644 --- a/packages/vite-plugin/vitest.config.ts +++ b/packages/vite-plugin/vitest.config.ts @@ -1,4 +1,5 @@ import path from "node:path"; +import tsconfigPaths from "vite-tsconfig-paths"; import { defineProject } from "vitest/config"; export default defineProject({ @@ -10,4 +11,5 @@ export default defineProject({ arkenv: path.resolve(__dirname, "../arkenv/src/index.ts"), }, }, + plugins: [tsconfigPaths()], }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a3879ecc..acfe58acb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -609,6 +609,9 @@ importers: typescript: specifier: 'catalog:' version: 5.9.3 + vite-tsconfig-paths: + specifier: 'catalog:' + version: 6.0.3(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) vitest: specifier: 'catalog:' version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) From 11590ade7f7ae7ecc716a763ffe8009c42aa7080 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Wed, 14 Jan 2026 00:33:06 +0500 Subject: [PATCH 102/117] refactor(arkenv): use Dict type for env variables - Replaced Record with Dict - Imported Dict from @repo/types - Updated type definitions and function signatures --- packages/arkenv/src/create-env.ts | 8 ++++---- packages/arkenv/src/utils/coerce.ts | 3 ++- packages/internal/types/src/helpers.ts | 1 + packages/internal/types/src/index.ts | 6 ++---- 4 files changed, 9 insertions(+), 9 deletions(-) create mode 100644 packages/internal/types/src/helpers.ts diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 29266947e..73948cc78 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -1,5 +1,5 @@ import { $ } from "@repo/scope"; -import type { InferType, StandardSchemaV1 } from "@repo/types"; +import type { Dict, InferType, StandardSchemaV1 } from "@repo/types"; import type { Type } from "arktype"; import { ArkEnvError, type EnvIssue } from "./errors"; import { coerce } from "./utils/coerce"; @@ -11,7 +11,7 @@ export type ArkEnvConfig = { /** * The environment variables to validate. Defaults to `process.env`. */ - env?: Record; + env?: Dict; /** * Whether to coerce environment variables to their expected types. * Defaults to `true`. @@ -112,7 +112,7 @@ function isArkErrors(result: unknown): boolean { function validateArkType( def: unknown, config: ArkEnvConfig, - env: Record, + env: Dict, ): { success: true; value: unknown } | { success: false; issues: EnvIssue[] } { const { isArkCompiled: isCompiledType } = detectValidatorType(def); @@ -177,7 +177,7 @@ function validateArkType( function validateStandardSchemaMapping( mapping: Record, - env: Record, + env: Dict, ): { success: true; value: unknown } | { success: false; issues: EnvIssue[] } { const result: Record = {}; const allIssues: EnvIssue[] = []; diff --git a/packages/arkenv/src/utils/coerce.ts b/packages/arkenv/src/utils/coerce.ts index 91c329c69..e53338f15 100644 --- a/packages/arkenv/src/utils/coerce.ts +++ b/packages/arkenv/src/utils/coerce.ts @@ -1,4 +1,5 @@ import { arktypeLoader } from "@repo/scope"; +import type { Dict } from "@repo/types"; import type { Type } from "arktype"; import { maybeBooleanFn, maybeJsonFn, maybeNumberFn } from "@/morphs"; @@ -27,7 +28,7 @@ const DEFAULT_CONFIG: CoerceConfig = { */ export function coerce( schema: Type, - env?: Record | string, + env?: Dict | string, config: CoerceConfig = DEFAULT_CONFIG, ): any { // If env is not provided, return a wrapper function (compat with older tests) diff --git a/packages/internal/types/src/helpers.ts b/packages/internal/types/src/helpers.ts new file mode 100644 index 000000000..46cf7cb01 --- /dev/null +++ b/packages/internal/types/src/helpers.ts @@ -0,0 +1 @@ +export type Dict = Record; diff --git a/packages/internal/types/src/index.ts b/packages/internal/types/src/index.ts index 8e5de3843..11490eb65 100644 --- a/packages/internal/types/src/index.ts +++ b/packages/internal/types/src/index.ts @@ -1,14 +1,12 @@ /** - * Internal TypeScript/ArkType types shared across ArkEnv packages. + * Internal TypeScript types shared across ArkEnv packages. * * This package is not published to npm and is intended for internal use only * within the monorepo. */ -// Only TypeScript types export type * from "./filter-by-prefix"; +export * from "./helpers"; export type * from "./infer-type"; - -// Also includes ArkType types export * from "./schema"; export * from "./standard-schema"; From 30d392f5497f5018ed8a7f944b9f3da20fba6740 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Wed, 14 Jan 2026 00:34:35 +0500 Subject: [PATCH 103/117] docs: Improve JSDoc comments for ArkEnvConfig - Clarified default values in descriptions - Added @default tags for better tooling support - Updated formatting for readability --- packages/arkenv/src/create-env.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 73948cc78..da26146a3 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -9,29 +9,35 @@ import { coerce } from "./utils/coerce"; */ export type ArkEnvConfig = { /** - * The environment variables to validate. Defaults to `process.env`. + * The environment variables to validate. + * + * @default `process.env` */ env?: Dict; /** * Whether to coerce environment variables to their expected types. - * Defaults to `true`. + * + * @default `true` */ coerce?: | boolean | { /** * Whether to coerce environment variables to numbers. - * Defaults to `true`. + * + * @default `true` */ numbers?: boolean; /** * Whether to coerce environment variables to booleans. - * Defaults to `true`. + * + * @default `true` */ booleans?: boolean; /** * Whether to coerce environment variables to objects. - * Defaults to `true`. + * + * @default `true` */ objects?: boolean; }; From dc3189b3a6859eff127c12cf8ca4d84463d20eae Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Wed, 14 Jan 2026 00:36:05 +0500 Subject: [PATCH 104/117] docs(arkenv): improve onUndeclaredKey documentation - Clarified behavior of 'delete', 'ignore', and 'reject' - Added default behavior explanation - Linked to ArkType documentation --- packages/arkenv/src/create-env.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index da26146a3..9c47f0a3c 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -42,13 +42,25 @@ export type ArkEnvConfig = { objects?: boolean; }; /** - * The policy for undeclared environment variables. + * Control how ArkEnv handles environment variables that are not defined in your schema. + * + * Defaults to `'delete'` to ensure your output object only contains + * keys you've explicitly declared. This differs from ArkType's standard behavior, which + * mirrors TypeScript by defaulting to `'ignore'`. + * + * - `delete` (ArkEnv default): Undeclared keys are allowed on input but stripped from the output. + * - `ignore` (ArkType default): Undeclared keys are allowed and preserved in the output. + * - `reject`: Undeclared keys will cause validation to fail. * * @default "delete" + * @see https://arktype.io/docs/configuration#onundeclaredkey */ onUndeclaredKey?: "ignore" | "reject" | "delete"; /** - * The format for array environment variables. + * The format to use for array coercion (when `coerce` is enabled). + * + * - `comma` (default): Strings are split by comma and trimmed. + * - `json`: Strings are parsed as JSON. * * @default "comma" */ From 88c5447bf05801fcaf25123149d24993aa345a1b Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Wed, 14 Jan 2026 00:39:57 +0500 Subject: [PATCH 105/117] feat(arkenv): refactor coerce configuration - Extracted CoerceConfig to its own type - Simplified ArkEnvConfig type for coerce - Improved type safety for coerce options - Moved arrayFormat to CoerceConfig --- packages/arkenv/src/create-env.ts | 34 ++--------------------------- packages/arkenv/src/utils/coerce.ts | 26 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 32 deletions(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 9c47f0a3c..a76865cf3 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -2,7 +2,7 @@ import { $ } from "@repo/scope"; import type { Dict, InferType, StandardSchemaV1 } from "@repo/types"; import type { Type } from "arktype"; import { ArkEnvError, type EnvIssue } from "./errors"; -import { coerce } from "./utils/coerce"; +import { type CoerceConfig, coerce } from "./utils/coerce"; /** * The configuration for the ArkEnv library. @@ -19,28 +19,7 @@ export type ArkEnvConfig = { * * @default `true` */ - coerce?: - | boolean - | { - /** - * Whether to coerce environment variables to numbers. - * - * @default `true` - */ - numbers?: boolean; - /** - * Whether to coerce environment variables to booleans. - * - * @default `true` - */ - booleans?: boolean; - /** - * Whether to coerce environment variables to objects. - * - * @default `true` - */ - objects?: boolean; - }; + coerce?: boolean | Partial; /** * Control how ArkEnv handles environment variables that are not defined in your schema. * @@ -56,15 +35,6 @@ export type ArkEnvConfig = { * @see https://arktype.io/docs/configuration#onundeclaredkey */ onUndeclaredKey?: "ignore" | "reject" | "delete"; - /** - * The format to use for array coercion (when `coerce` is enabled). - * - * - `comma` (default): Strings are split by comma and trimmed. - * - `json`: Strings are parsed as JSON. - * - * @default "comma" - */ - arrayFormat?: "comma" | "json"; }; /** diff --git a/packages/arkenv/src/utils/coerce.ts b/packages/arkenv/src/utils/coerce.ts index e53338f15..8d9dd4cc7 100644 --- a/packages/arkenv/src/utils/coerce.ts +++ b/packages/arkenv/src/utils/coerce.ts @@ -3,10 +3,36 @@ import type { Dict } from "@repo/types"; import type { Type } from "arktype"; import { maybeBooleanFn, maybeJsonFn, maybeNumberFn } from "@/morphs"; +/** + * Configuration for the coercion engine. + */ export type CoerceConfig = { + /** + * Whether to coerce environment variables to numbers. + * + * @default `true` + */ numbers: boolean; + /** + * Whether to coerce environment variables to booleans. + * + * @default `true` + */ booleans: boolean; + /** + * Whether to coerce environment variables to objects. + * + * @default `true` + */ objects: boolean; + /** + * The format to use for array coercion (when `coerce` is enabled). + * + * - `comma` (default): Strings are split by comma and trimmed. + * - `json`: Strings are parsed as JSON. + * + * @default "comma" + */ arrayFormat: "comma" | "json"; }; From f7f11dcf6f9a4aab3d5c29a15bba48bb5bdbc987 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Wed, 14 Jan 2026 00:45:27 +0500 Subject: [PATCH 106/117] feat(coerce): Move arrayFormat to coerce config - Updated coercion configuration structure - Refactored arrayFormat handling - Adjusted tests for new config syntax --- .../arkenv/src/coercion.integration.test.ts | 8 ++++++-- packages/arkenv/src/create-env.test.ts | 20 ++++++++++++++----- packages/arkenv/src/create-env.ts | 5 ++++- packages/arkenv/src/utils/coerce.ts | 4 ++-- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/packages/arkenv/src/coercion.integration.test.ts b/packages/arkenv/src/coercion.integration.test.ts index a2f0690fd..c7f0d7b8f 100644 --- a/packages/arkenv/src/coercion.integration.test.ts +++ b/packages/arkenv/src/coercion.integration.test.ts @@ -162,7 +162,9 @@ describe("coercion integration", () => { VERSION: "1.0.0", EXTRA: "unused", } as Record, - arrayFormat: "json", + coerce: { + arrayFormat: "json", + }, onUndeclaredKey: "delete", }, ); @@ -180,7 +182,9 @@ describe("coercion integration", () => { { TAGS: "string[]" }, { env: { TAGS: '["invalid-json' }, - arrayFormat: "json", + coerce: { + arrayFormat: "json", + }, }, ); }).toThrow("must be an array"); diff --git a/packages/arkenv/src/create-env.test.ts b/packages/arkenv/src/create-env.test.ts index ea6571a60..76bd2dd33 100644 --- a/packages/arkenv/src/create-env.test.ts +++ b/packages/arkenv/src/create-env.test.ts @@ -508,7 +508,9 @@ describe("createEnv", () => { { TAGS: "string[]" }, { env: { TAGS: '["foo", "bar"]' }, - arrayFormat: "json", + coerce: { + arrayFormat: "json", + }, }, ); expect(env.TAGS).toEqual(["foo", "bar"]); @@ -520,7 +522,9 @@ describe("createEnv", () => { { TAGS: "string[]" }, { env: { TAGS: "foo,bar" }, // Comma separated, not JSON - arrayFormat: "json", + coerce: { + arrayFormat: "json", + }, }, ); }).toThrow("must be an array"); @@ -542,7 +546,9 @@ describe("createEnv", () => { { NUMBERS: "number[]" }, { env: { NUMBERS: "[1, 2, 3]" }, - arrayFormat: "json", + coerce: { + arrayFormat: "json", + }, }, ); expect(env.NUMBERS).toEqual([1, 2, 3]); @@ -566,7 +572,9 @@ describe("createEnv", () => { { TAGS: "string[]" }, { env: { TAGS: "[]" }, - arrayFormat: "json", + coerce: { + arrayFormat: "json", + }, }, ); expect(env.TAGS).toEqual([]); @@ -620,7 +628,9 @@ describe("createEnv", () => { SERVICES: '[{"NAME": "web", "PORT": "80"}, {"NAME": "api", "PORT": "3000"}]', }, - arrayFormat: "json", + coerce: { + arrayFormat: "json", + }, }, ); expect(env.SERVICES).toEqual([ diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index a76865cf3..84ba53636 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -116,6 +116,7 @@ function validateArkType( schema = (schema as any).onUndeclaredKey("delete"); } + // TODO: This can be simplified const coercionConfig = { numbers: config.coerce === false @@ -135,7 +136,9 @@ function validateArkType( : typeof config.coerce === "object" ? (config.coerce.objects ?? true) : true, - arrayFormat: config.arrayFormat ?? "comma", + arrayFormat: + (typeof config?.coerce === "object" && config.coerce.arrayFormat) ?? + "comma", }; const coercedEnv = coerce(schema, env, coercionConfig as any); diff --git a/packages/arkenv/src/utils/coerce.ts b/packages/arkenv/src/utils/coerce.ts index 8d9dd4cc7..e672cdbc8 100644 --- a/packages/arkenv/src/utils/coerce.ts +++ b/packages/arkenv/src/utils/coerce.ts @@ -6,7 +6,7 @@ import { maybeBooleanFn, maybeJsonFn, maybeNumberFn } from "@/morphs"; /** * Configuration for the coercion engine. */ -export type CoerceConfig = { +export type CoerceConfig = Readonly<{ /** * Whether to coerce environment variables to numbers. * @@ -34,7 +34,7 @@ export type CoerceConfig = { * @default "comma" */ arrayFormat: "comma" | "json"; -}; +}>; const DEFAULT_CONFIG: CoerceConfig = { numbers: true, From b38b221c52f5207f62a0ee9f8c49b1e14aca595c Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Wed, 14 Jan 2026 00:56:48 +0500 Subject: [PATCH 107/117] refactor(arkenv): improve arrayFormat coercion logic - Handle `coerce: false` explicitly - Ensure `arrayFormat` defaults correctly - Simplify coercion config logic --- packages/arkenv/src/create-env.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 84ba53636..303b7d033 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -137,8 +137,11 @@ function validateArkType( ? (config.coerce.objects ?? true) : true, arrayFormat: - (typeof config?.coerce === "object" && config.coerce.arrayFormat) ?? - "comma", + config.coerce === false + ? false + : typeof config.coerce === "object" + ? (config.coerce.arrayFormat ?? "comma") + : "comma", }; const coercedEnv = coerce(schema, env, coercionConfig as any); From 732efd30a17dc65401799b9848c0d87a6c65ce76 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Wed, 14 Jan 2026 00:58:23 +0500 Subject: [PATCH 108/117] chore: Add npm test task and improve coerce logic - Added 'npm run test' to tasks.json - Ensured nested objects are created in coerce - Handles non-object or null values during coercion --- .vscode/tasks.json | 7 +++++++ packages/arkenv/src/utils/coerce.ts | 8 +++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index f52251449..3e9893e7e 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -24,6 +24,13 @@ "problemMatcher": [], "label": "changeset", "detail": "changeset" + }, + { + "type": "npm", + "script": "test", + "problemMatcher": [], + "label": "test", + "detail": "vitest" } ] } diff --git a/packages/arkenv/src/utils/coerce.ts b/packages/arkenv/src/utils/coerce.ts index e672cdbc8..645276af1 100644 --- a/packages/arkenv/src/utils/coerce.ts +++ b/packages/arkenv/src/utils/coerce.ts @@ -150,7 +150,13 @@ function coerceInternal( let current = result; for (let i = 0; i < parts.length - 1; i++) { const part = parts[i]; - if (!(part in current)) current[part] = {}; + if ( + !(part in current) || + typeof current[part] !== "object" || + current[part] === null + ) { + current[part] = {}; + } current = current[part] as Record; } const lastPart = parts[parts.length - 1]; From 74cf8ef645f558eb2c9bea379524b68ac73fa365 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Wed, 14 Jan 2026 00:59:19 +0500 Subject: [PATCH 109/117] refactor(coerce): improve value coercion logic - Prioritize object (JSON) coercion - Coerce numbers and booleans only if value is string - Ensure correct coercion order --- packages/arkenv/src/utils/coerce.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/arkenv/src/utils/coerce.ts b/packages/arkenv/src/utils/coerce.ts index 645276af1..a2ae71746 100644 --- a/packages/arkenv/src/utils/coerce.ts +++ b/packages/arkenv/src/utils/coerce.ts @@ -225,9 +225,15 @@ function applyCoercion( function coerceSimpleValue(value: any, config: CoerceConfig): any { let coerced = value; - if (config.numbers) coerced = maybeNumberFn(coerced); - if (config.booleans) coerced = maybeBooleanFn(coerced); + // Priority: object (JSON Parsing) > number > boolean + // (arrays are not handled here since we don't have type information) if (config.objects) coerced = maybeJsonFn(coerced); + if (typeof coerced === "string" && config.numbers) { + coerced = maybeNumberFn(coerced); + } + if (typeof coerced === "string" && config.booleans) { + coerced = maybeBooleanFn(coerced); + } return coerced; } From cfacdd371abee8ae05b59b2b91c04181fb77d76d Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Wed, 14 Jan 2026 00:59:55 +0500 Subject: [PATCH 110/117] fix(scope): assert port with number type - Changed port assertion to accept numbers - Updated test cases to use number literals --- packages/internal/scope/src/keywords.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/internal/scope/src/keywords.test.ts b/packages/internal/scope/src/keywords.test.ts index a948bc446..df563ce5a 100644 --- a/packages/internal/scope/src/keywords.test.ts +++ b/packages/internal/scope/src/keywords.test.ts @@ -12,11 +12,11 @@ describe("port", () => { }); it("should throw when the port is negative", () => { - expect(() => port.assert("-2")).toThrow(); + expect(() => port.assert(-2)).toThrow(); }); it("should throw when the port is too large", () => { - expect(() => port.assert("65536")).toThrow(); + expect(() => port.assert(65536)).toThrow(); }); }); From 16d7abfce589877a6cf8bf85079b90cd70d2ec7c Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Wed, 14 Jan 2026 01:15:31 +0500 Subject: [PATCH 111/117] refactor: Rename morph functions to be more descriptive - Renamed `maybeNumberFn` to `coerceNumber` - Renamed `maybeBooleanFn` to `coerceBoolean` - Renamed `maybeJsonFn` to `coerceJson` - Updated imports and usages accordingly --- packages/arkenv/src/morphs.test.ts | 82 ++++++++++++++--------------- packages/arkenv/src/morphs.ts | 34 +++++++++--- packages/arkenv/src/utils/coerce.ts | 16 +++--- 3 files changed, 77 insertions(+), 55 deletions(-) diff --git a/packages/arkenv/src/morphs.test.ts b/packages/arkenv/src/morphs.test.ts index b20ff1814..a34de9d5a 100644 --- a/packages/arkenv/src/morphs.test.ts +++ b/packages/arkenv/src/morphs.test.ts @@ -1,96 +1,96 @@ import { describe, expect, it } from "vitest"; -import { maybeBooleanFn, maybeJsonFn, maybeNumberFn } from "./morphs"; +import { coerceBoolean, coerceJson, coerceNumber } from "./morphs"; describe("Morph Functions", () => { - describe("maybeNumberFn", () => { + describe("coerceNumber", () => { it("should return numbers as-is", () => { - expect(maybeNumberFn(42)).toBe(42); - expect(maybeNumberFn(0)).toBe(0); - expect(maybeNumberFn(-5.5)).toBe(-5.5); + expect(coerceNumber(42)).toBe(42); + expect(coerceNumber(0)).toBe(0); + expect(coerceNumber(-5.5)).toBe(-5.5); }); it("should convert numeric strings to numbers", () => { - expect(maybeNumberFn("42")).toBe(42); - expect(maybeNumberFn("0")).toBe(0); - expect(maybeNumberFn("-5.5")).toBe(-5.5); - expect(maybeNumberFn(" 123 ")).toBe(123); + expect(coerceNumber("42")).toBe(42); + expect(coerceNumber("0")).toBe(0); + expect(coerceNumber("-5.5")).toBe(-5.5); + expect(coerceNumber(" 123 ")).toBe(123); }); it("should handle NaN string", () => { - expect(maybeNumberFn("NaN")).toBeNaN(); + expect(coerceNumber("NaN")).toBeNaN(); }); it("should return non-numeric strings as-is", () => { - expect(maybeNumberFn("hello")).toBe("hello"); - expect(maybeNumberFn("not a number")).toBe("not a number"); + expect(coerceNumber("hello")).toBe("hello"); + expect(coerceNumber("not a number")).toBe("not a number"); }); it("should return empty strings as-is", () => { - expect(maybeNumberFn("")).toBe(""); - expect(maybeNumberFn(" ")).toBe(" "); + expect(coerceNumber("")).toBe(""); + expect(coerceNumber(" ")).toBe(" "); }); it("should return non-string, non-number values as-is", () => { const obj = { foo: "bar" }; - expect(maybeNumberFn(obj)).toBe(obj); - expect(maybeNumberFn(null)).toBe(null); - expect(maybeNumberFn(undefined)).toBe(undefined); + expect(coerceNumber(obj)).toBe(obj); + expect(coerceNumber(null)).toBe(null); + expect(coerceNumber(undefined)).toBe(undefined); }); }); - describe("maybeBooleanFn", () => { + describe("coerceBoolean", () => { it("should convert 'true' string to boolean true", () => { - expect(maybeBooleanFn("true")).toBe(true); + expect(coerceBoolean("true")).toBe(true); }); it("should convert 'false' string to boolean false", () => { - expect(maybeBooleanFn("false")).toBe(false); + expect(coerceBoolean("false")).toBe(false); }); it("should return other values as-is", () => { - expect(maybeBooleanFn("yes")).toBe("yes"); - expect(maybeBooleanFn("no")).toBe("no"); - expect(maybeBooleanFn("1")).toBe("1"); - expect(maybeBooleanFn("0")).toBe("0"); - expect(maybeBooleanFn(true)).toBe(true); - expect(maybeBooleanFn(false)).toBe(false); - expect(maybeBooleanFn(42)).toBe(42); + expect(coerceBoolean("yes")).toBe("yes"); + expect(coerceBoolean("no")).toBe("no"); + expect(coerceBoolean("1")).toBe("1"); + expect(coerceBoolean("0")).toBe("0"); + expect(coerceBoolean(true)).toBe(true); + expect(coerceBoolean(false)).toBe(false); + expect(coerceBoolean(42)).toBe(42); }); }); - describe("maybeJsonFn", () => { + describe("coerceJson", () => { it("should parse valid JSON objects", () => { - expect(maybeJsonFn('{"foo":"bar"}')).toEqual({ foo: "bar" }); - expect(maybeJsonFn(' {"nested":{"value":123}} ')).toEqual({ + expect(coerceJson('{"foo":"bar"}')).toEqual({ foo: "bar" }); + expect(coerceJson(' {"nested":{"value":123}} ')).toEqual({ nested: { value: 123 }, }); }); it("should parse valid JSON arrays", () => { - expect(maybeJsonFn("[1,2,3]")).toEqual([1, 2, 3]); - expect(maybeJsonFn('["a","b","c"]')).toEqual(["a", "b", "c"]); + expect(coerceJson("[1,2,3]")).toEqual([1, 2, 3]); + expect(coerceJson('["a","b","c"]')).toEqual(["a", "b", "c"]); }); it("should return non-JSON strings as-is", () => { - expect(maybeJsonFn("hello")).toBe("hello"); - expect(maybeJsonFn("not json")).toBe("not json"); + expect(coerceJson("hello")).toBe("hello"); + expect(coerceJson("not json")).toBe("not json"); }); it("should return strings that don't start with { or [ as-is", () => { - expect(maybeJsonFn("plain text")).toBe("plain text"); - expect(maybeJsonFn(" plain text ")).toBe(" plain text "); + expect(coerceJson("plain text")).toBe("plain text"); + expect(coerceJson(" plain text ")).toBe(" plain text "); }); it("should return invalid JSON as-is", () => { - expect(maybeJsonFn("{invalid}")).toBe("{invalid}"); - expect(maybeJsonFn("[broken")).toBe("[broken"); + expect(coerceJson("{invalid}")).toBe("{invalid}"); + expect(coerceJson("[broken")).toBe("[broken"); }); it("should return non-string values as-is", () => { - expect(maybeJsonFn(42)).toBe(42); - expect(maybeJsonFn(true)).toBe(true); + expect(coerceJson(42)).toBe(42); + expect(coerceJson(true)).toBe(true); const obj = { foo: "bar" }; - expect(maybeJsonFn(obj)).toBe(obj); + expect(coerceJson(obj)).toBe(obj); }); }); }); diff --git a/packages/arkenv/src/morphs.ts b/packages/arkenv/src/morphs.ts index 551432763..636c0ddec 100644 --- a/packages/arkenv/src/morphs.ts +++ b/packages/arkenv/src/morphs.ts @@ -1,7 +1,15 @@ /** - * A loose numeric morph function (internal use only). + * Attempts to coerce a value to a number. + * + * If the input is already a number, returns it unchanged. + * If the input is a string that can be parsed as a number, returns the parsed number. + * Otherwise, returns the original value unchanged. + * + * @internal + * @param s - The value to coerce + * @returns The coerced number or the original value */ -export const maybeNumberFn = (s: unknown) => { +export const coerceNumber = (s: unknown) => { if (typeof s === "number") return s; if (typeof s !== "string") return s; const trimmed = s.trim(); @@ -12,18 +20,32 @@ export const maybeNumberFn = (s: unknown) => { }; /** - * A loose boolean morph function (internal use only). + * Attempts to coerce a value to a boolean. + * + * Converts the strings "true" and "false" to their boolean equivalents. + * All other values are returned unchanged. + * + * @internal + * @param s - The value to coerce + * @returns The coerced boolean or the original value */ -export const maybeBooleanFn = (s: unknown) => { +export const coerceBoolean = (s: unknown) => { if (s === "true") return true; if (s === "false") return false; return s; }; /** - * A loose JSON morph function (internal use only). + * Attempts to parse a value as JSON. + * + * If the input is a string that starts with `{` or `[` and can be parsed as JSON, + * returns the parsed object or array. Otherwise, returns the original value unchanged. + * + * @internal + * @param s - The value to parse + * @returns The parsed JSON or the original value */ -export const maybeJsonFn = (s: unknown) => { +export const coerceJson = (s: unknown) => { if (typeof s !== "string") return s; const trimmed = s.trim(); if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return s; diff --git a/packages/arkenv/src/utils/coerce.ts b/packages/arkenv/src/utils/coerce.ts index a2ae71746..c790c3fb3 100644 --- a/packages/arkenv/src/utils/coerce.ts +++ b/packages/arkenv/src/utils/coerce.ts @@ -1,7 +1,7 @@ import { arktypeLoader } from "@repo/scope"; import type { Dict } from "@repo/types"; import type { Type } from "arktype"; -import { maybeBooleanFn, maybeJsonFn, maybeNumberFn } from "@/morphs"; +import { coerceBoolean, coerceJson, coerceNumber } from "@/morphs"; /** * Configuration for the coercion engine. @@ -184,7 +184,7 @@ function applyCoercion( // Priority: object (JSON Parsing) > array (Splitting) > number > boolean if (types.includes("object") && config.objects) { - coerced = maybeJsonFn(coerced); + coerced = coerceJson(coerced); } if (typeof coerced === "string") { @@ -194,7 +194,7 @@ function applyCoercion( coerced.startsWith("[") || coerced.startsWith("{") ) { - const parsed = maybeJsonFn(coerced); + const parsed = coerceJson(coerced); if (Array.isArray(parsed)) return parsed; } @@ -209,14 +209,14 @@ function applyCoercion( } if (types.includes("number") && config.numbers) { - coerced = maybeNumberFn(coerced); + coerced = coerceNumber(coerced); } if ( typeof coerced === "string" && types.includes("boolean") && config.booleans ) { - coerced = maybeBooleanFn(coerced); + coerced = coerceBoolean(coerced); } } @@ -227,12 +227,12 @@ function coerceSimpleValue(value: any, config: CoerceConfig): any { let coerced = value; // Priority: object (JSON Parsing) > number > boolean // (arrays are not handled here since we don't have type information) - if (config.objects) coerced = maybeJsonFn(coerced); + if (config.objects) coerced = coerceJson(coerced); if (typeof coerced === "string" && config.numbers) { - coerced = maybeNumberFn(coerced); + coerced = coerceNumber(coerced); } if (typeof coerced === "string" && config.booleans) { - coerced = maybeBooleanFn(coerced); + coerced = coerceBoolean(coerced); } return coerced; } From b2159c6a54c1efb56d0adbef46b9e3bd14c1cc20 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Wed, 14 Jan 2026 01:16:50 +0500 Subject: [PATCH 112/117] docs(arkenv): Update type validator documentation - Enhanced description of `type` function - Added arkenv-specific keywords explanation - Included usage examples for `type` - Linked to ArkType documentation --- packages/arkenv/src/type.ts | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/arkenv/src/type.ts b/packages/arkenv/src/type.ts index b372d1adc..930ef7441 100644 --- a/packages/arkenv/src/type.ts +++ b/packages/arkenv/src/type.ts @@ -1,8 +1,32 @@ import { $ } from "@repo/scope"; /** - * `type` is a typesafe environment variable validator, an alias for `arktype`'s `type`. - * It includes arkenv-specific keywords like `string.host` and `number.port`. + * Create type-safe environment variable validators using ArkType's syntax. + * + * This is a re-export of ArkType's `type` function with arkenv-specific keywords added: + * - `string.host` - Validates hostnames and IP addresses + * - `number.port` - Validates port numbers (0-65535) + * + * The proxy wrapper ensures that created types are properly marked as ArkType instances + * for internal detection and handling. + * + * @example + * ```ts + * // Create a simple type + * const Port = type("number.port"); + * + * // Create an object schema + * const Config = type({ + * HOST: "string.host", + * PORT: "number.port", + * DEBUG: "boolean" + * }); + * + * // Use with createEnv + * const env = createEnv(Config); + * ``` + * + * @see {@link https://arktype.io/docs | ArkType Documentation} */ export const type: typeof $.type = new Proxy((() => {}) as any, { get(_, prop) { From 900d939be604cab832ac6a3f33b840886194fff0 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Wed, 14 Jan 2026 01:17:41 +0500 Subject: [PATCH 113/117] refactor(arkenv): improve coercion config typing - Added explicit CoerceConfig type - Removed unnecessary 'as any' cast - Simplified arrayFormat logic --- packages/arkenv/src/create-env.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 303b7d033..3d7babb56 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -117,7 +117,7 @@ function validateArkType( } // TODO: This can be simplified - const coercionConfig = { + const coercionConfig: CoerceConfig = { numbers: config.coerce === false ? false @@ -137,14 +137,12 @@ function validateArkType( ? (config.coerce.objects ?? true) : true, arrayFormat: - config.coerce === false - ? false - : typeof config.coerce === "object" - ? (config.coerce.arrayFormat ?? "comma") - : "comma", + typeof config.coerce === "object" + ? (config.coerce.arrayFormat ?? "comma") + : "comma", }; - const coercedEnv = coerce(schema, env, coercionConfig as any); + const coercedEnv = coerce(schema, env, coercionConfig); const result = schema(coercedEnv); From 941a61fdedbc83e9023b3041e0a49afd70cdd863 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Wed, 14 Jan 2026 01:32:35 +0500 Subject: [PATCH 114/117] fix(arkenv): remove unnecessary type assertion - Removed `as any` from ArkEnvError instantiation - Ensures correct type safety for issues --- packages/arkenv/src/create-env.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 3d7babb56..e58c69029 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -245,7 +245,7 @@ export function createEnv( if (isArkCompiled) { const result = validateArkType(def, config, env); if (!result.success) { - throw new ArkEnvError(result.issues as any); + throw new ArkEnvError(result.issues); } return result.value as InferType; } @@ -265,7 +265,7 @@ export function createEnv( if (!result.success) { // Use result.issues which is mapped from ArkErrors if needed - throw new ArkEnvError(result.issues as any); + throw new ArkEnvError(result.issues); } return result.value as InferType; From ce54c20ff6319843606b8a862227ea353e63f35a Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Wed, 14 Jan 2026 01:35:27 +0500 Subject: [PATCH 115/117] feat(arkenv): introduce coerce option - Allows explicit coercion configuration - Supports nested coercion rules - Improves type handling for arrays --- apps/www/content/docs/arkenv/coercion.mdx | 4 +++- packages/arkenv/src/utils/coerce.ts | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/www/content/docs/arkenv/coercion.mdx b/apps/www/content/docs/arkenv/coercion.mdx index ff5f500f4..1859c5e7b 100644 --- a/apps/www/content/docs/arkenv/coercion.mdx +++ b/apps/www/content/docs/arkenv/coercion.mdx @@ -62,7 +62,9 @@ import arkenv from "arkenv"; const env = arkenv( { TAGS: "string[]" }, { - arrayFormat: "json", + coerce: { + arrayFormat: "json", + }, env: { TAGS: '["web", "app"]' } } ); diff --git a/packages/arkenv/src/utils/coerce.ts b/packages/arkenv/src/utils/coerce.ts index c790c3fb3..6d43eb259 100644 --- a/packages/arkenv/src/utils/coerce.ts +++ b/packages/arkenv/src/utils/coerce.ts @@ -155,6 +155,9 @@ function coerceInternal( typeof current[part] !== "object" || current[part] === null ) { + // Note: This overwrites any existing non-object value. + // If both "FOO" and "FOO.BAR" are defined, "FOO.BAR" takes precedence + // and "FOO" becomes an object container. current[part] = {}; } current = current[part] as Record; From 1fdaaf46bae923660dc82f4b68d8d6056d4ce116 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Wed, 14 Jan 2026 01:46:39 +0500 Subject: [PATCH 116/117] fix(arkenv): preserve env var path in validation issues - Preserve env var path as-is - Handle dots in env var names --- packages/arkenv/src/create-env.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index e58c69029..d4e2bacc7 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -152,7 +152,9 @@ function validateArkType( if (errors.byPath) { for (const [path, error] of Object.entries(errors.byPath)) { issues.push({ - path: path.split("."), + // Preserve the path as-is since it's an environment variable name + // which may legitimately contain dots (e.g., "DATABASE.HOST") + path: [path], message: (error as any).message, }); } From 53c8bc4fb4751efa65075705c5c7dd95079016a9 Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Wed, 14 Jan 2026 02:04:49 +0500 Subject: [PATCH 117/117] fix(arkenv): Improve error message for empty schema - Added check for empty schema object - Throws a clear error message - Guides user on providing valid schema --- packages/arkenv/src/create-env.test.ts | 6 ++++++ packages/arkenv/src/create-env.ts | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/packages/arkenv/src/create-env.test.ts b/packages/arkenv/src/create-env.test.ts index 76bd2dd33..79f5ee4a6 100644 --- a/packages/arkenv/src/create-env.test.ts +++ b/packages/arkenv/src/create-env.test.ts @@ -273,6 +273,12 @@ describe("createEnv", () => { ); }); + it("should throw a clear error when an empty schema object is provided", () => { + expect(() => createEnv({})).toThrow( + "ArkEnv expects a mapping of environment variables to validators. Please provide at least one key (e.g., createEnv({ PORT: 'number' })) or pass a compiled ArkType schema directly.", + ); + }); + it("should validate against a custom environment", () => { const env = { TEST_STRING: "hello", diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index d4e2bacc7..8b3752fba 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -208,8 +208,10 @@ function detectMappingType(mapping: SchemaShape): { } { let hasStandard = false; let hasArktype = false; + let hasKeys = false; for (const value of Object.values(mapping)) { + hasKeys = true; if (typeof value === "string" || isArktype(value)) { hasArktype = true; } else if ((value as any)?.["~standard"]) { @@ -217,6 +219,12 @@ function detectMappingType(mapping: SchemaShape): { } } + if (!hasKeys) { + throw new Error( + "ArkEnv expects a mapping of environment variables to validators. Please provide at least one key (e.g., createEnv({ PORT: 'number' })) or pass a compiled ArkType schema directly.", + ); + } + // Prioritize ArkType detection: if anything identifies as ArkType, // we use the ArkType path which handles both ArkType and Standard Schema // (via $.type wrapping).