diff --git a/.changeset/mighty-radios-walk.md b/.changeset/mighty-radios-walk.md new file mode 100644 index 000000000..00858f213 --- /dev/null +++ b/.changeset/mighty-radios-walk.md @@ -0,0 +1,18 @@ +--- +"@arkenv/bun-plugin": patch +--- + +#### Support configuration + +Add support for an optional configuration object as the second argument. This allows you to set the `validator` mode to `"standard"`, enabling support for libraries like Zod or Valibot without an ArkType dependency. + +```ts +import { z } from "zod"; +import arkenv from "@arkenv/bun-plugin"; + +arkenv({ + BUN_PUBLIC_API_URL: z.string().url() +}, { + validator: "standard" +}) +``` diff --git a/apps/www/content/docs/bun-plugin/index.mdx b/apps/www/content/docs/bun-plugin/index.mdx index aaa696bcd..bffa5f403 100644 --- a/apps/www/content/docs/bun-plugin/index.mdx +++ b/apps/www/content/docs/bun-plugin/index.mdx @@ -90,13 +90,35 @@ export default arkenv(Env); plugins = ["./plugins/arkenv.ts"] ``` +### Validator Mode + +By default, ArkEnv uses ArkType for validation. However, you can use any Standard Schema compatible library (like Zod or Valibot) by setting the `validator` option to `"standard"`. + +In this mode, you don't need to install `arktype`. + +```ts title="build.ts" +import arkenv from "@arkenv/bun-plugin"; +import { z } from "zod"; + +await Bun.build({ + entrypoints: ["./app.tsx"], + outdir: "./dist", + plugins: [ + arkenv({ + BUN_PUBLIC_API_URL: z.string().url(), + }, { + validator: "standard" + }) + ], +}); +``` + > [!IMPORTANT] -> This plugin requires `arktype` to be installed in your project. +> By default, this plugin uses [ArkType](https://arktype.io) for validation. > -> It does not support `validator: "standard"`. -> You can still use Zod or Valibot schemas alongside ArkType's DSL, since ArkType natively supports Standard Schema. +> You can also use `validator: "standard"` if you prefer using Zod or Valibot directly without an ArkType dependency. > -> See [Standard Schema integration](/docs/arkenv/integrations/standard-schema) for details. +> See [Validator mode](/docs/arkenv/validator-mode) for details. ## Features @@ -107,5 +129,11 @@ plugins = ["./plugins/arkenv.ts"] ## Installation ```package-install -@arkenv/bun-plugin arkenv arktype +@arkenv/bun-plugin arkenv +``` + +If you intend to use the default validator mode, you should also install `arktype`: + +```package-install +arktype ``` diff --git a/openspec/changes/bun-plugin-validator-mode/design.md b/openspec/changes/bun-plugin-validator-mode/design.md new file mode 100644 index 000000000..fcf46682d --- /dev/null +++ b/openspec/changes/bun-plugin-validator-mode/design.md @@ -0,0 +1,71 @@ +# Design: Bun Plugin Validator Mode Support + +## Problem +The Bun plugin needs to support the `validator: "standard"` mode to allow users to build projects without an `arktype` dependency. + +## Implementation Details + +### 1. Plugin Signature +We will update the `arkenv` function in `packages/bun-plugin/src/plugin.ts`: + +```typescript +export function arkenv( + options: CompiledEnvSchema, + arkenvConfig?: ArkEnvConfig, +): BunPlugin; +export function arkenv( + options: EnvSchema, + arkenvConfig?: ArkEnvConfig, +): BunPlugin; +export function arkenv( + options: EnvSchema | CompiledEnvSchema, + arkenvConfig?: ArkEnvConfig, +): BunPlugin { + const envMap = processEnvSchema(options, arkenvConfig); + // ... +} +``` + +### 2. Update processEnvSchema Utility +The `processEnvSchema` function in `packages/bun-plugin/src/utils.ts` will be updated to accept and pass through the config: + +```typescript +export function processEnvSchema( + options: EnvSchema | CompiledEnvSchema, + config?: ArkEnvConfig, +): Map { + const env: SchemaShape = createEnv(options as any, { + ...config, + env: process.env, + }); + // ... rest of the function +} +``` + +### 3. Hybrid Mode Consideration +The `hybrid` export (zero-config mode) will continue to work without config, defaulting to ArkType mode. Users who want Standard Schema support must use the function call syntax. + +### 4. Bun Config Support +Users will be able to configure it like this: + +```ts +import { z } from 'zod' +import arkenv from '@arkenv/bun-plugin' + +Bun.build({ + plugins: [ + arkenv({ + BUN_PUBLIC_API_URL: z.string().url() + }, { + validator: 'standard' + }) + ] +}) +``` + +By passing `validator: 'standard'`, `createEnv` will skip the dynamic loading of ArkType, thus avoiding the `MODULE_NOT_FOUND` error. + +## Differences from Vite Plugin +- The Bun plugin uses `processEnvSchema` utility instead of inline `createEnv` call +- The Bun plugin has a `hybrid` export for zero-config usage (which won't support config parameter) +- The Bun plugin uses `BUN_PUBLIC_` prefix instead of `VITE_` diff --git a/openspec/changes/bun-plugin-validator-mode/proposal.md b/openspec/changes/bun-plugin-validator-mode/proposal.md new file mode 100644 index 000000000..8e8f78db8 --- /dev/null +++ b/openspec/changes/bun-plugin-validator-mode/proposal.md @@ -0,0 +1,16 @@ +# Proposal: Support Validator Mode in Bun Plugin + +## Why +Support the "standard" validator mode in `@arkenv/bun-plugin` to allow users to validate environment variables using libraries like Zod or Valibot without requiring `arktype` as a dependency. This enables a zero-ArkType runtime for Bun projects. + +## What Changes +- **MODIFIED**: Update the `arkenv()` plugin factory in `packages/bun-plugin/src/plugin.ts` to accept an optional `ArkEnvConfig` object as a second argument. +- **MODIFIED**: Update the `processEnvSchema` utility in `packages/bun-plugin/src/utils.ts` to accept and pass through the `ArkEnvConfig` to the `createEnv` call. +- **FILES AFFECTED**: + - `packages/bun-plugin/src/plugin.ts` + - `packages/bun-plugin/src/utils.ts` + +## Impact +- **Users**: Enables users who have removed `arktype` to still use ArkEnv's Bun plugin with Standard Schema validators. +- **Type System**: Leverages downstream type inference improvements via `InferType` (from `vite-plugin-validator-mode`). +- **Dependencies**: Introduces a dependency on the logic/types developed in the `vite-plugin-validator-mode` change. diff --git a/openspec/changes/bun-plugin-validator-mode/specs/bun-plugin/spec.md b/openspec/changes/bun-plugin-validator-mode/specs/bun-plugin/spec.md new file mode 100644 index 000000000..44d3c79d7 --- /dev/null +++ b/openspec/changes/bun-plugin-validator-mode/specs/bun-plugin/spec.md @@ -0,0 +1,65 @@ +# bun-plugin Spec Delta + +## ADDED Requirements + +### Requirement: Bun Plugin Validator Mode Configuration + +The Bun plugin SHALL accept an optional `ArkEnvConfig` parameter to configure the validator engine, allowing users to choose between ArkType (default) and Standard Schema validators (e.g., Zod, Valibot). + +#### Scenario: Configure plugin with Standard Schema validator + +- **WHEN** a user configures the Bun plugin with a Zod schema +- **AND** they pass `{ validator: "standard" }` as the second argument +- **THEN** the plugin SHALL use Standard Schema validation instead of ArkType +- **AND** the plugin SHALL NOT require `arktype` as a dependency +- **AND** environment variables SHALL be validated using the Standard Schema validator +- **AND** the validated values SHALL be statically replaced in the bundle + +```typescript +import { z } from 'zod' +import arkenv from '@arkenv/bun-plugin' + +Bun.build({ + plugins: [ + arkenv({ + BUN_PUBLIC_API_URL: z.string().url(), + BUN_PUBLIC_DEBUG: z.boolean() + }, { + validator: 'standard' + }) + ] +}) +``` + +#### Scenario: Default to ArkType when no validator config provided + +- **WHEN** a user configures the Bun plugin without a second argument +- **THEN** the plugin SHALL default to `validator: "arktype"` +- **AND** the plugin SHALL use ArkType for validation +- **AND** existing behavior SHALL remain unchanged + +#### Scenario: Validator config is passed through to createEnv + +- **WHEN** a user provides an `ArkEnvConfig` object to the plugin +- **THEN** the plugin SHALL merge this config with the environment variables +- **AND** the config SHALL be passed to `createEnv` along with `env: process.env` +- **AND** all config options (validator, coerce, onUndeclaredKey, arrayFormat) SHALL be respected + +## MODIFIED Requirements + +### Requirement: Bun Plugin Environment Variable Validation and Transformation + +The Bun plugin SHALL validate environment variables at build-time using ArkEnv's schema validation **with configurable validator engine** and statically replace `process.env.VARIABLE` access with validated, transformed values during bundling. The plugin SHALL transform values according to the schema (e.g., string to boolean, apply default values). + +**Changes:** +- Added support for configurable validator engine via `ArkEnvConfig` parameter +- Plugin can now use either ArkType or Standard Schema validators + +#### Scenario: Plugin validates with Standard Schema validators + +- **WHEN** a user configures the Bun plugin with a Standard Schema (Zod/Valibot) and `validator: "standard"` +- **AND** the schema includes variables with various types +- **THEN** the plugin validates all variables using the Standard Schema validator +- **AND** the plugin transforms values according to the schema +- **AND** the plugin statically replaces `process.env.VARIABLE` with the validated value +- **AND** validation errors from the Standard Schema library are properly reported diff --git a/openspec/changes/bun-plugin-validator-mode/tasks.md b/openspec/changes/bun-plugin-validator-mode/tasks.md new file mode 100644 index 000000000..a0bfdc424 --- /dev/null +++ b/openspec/changes/bun-plugin-validator-mode/tasks.md @@ -0,0 +1,8 @@ +# Tasks: Bun Plugin Validator Mode Support + +1. - [x] Update `@arkenv/bun-plugin` signature to `arkenv(schema, config?)` +2. - [x] Update `processEnvSchema` to accept and pass `config` to `createEnv` +3. - [x] Add JSDoc examples showing `validator: "standard"` usage +4. - [x] Update plugin documentation with Standard Schema examples +5. - [x] Add test case for `validator: "standard"` in Bun plugin tests +6. - [x] Verify using a Zod schema in an example project with `arktype` uninstalled diff --git a/packages/bun-plugin/src/plugin.test.ts b/packages/bun-plugin/src/plugin.test.ts index 09091d30b..5026e7495 100644 --- a/packages/bun-plugin/src/plugin.test.ts +++ b/packages/bun-plugin/src/plugin.test.ts @@ -1,15 +1,29 @@ -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +// 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, + createEnv: vi.fn(actual.createEnv), + }; +}); + import { arkenv } from "./plugin"; +const { createEnv: mockCreateEnv } = vi.mocked(await import("arkenv")); + describe("Bun Plugin", () => { let originalEnv: NodeJS.ProcessEnv; beforeEach(() => { originalEnv = { ...process.env }; + mockCreateEnv.mockClear(); }); afterEach(() => { process.env = originalEnv; + mockCreateEnv.mockReset(); }); it("should create a plugin function", () => { @@ -44,4 +58,29 @@ describe("Bun Plugin", () => { arkenv({ BUN_PUBLIC_REQUIRED: "string" }); }).toThrow(); }); + + it("should pass arkenvConfig to createEnv", () => { + process.env.BUN_PUBLIC_TEST = "test-value"; + + const standardSchema = { + "~standard": { + version: 1 as const, + vendor: "test", + validate: (val: unknown) => ({ value: val }), + }, + }; + + arkenv( + { BUN_PUBLIC_TEST: standardSchema as any }, + { validator: "standard" }, + ); + + expect(mockCreateEnv).toHaveBeenCalledWith( + { BUN_PUBLIC_TEST: standardSchema }, + expect.objectContaining({ + validator: "standard", + env: expect.any(Object), + }), + ); + }); }); diff --git a/packages/bun-plugin/src/plugin.ts b/packages/bun-plugin/src/plugin.ts index dc3736142..e25e9a0a7 100644 --- a/packages/bun-plugin/src/plugin.ts +++ b/packages/bun-plugin/src/plugin.ts @@ -1,6 +1,6 @@ import { join } from "node:path"; import type { CompiledEnvSchema, SchemaShape } from "@repo/types"; -import type { EnvSchema } from "arkenv"; +import type { ArkEnvConfig, EnvSchema } from "arkenv"; import type { BunPlugin } from "bun"; import { processEnvSchema, registerLoader } from "./utils"; @@ -34,15 +34,42 @@ import { processEnvSchema, registerLoader } from "./utils"; * plugins: [arkenv(Env)] * }) * ``` + * + * @param options - The environment variable schema definition. + * @param arkenvConfig - Optional configuration for ArkEnv, including validator mode selection. + * Use `{ validator: "standard" }` to use Standard Schema validators (e.g., Zod, Valibot) without ArkType. + * @returns A Bun plugin that validates environment variables and exposes prefixed variables to client code. + * + * @example + * ```ts + * // Using Zod with Standard Schema validator + * import { z } from 'zod'; + * import arkenv from '@arkenv/bun-plugin'; + * + * Bun.build({ + * plugins: [ + * arkenv({ + * BUN_PUBLIC_API_URL: z.string().url(), + * }, { + * validator: 'standard' + * }), + * ], + * }); + * ``` */ -export function arkenv(options: CompiledEnvSchema): BunPlugin; +export function arkenv( + options: CompiledEnvSchema, + arkenvConfig?: ArkEnvConfig, +): BunPlugin; export function arkenv( options: EnvSchema, + arkenvConfig?: ArkEnvConfig, ): BunPlugin; export function arkenv( options: EnvSchema | CompiledEnvSchema, + arkenvConfig?: ArkEnvConfig, ): BunPlugin { - const envMap = processEnvSchema(options); + const envMap = processEnvSchema(options, arkenvConfig); return { name: "@arkenv/bun-plugin", diff --git a/packages/bun-plugin/src/utils.ts b/packages/bun-plugin/src/utils.ts index 51528049b..f616a97f5 100644 --- a/packages/bun-plugin/src/utils.ts +++ b/packages/bun-plugin/src/utils.ts @@ -1,5 +1,5 @@ import type { CompiledEnvSchema, SchemaShape } from "@repo/types"; -import { createEnv, type EnvSchema } from "arkenv"; +import { type ArkEnvConfig, createEnv, type EnvSchema } from "arkenv"; import type { Loader, PluginBuilder } from "bun"; /** @@ -8,10 +8,14 @@ import type { Loader, PluginBuilder } from "bun"; */ export function processEnvSchema( options: EnvSchema | CompiledEnvSchema, + config?: ArkEnvConfig, ): Map { // Use type assertion because options could be either EnvSchema or CompiledEnvSchema // The union type can't match the overloads directly - const env: SchemaShape = createEnv(options as any, { env: process.env }); + const env: SchemaShape = createEnv(options as any, { + ...config, + env: config?.env ?? process.env, + }); const prefix = "BUN_PUBLIC_"; const filteredEnv = Object.fromEntries( Object.entries(env).filter(([key]) => key.startsWith(prefix)), diff --git a/packages/vite-plugin/src/index.ts b/packages/vite-plugin/src/index.ts index 90de0c530..ee42a0e6b 100644 --- a/packages/vite-plugin/src/index.ts +++ b/packages/vite-plugin/src/index.ts @@ -90,7 +90,7 @@ export default function arkenv( // The union type can't match the overloads directly const env: SchemaShape = createEnv(options as any, { ...arkenvConfig, - env: loadEnv(mode, envDir, ""), + env: arkenvConfig?.env ?? loadEnv(mode, envDir, ""), }); // Filter to only include environment variables matching the prefix