diff --git a/apps/playgrounds/node/index.ts b/apps/playgrounds/node/index.ts index 8a3adf28f..ebc06e543 100644 --- a/apps/playgrounds/node/index.ts +++ b/apps/playgrounds/node/index.ts @@ -1,6 +1,6 @@ import arkenv, { type } from "arkenv"; -const env = arkenv({ +const envSchema = type({ HOST: "string.host", PORT: "number.port", NODE_ENV: "'development' | 'production' | 'test' = 'development'", @@ -8,6 +8,8 @@ const env = arkenv({ DEBUG: "boolean = true", }); +const env = arkenv(envSchema, process.env); + // Automatically validate and parse process.env // TypeScript knows the ✨exact✨ types! const host = env.HOST; diff --git a/apps/www/app/layout.tsx b/apps/www/app/layout.tsx index fb234ced4..095755df3 100644 --- a/apps/www/app/layout.tsx +++ b/apps/www/app/layout.tsx @@ -1,4 +1,4 @@ -import { Banner } from "fumadocs-ui/components/banner"; +// import { Banner } from "fumadocs-ui/components/banner"; import "./globals.css"; import { Analytics } from "@vercel/analytics/next"; import { SpeedInsights } from "@vercel/speed-insights/next"; @@ -48,7 +48,7 @@ export default function Layout({ children }: { children: ReactNode }) { enableSystem: true, }} > - + {/* 🎉 We are now featured on  ! - + */} {children} diff --git a/apps/www/content/docs/how-to/reuse-schemas.mdx b/apps/www/content/docs/how-to/reuse-schemas.mdx new file mode 100644 index 000000000..a3f526906 --- /dev/null +++ b/apps/www/content/docs/how-to/reuse-schemas.mdx @@ -0,0 +1,163 @@ +--- +title: How to reuse your schema +description: Define your schema once and reuse it across your application. +icon: New +--- + +ArkEnv supports both raw schema objects and type definitions created with ArkType's `type()` function. Use type definitions when you need to validate the same environment variables in multiple places. + +## Basic usage + +Recall that with ArkEnv, defining your schema is done at the same time as you parse the environment variables: + +```ts twoslash +import arkenv from 'arkenv'; + +const env = arkenv({ + HOST: "string.host", + PORT: "number.port", + DEBUG: "boolean", +}); +``` + +But what if you wanted to share this schema across your app and validate+parse it against different environments? + +This might sound like a niche use-case, but it comes up more often than you'd think - especially with tools like [Vite](https://github.com/yamcodes/arkenv/tree/main/apps/playgrounds/vite), [Next.js](https://nextjs.org/), or [Bun](https://github.com/yamcodes/arkenv/tree/main/examples/with-bun), where multiple runtimes might need access to the same environment variables. Even in something as simple as a Vite+React setup, you might want your `.env` schema available both [inside your `vite.config.ts` (Node.js)](https://vite.dev/config/#using-environment-variables-in-config) and [in client code (Vite dev server)](https://vite.dev/guide/env-and-mode). See the [Node.js example](https://github.com/yamcodes/arkenv/tree/main/examples/basic) for a basic setup, or the [Vite playground](https://github.com/yamcodes/arkenv/tree/main/apps/playgrounds/vite) for a complete multi-runtime configuration. + +That's where type definitions start making sense: define your schema once with `type()`, and reuse it wherever you need. + +```ts twoslash +import arkenv, { type } from 'arkenv'; + +const envSchema = type({ + HOST: "string.host", + PORT: "number.port", + DEBUG: "boolean", +}); + +// Use it in multiple places with full type inference +const env1 = arkenv(envSchema, process.env); +const env2 = arkenv(envSchema, { HOST: "localhost", PORT: "3000", DEBUG: "true" }); + +// TypeScript knows the exact types +const host: string = env1.HOST; +const port: number = env1.PORT; +``` + +## Sharing across modules + +Export a type definition from one module and import it in others: + + + + ```ts twoslash + import { type } from 'arkenv'; + + export const envSchema = type({ + DATABASE_HOST: "string.host", + DATABASE_PORT: "number.port", + API_KEY: "string", + }); + ``` + + + ```ts twoslash + // @filename: env-schema.ts + import { type } from 'arkenv'; + + export const envSchema = type({ + DATABASE_HOST: "string.host", + DATABASE_PORT: "number.port", + API_KEY: "string", + }); + // @filename: database.ts + // ---cut--- + import arkenv from 'arkenv'; + import { envSchema } from './env-schema'; + + export const dbEnv = arkenv(envSchema, process.env); + ``` + + + ```ts twoslash + // @filename: env-schema.ts + import { type } from 'arkenv'; + + export const envSchema = type({ + DATABASE_HOST: "string.host", + DATABASE_PORT: "number.port", + API_KEY: "string", + }); + // @filename: api.ts + // ---cut--- + import arkenv from 'arkenv'; + import { envSchema } from './env-schema'; + + export const apiEnv = arkenv(envSchema, process.env); + ``` + + + +## Type inference + +Type definitions provide the same type safety as raw schema objects: + +```ts twoslash +import arkenv, { type } from 'arkenv'; + +const envSchema = type({ + PORT: "number.port", + HOST: "string.host", + TIMEOUT: "number >= 0", +}); + +const env = arkenv(envSchema, process.env); + +const port: number = env.PORT; +const host: string = env.HOST; +const timeout: number = env.TIMEOUT; +``` + +## Different environment sources + +Use the same schema with different environment sources: + +```ts twoslash +import arkenv, { type } from 'arkenv'; + +const envSchema = type({ + API_URL: "string", + API_KEY: "string", +}); + +const productionEnv = arkenv(envSchema, process.env); +const testEnv = arkenv(envSchema, { + API_URL: "http://localhost:3000", + API_KEY: "test-key", +}); +``` + +## Mixing approaches + +You can mix raw schema objects and type definitions in the same codebase: + +```ts twoslash +import arkenv, { type } from 'arkenv'; + +// Raw schema object (simple, one-off) +const simpleEnv = arkenv({ + NODE_ENV: "'development' | 'production'", +}); + +// Type definition (reusable) +const complexSchema = type({ + DATABASE_HOST: "string.host", + DATABASE_PORT: "number.port", + DEBUG: "boolean", +}); + +const complexEnv = arkenv(complexSchema, process.env); +``` + +Both approaches provide the same validation and type safety. Use raw objects for simple, one-off schemas, and type definitions when you need to reuse the schema. + diff --git a/apps/www/content/docs/meta.json b/apps/www/content/docs/meta.json index e23927835..6a5416027 100644 --- a/apps/www/content/docs/meta.json +++ b/apps/www/content/docs/meta.json @@ -10,6 +10,7 @@ "[VS Code & Cursor](/docs/integrations/vscode)", "[JetBrains IDEs](/docs/integrations/jetbrains)", "---How-to---", - "[Load environment variables](/docs/how-to/load-environment-variables)" + "[Load environment variables](/docs/how-to/load-environment-variables)", + "[New][Reuse your schema](/docs/how-to/reuse-schemas)" ] } diff --git a/apps/www/content/docs/morphs.mdx b/apps/www/content/docs/morphs.mdx index d067ed425..592c5e721 100644 --- a/apps/www/content/docs/morphs.mdx +++ b/apps/www/content/docs/morphs.mdx @@ -1,6 +1,5 @@ --- title: Morphs -icon: New --- Since environment variables are defined as strings, ArkEnv automatically transforms certain ArkType keywords when imported through ArkEnv instead of directly from ArkType. These "morphs" provide environment-variable-specific behavior that's optimized for configuration management. diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index d2d0487a9..bb7b962e7 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -6,6 +6,20 @@ type RuntimeEnvironment = Record; export type EnvSchema = type.validate; +/** + * 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 + */ +type InferType = T extends ( + value: Record, +) => infer R + ? R extends type.errors + ? never + : R + : T extends type.Any + ? U + : never; + /** * TODO: 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`. @@ -13,16 +27,33 @@ export type EnvSchema = type.validate; /** * Create an environment variables object from a schema and an environment - * @param def - The environment variable schema + * @param def - The environment variable schema (raw object or type definition created with `type()`) * @param env - The environment variables to validate, defaults to `process.env` * @returns The validated environment variable schema * @throws An {@link ArkEnvError | error} if the environment variables are invalid. */ export function createEnv>( def: EnvSchema, + env?: RuntimeEnvironment, +): distill.Out>; +export function createEnv( + def: T, + env?: RuntimeEnvironment, +): InferType; +export function createEnv>( + def: EnvSchema | type.Any, + env?: RuntimeEnvironment, +): distill.Out> | InferType; +export function createEnv>( + def: EnvSchema | type.Any, env: RuntimeEnvironment = process.env, -): distill.Out> { - const schema = $.type.raw(def); +): distill.Out> | InferType { + // If def is a type definition (has assert method), use it directly + // Otherwise, use raw() to convert the schema definition + const schema = + typeof def === "function" && "assert" in def + ? def + : $.type.raw(def as EnvSchema); const validatedEnv = schema(env); diff --git a/packages/vite-plugin/src/index.ts b/packages/vite-plugin/src/index.ts index e109dc645..ce7015c24 100644 --- a/packages/vite-plugin/src/index.ts +++ b/packages/vite-plugin/src/index.ts @@ -1,5 +1,6 @@ import type { EnvSchema } from "arkenv"; import { createEnv } from "arkenv"; +import type { type } from "arktype"; import { loadEnv, type Plugin } from "vite"; /** @@ -9,10 +10,16 @@ import { loadEnv, type Plugin } from "vite"; export default function arkenv>( options: EnvSchema, +): Plugin; +export default function arkenv(options: type.Any): Plugin; +export default function arkenv>( + options: EnvSchema | type.Any, ): Plugin { return { name: "@arkenv/vite-plugin", config(_config, { mode }) { + // createEnv accepts both EnvSchema and type.Any at runtime + // We use overloads above to provide external type precision const env = createEnv(options, loadEnv(mode, process.cwd(), "")); // Expose transformed environment variables through Vite's define option diff --git a/tooling/playwright-www/tests/docs-navigation.test.ts b/tooling/playwright-www/tests/docs-navigation.test.ts index f1ab66979..0140b489b 100644 --- a/tooling/playwright-www/tests/docs-navigation.test.ts +++ b/tooling/playwright-www/tests/docs-navigation.test.ts @@ -146,10 +146,9 @@ test.describe("Documentation Navigation", () => { if (githubLinkCount > 0) { const firstGithubLink = githubLinks.first(); await expect(firstGithubLink).toHaveAttribute("target", "_blank"); - await expect(firstGithubLink).toHaveAttribute( - "rel", - "noreferrer noopener", - ); + const rel = await firstGithubLink.getAttribute("rel"); + expect(rel).toContain("noopener"); + expect(rel).toContain("noreferrer"); } // Check for ArkType links @@ -159,10 +158,9 @@ test.describe("Documentation Navigation", () => { if (arkTypeLinkCount > 0) { const firstArkTypeLink = arkTypeLinks.first(); await expect(firstArkTypeLink).toHaveAttribute("target", "_blank"); - await expect(firstArkTypeLink).toHaveAttribute( - "rel", - "noopener noreferrer", - ); + const rel = await firstArkTypeLink.getAttribute("rel"); + expect(rel).toContain("noopener"); + expect(rel).toContain("noreferrer"); } });