From c679758031fed5c1c018e5094a60135831ea4230 Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Tue, 25 Nov 2025 21:24:16 +0500
Subject: [PATCH 01/48] Update layout.tsx to enhance user experience
- Integrated the Banner component to provide announcements about Vite support.
- Updated the Banner content with a link to the new documentation for better user guidance.
- Removed obsolete commented-out code, improving the clarity and maintainability of the layout file.
---
openspec/changes/add-bun-plugin/design.md | 138 ++++++++++++++++++
openspec/changes/add-bun-plugin/proposal.md | 47 ++++++
.../add-bun-plugin/specs/bun-plugin/spec.md | 68 +++++++++
openspec/changes/add-bun-plugin/tasks.md | 60 ++++++++
4 files changed, 313 insertions(+)
create mode 100644 openspec/changes/add-bun-plugin/design.md
create mode 100644 openspec/changes/add-bun-plugin/proposal.md
create mode 100644 openspec/changes/add-bun-plugin/specs/bun-plugin/spec.md
create mode 100644 openspec/changes/add-bun-plugin/tasks.md
diff --git a/openspec/changes/add-bun-plugin/design.md b/openspec/changes/add-bun-plugin/design.md
new file mode 100644
index 000000000..90b65f633
--- /dev/null
+++ b/openspec/changes/add-bun-plugin/design.md
@@ -0,0 +1,138 @@
+# Design: Bun Plugin for ArkEnv
+
+## Context
+
+Bun provides a universal plugin API that can be used to extend both the runtime and bundler. For ArkEnv, we need to intercept environment variable access during Bun's bundling phase to:
+
+1. Validate environment variables using ArkEnv's schema
+2. Transform values (string to boolean, apply defaults, etc.)
+3. Filter to only expose prefixed variables (defaults to `BUN_PUBLIC_*`) to client code
+4. Statically replace `process.env.VARIABLE` with validated values
+5. Provide TypeScript type augmentation
+
+This is similar to how the Vite plugin works, but uses Bun's plugin API instead of Vite's `config` hook.
+
+## Goals
+
+- Provide build-time environment variable validation for Bun applications
+- Support full-stack React apps using Bun's `serve` function
+- Filter environment variables based on Bun's prefix (defaults to `BUN_PUBLIC_*`)
+- Provide type-safe access to environment variables via type augmentation
+- Match the developer experience of the Vite plugin
+
+## Non-Goals
+
+- Runtime environment variable validation (handled by core `arkenv` package)
+- Support for Bun's native plugin API (onBeforeParse) - JavaScript plugin is sufficient
+- Custom prefix configuration in the plugin (uses Bun's default `BUN_PUBLIC_*` or bunfig.toml configuration)
+
+## Decisions
+
+### Decision: Use `onLoad` Hook for Environment Variable Transformation
+
+**What**: Use Bun's `onLoad` plugin hook to intercept module loading and transform `process.env` access.
+
+**Why**:
+- `onLoad` allows us to transform module contents before parsing
+- We can replace `process.env.VARIABLE` with validated, transformed values
+- This matches the static replacement behavior needed for Bun's bundler
+- Similar pattern to how other Bun plugins transform code
+
+**Alternatives considered**:
+- `onResolve`: Only changes module paths, doesn't allow content transformation
+- `onStart`: Runs once at bundle start, too early for per-module transformation
+- `onBeforeParse`: Requires native plugin (NAPI), adds unnecessary complexity
+
+### Decision: Filter Based on Bun's Prefix Configuration
+
+**What**: Filter environment variables to only expose those matching Bun's configured prefix (defaults to `BUN_PUBLIC_*`).
+
+**Why**:
+- Matches Bun's default behavior for client-exposed environment variables
+- Prevents server-only variables from being exposed to client code
+- Consistent with Vite plugin's filtering behavior
+- Users can configure prefix via `bunfig.toml` (e.g., `[serve.static] env = "BUN_PUBLIC_*"`)
+
+**Alternatives considered**:
+- No filtering: Would expose all variables, including server-only ones (security risk)
+- Custom prefix in plugin: Adds complexity, Bun's configuration is authoritative
+
+### Decision: Type Augmentation Similar to Vite Plugin
+
+**What**: Provide `ProcessEnvAugmented` type similar to Vite's `ImportMetaEnvAugmented` for type-safe `process.env` access.
+
+**Why**:
+- Provides consistent developer experience with Vite plugin
+- Enables TypeScript type checking for environment variables
+- Users can augment `process.env` types in their TypeScript declaration files
+
+**Implementation**:
+- Create `ProcessEnvAugmented` type
+- Filter schema to only include variables matching prefix
+- Export type from plugin package for user augmentation
+
+### Decision: Package Structure
+
+**What**: Create new package `packages/bun-plugin/` following the same structure as `packages/vite-plugin/`.
+
+**Why**:
+- Consistent with existing monorepo structure
+- Allows independent versioning and publishing
+- Keeps plugin-specific code separate from core library
+
+**Structure**:
+```
+packages/bun-plugin/
+├── src/
+│ ├── index.ts # Main plugin export
+│ └── types.ts # Type augmentation utilities
+├── package.json
+├── tsconfig.json
+└── README.md
+```
+
+## Risks / Trade-offs
+
+### Risk: Bun Plugin API Changes
+
+**Mitigation**:
+- Follow Bun's official plugin API documentation
+- Test against multiple Bun versions if possible
+- Document minimum Bun version requirement
+
+### Risk: Static Replacement Complexity
+
+**Mitigation**:
+- Use regex or AST transformation to replace `process.env.VARIABLE` patterns
+- Test thoroughly with various access patterns (destructuring, optional chaining, etc.)
+- Provide clear error messages if transformation fails
+
+### Risk: Type Augmentation Complexity
+
+**Mitigation**:
+- Reuse patterns from Vite plugin's type augmentation
+- Leverage existing `@repo/types` package for type inference
+- Provide clear documentation and examples
+
+## Migration Plan
+
+### For New Users
+- Install `@arkenv/bun-plugin` package
+- Configure plugin in Bun build/serve configuration
+- Add type augmentation to TypeScript declaration file
+- Define environment variable schema
+
+### For Existing Bun Users
+- No breaking changes to core `arkenv` package
+- Plugin is opt-in, existing code continues to work
+- Users can migrate gradually by adding plugin to their build configuration
+
+## Open Questions
+
+- Should the plugin support custom prefix configuration, or always use Bun's configured prefix?
+ - **Decision**: Use Bun's configuration (bunfig.toml), no custom prefix option
+- How should the plugin handle environment variables accessed via destructuring (e.g., `const { VAR } = process.env`)?
+ - **Decision**: Transform during `onLoad` before parsing, handle common patterns
+- Should the plugin validate all variables in the schema, or only those matching the prefix?
+ - **Decision**: Validate all variables in schema, but only expose prefixed ones to client code (similar to Vite plugin)
+
diff --git a/openspec/changes/add-bun-plugin/proposal.md b/openspec/changes/add-bun-plugin/proposal.md
new file mode 100644
index 000000000..e52f314d7
--- /dev/null
+++ b/openspec/changes/add-bun-plugin/proposal.md
@@ -0,0 +1,47 @@
+# Change: Add Bun Plugin for ArkEnv
+
+## Why
+
+ArkEnv currently provides a Vite plugin for build-time environment variable validation and type-safe access in client code. However, Bun users (especially those building full-stack React applications with Bun's `serve` function) need similar functionality.
+
+Bun's bundler statically replaces `process.env` variables during build, which means:
+- Environment variables must be validated and transformed at build-time
+- Only variables matching Bun's prefix (defaults to `BUN_PUBLIC_*`) should be exposed to client code
+- Type augmentation is needed for type-safe access to `process.env` in client code
+- The plugin must work within Bun's serve function for full-stack React apps
+
+Without a Bun plugin, users must manually validate environment variables or risk runtime errors, and they lose the type safety and build-time validation benefits that ArkEnv provides.
+
+## What Changes
+
+- **ADDED**: New `@arkenv/bun-plugin` package that provides Bun plugin integration
+- **ADDED**: Build-time environment variable validation using Bun's plugin API
+- **ADDED**: Automatic filtering of environment variables based on Bun's prefix (defaults to `BUN_PUBLIC_*`)
+- **ADDED**: Static replacement of `process.env` variables with validated, transformed values during bundling
+- **ADDED**: Type augmentation for `process.env` similar to Vite plugin's `ImportMetaEnvAugmented`
+- **ADDED**: Support for Bun's serve function in full-stack React applications
+- **ADDED**: Documentation and examples for using the Bun plugin
+
+The plugin will work very similarly to the Vite plugin:
+- Uses Bun's `onLoad` hook to intercept and transform environment variable access
+- Validates environment variables using ArkEnv's schema validation
+- Filters variables to only expose those matching the configured prefix
+- Replaces `process.env.VARIABLE` with validated, transformed values (e.g., string to boolean, default values)
+- Provides TypeScript type augmentation for type-safe access
+
+## Impact
+
+- **Affected specs**: New capability `bun-plugin`
+- **Affected code**:
+ - New package: `packages/bun-plugin/` (or similar structure)
+ - Documentation files (README, docs)
+ - Example Bun React playground updates
+ - Type augmentation utilities (similar to `packages/vite-plugin/src/types.ts`)
+- **User-facing**: Bun users can now validate environment variables at build-time with full type safety, similar to Vite users
+
+## References
+
+- [Bun Plugin API Documentation](https://bun.com/docs/bundler/plugins) - Official Bun plugin API reference
+- Existing Vite plugin implementation (`packages/vite-plugin/`) - Reference for similar functionality
+- Bun React playground (`apps/playgrounds/bun-react/`) - Target environment for testing
+
diff --git a/openspec/changes/add-bun-plugin/specs/bun-plugin/spec.md b/openspec/changes/add-bun-plugin/specs/bun-plugin/spec.md
new file mode 100644
index 000000000..90be5bb07
--- /dev/null
+++ b/openspec/changes/add-bun-plugin/specs/bun-plugin/spec.md
@@ -0,0 +1,68 @@
+# bun-plugin Specification
+
+## ADDED Requirements
+
+### Requirement: Bun Plugin Environment Variable Validation and Transformation
+
+The Bun plugin SHALL validate environment variables at build-time using ArkEnv's schema validation 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).
+
+#### Scenario: Plugin validates and transforms environment variables
+- **WHEN** a user configures the Bun plugin with a schema containing environment variables
+- **AND** the schema includes variables with various types (e.g., `BUN_PUBLIC_API_URL: "string"`, `BUN_PUBLIC_DEBUG: "boolean"`)
+- **THEN** the plugin validates all variables in the schema at build-time
+- **AND** the plugin transforms values according to the schema (e.g., `"true"` string to `true` boolean)
+- **AND** the plugin statically replaces `process.env.VARIABLE` with the validated, transformed value
+- **AND** default values are applied when variables are missing
+
+#### Scenario: Plugin handles various process.env access patterns
+- **WHEN** a user accesses environment variables via different patterns (e.g., `process.env.VAR`, `process.env["VAR"]`, destructuring)
+- **AND** the plugin is configured with a schema
+- **THEN** the plugin transforms all access patterns to use validated values
+- **AND** the transformed code uses the validated, transformed values instead of raw `process.env` access
+
+### Requirement: Bun Plugin Environment Variable Filtering
+
+The Bun plugin SHALL automatically filter environment variables to only expose those matching Bun's configured prefix (defaults to `BUN_PUBLIC_*`) to client code. Server-only environment variables without the prefix SHALL NOT be exposed to the client bundle.
+
+#### Scenario: Only prefixed variables are exposed to client
+- **WHEN** a user passes a schema containing both prefixed (`BUN_PUBLIC_*`) and unprefixed variables to the Bun plugin
+- **AND** the schema includes server-only variables like `PORT` and client-safe variables like `BUN_PUBLIC_API_URL`
+- **THEN** only variables starting with the Bun prefix are validated and exposed to client code via `process.env.*`
+- **AND** server-only variables are not included in the client bundle
+- **AND** the plugin respects Bun's prefix configuration (from bunfig.toml or defaults to `BUN_PUBLIC_*`)
+
+#### Scenario: Default prefix behavior
+- **WHEN** a user does not configure a prefix in bunfig.toml
+- **AND** they pass a schema to the Bun plugin
+- **THEN** the plugin defaults to `"BUN_PUBLIC_"` as the prefix
+- **AND** only variables starting with `BUN_PUBLIC_` are exposed to client code
+
+### Requirement: Bun Plugin Type Augmentation
+
+The Bun plugin SHALL provide TypeScript type augmentation for `process.env` similar to the Vite plugin's `ImportMetaEnvAugmented`, enabling type-safe access to environment variables in client code.
+
+#### Scenario: Type augmentation enables type-safe access
+- **WHEN** a user configures the Bun plugin with a schema
+- **AND** they augment `process.env` types using `ProcessEnvAugmented`
+- **THEN** TypeScript provides type checking and autocomplete for environment variables
+- **AND** only variables matching the configured prefix are included in the type
+- **AND** types reflect the validated, transformed values (e.g., `boolean` instead of `string`)
+
+#### Scenario: Type augmentation filters by prefix
+- **WHEN** a user defines a schema with both prefixed and unprefixed variables
+- **AND** they use `ProcessEnvAugmented` for type augmentation
+- **THEN** the type only includes variables matching the configured prefix
+- **AND** server-only variables are excluded from the type
+
+### Requirement: Bun Plugin Integration with Bun's Serve Function
+
+The Bun plugin SHALL work correctly with Bun's `serve` function for full-stack React applications, validating and transforming environment variables during the bundling phase before the server starts.
+
+#### Scenario: Plugin works with Bun serve in full-stack React app
+- **WHEN** a user uses Bun's `serve` function with a full-stack React application
+- **AND** they configure the Bun plugin with a schema
+- **THEN** the plugin validates environment variables during bundling
+- **AND** the plugin transforms `process.env` access in both server and client code
+- **AND** only prefixed variables are exposed to client code
+- **AND** the server starts successfully with validated environment variables
+
diff --git a/openspec/changes/add-bun-plugin/tasks.md b/openspec/changes/add-bun-plugin/tasks.md
new file mode 100644
index 000000000..2d8e26cb0
--- /dev/null
+++ b/openspec/changes/add-bun-plugin/tasks.md
@@ -0,0 +1,60 @@
+## 1. Package Setup
+
+- [ ] 1.1 Create `packages/bun-plugin/` directory structure
+- [ ] 1.2 Initialize package.json with proper metadata and peer dependencies (bun, arkenv, arktype)
+- [ ] 1.3 Set up TypeScript configuration (tsconfig.json)
+- [ ] 1.4 Set up build configuration (tsdown.config.ts or similar)
+- [ ] 1.5 Add package to pnpm workspace
+- [ ] 1.6 Configure changeset for the new package
+
+## 2. Core Plugin Implementation
+
+- [ ] 2.1 Implement main plugin function that accepts schema (similar to Vite plugin signature)
+- [ ] 2.2 Implement `onLoad` hook to intercept module loading
+- [ ] 2.3 Add logic to detect and transform `process.env.VARIABLE` patterns
+- [ ] 2.4 Integrate ArkEnv's `createEnv` for validation and transformation
+- [ ] 2.5 Implement filtering logic to only expose variables matching Bun's prefix (defaults to `BUN_PUBLIC_*`)
+- [ ] 2.6 Handle static replacement of `process.env` variables with validated values
+- [ ] 2.7 Add support for Bun's prefix configuration (read from bunfig.toml or use default)
+
+## 3. Type Augmentation
+
+- [ ] 3.1 Create `ProcessEnvAugmented` type utility
+- [ ] 3.2 Implement filtering logic to only include prefixed variables in type
+- [ ] 3.3 Export type from plugin package
+- [ ] 3.4 Add JSDoc documentation with usage examples
+
+## 4. Testing
+
+- [ ] 4.1 Add unit tests for plugin function
+- [ ] 4.2 Add integration tests using Bun React playground as fixture
+- [ ] 4.3 Test environment variable validation and error handling
+- [ ] 4.4 Test filtering behavior (only prefixed variables exposed)
+- [ ] 4.5 Test type augmentation (TypeScript compilation tests)
+- [ ] 4.6 Test various `process.env` access patterns (direct access, destructuring, optional chaining)
+- [ ] 4.7 Test with Bun's serve function in full-stack React app
+
+## 5. Documentation
+
+- [ ] 5.1 Create README.md for the package with installation and usage instructions
+- [ ] 5.2 Add documentation page to www app (docs/bun-plugin/)
+- [ ] 5.3 Document type augmentation pattern (similar to Vite plugin docs)
+- [ ] 5.4 Add examples showing common usage patterns
+- [ ] 5.5 Document Bun prefix configuration and filtering behavior
+- [ ] 5.6 Add troubleshooting section
+
+## 6. Examples and Playgrounds
+
+- [ ] 6.1 Update Bun React playground to use the plugin
+- [ ] 6.2 Add example showing environment variable validation
+- [ ] 6.3 Add example showing type augmentation setup
+- [ ] 6.4 Verify playground builds and runs correctly with plugin
+
+## 7. Validation
+
+- [ ] 7.1 Run `openspec validate add-bun-plugin --strict`
+- [ ] 7.2 Verify all tests pass
+- [ ] 7.3 Verify TypeScript compilation succeeds
+- [ ] 7.4 Verify playground examples work correctly
+- [ ] 7.5 Check bundle size meets project constraints (<2kB goal)
+
From 89d227d3ec4b738ca4d72ea74a8c44f85ff73774 Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Tue, 25 Nov 2025 21:52:04 +0500
Subject: [PATCH 02/48] Update pnpm-lock.yaml to add Bun plugin dependencies
- Added `@types/bun` and `typescript` as devDependencies for the Bun plugin.
- Updated package resolutions for `@types/bun` and `bun-types` to version 1.3.3, ensuring compatibility and stability in the project.
---
packages/bun-plugin/.gitignore | 34 ++++++++++++++++++++++++++++++
packages/bun-plugin/README.md | 15 +++++++++++++
packages/bun-plugin/package.json | 13 ++++++++++++
packages/bun-plugin/src/index.ts | 1 +
packages/bun-plugin/tsconfig.json | 35 +++++++++++++++++++++++++++++++
pnpm-lock.yaml | 23 ++++++++++++++++++++
6 files changed, 121 insertions(+)
create mode 100644 packages/bun-plugin/.gitignore
create mode 100644 packages/bun-plugin/README.md
create mode 100644 packages/bun-plugin/package.json
create mode 100644 packages/bun-plugin/src/index.ts
create mode 100644 packages/bun-plugin/tsconfig.json
diff --git a/packages/bun-plugin/.gitignore b/packages/bun-plugin/.gitignore
new file mode 100644
index 000000000..a14702c40
--- /dev/null
+++ b/packages/bun-plugin/.gitignore
@@ -0,0 +1,34 @@
+# dependencies (bun install)
+node_modules
+
+# output
+out
+dist
+*.tgz
+
+# code coverage
+coverage
+*.lcov
+
+# logs
+logs
+_.log
+report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# caches
+.eslintcache
+.cache
+*.tsbuildinfo
+
+# IntelliJ based IDEs
+.idea
+
+# Finder (MacOS) folder config
+.DS_Store
diff --git a/packages/bun-plugin/README.md b/packages/bun-plugin/README.md
new file mode 100644
index 000000000..93f828b88
--- /dev/null
+++ b/packages/bun-plugin/README.md
@@ -0,0 +1,15 @@
+# .
+
+To install dependencies:
+
+```bash
+bun install
+```
+
+To run:
+
+```bash
+bun run src/index.ts
+```
+
+This project was created using `bun init` in bun v1.3.2. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
diff --git a/packages/bun-plugin/package.json b/packages/bun-plugin/package.json
new file mode 100644
index 000000000..d1b040c76
--- /dev/null
+++ b/packages/bun-plugin/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "@arkenv/bun-plugin",
+ "version": "0.0.1",
+ "module": "src/index.ts",
+ "type": "module",
+ "scripts": {
+ "build": "bun build src/index.ts --outdir=dist"
+ },
+ "devDependencies": {
+ "@types/bun": "^1.3.3",
+ "typescript": "^5.9.3"
+ }
+}
diff --git a/packages/bun-plugin/src/index.ts b/packages/bun-plugin/src/index.ts
new file mode 100644
index 000000000..f67b2c645
--- /dev/null
+++ b/packages/bun-plugin/src/index.ts
@@ -0,0 +1 @@
+console.log("Hello via Bun!");
\ No newline at end of file
diff --git a/packages/bun-plugin/tsconfig.json b/packages/bun-plugin/tsconfig.json
new file mode 100644
index 000000000..723aad54a
--- /dev/null
+++ b/packages/bun-plugin/tsconfig.json
@@ -0,0 +1,35 @@
+{
+ "compilerOptions": {
+ // Environment setup & latest features
+ "lib": [
+ "ESNext"
+ ],
+ "target": "ESNext",
+ "module": "Preserve",
+ "moduleDetection": "force",
+ "jsx": "react-jsx",
+ "allowJs": true,
+ // Bundler mode
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+ // Best practices
+ "strict": true,
+ "skipLibCheck": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedIndexedAccess": true,
+ "noImplicitOverride": true,
+ // Some stricter flags (disabled by default)
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noPropertyAccessFromIndexSignature": false,
+ // Paths
+ "baseUrl": ".",
+ "paths": {
+ "@/*": [
+ "./src/*"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e6983262b..e02defa66 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -355,6 +355,15 @@ importers:
specifier: 4.0.10
version: 4.0.10(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.10)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)
+ packages/bun-plugin:
+ devDependencies:
+ '@types/bun':
+ specifier: ^1.3.3
+ version: 1.3.3
+ typescript:
+ specifier: ^5.9.3
+ version: 5.9.3
+
packages/internal/types:
devDependencies:
arktype:
@@ -3296,6 +3305,9 @@ packages:
'@types/bun@1.3.2':
resolution: {integrity: sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg==}
+ '@types/bun@1.3.3':
+ resolution: {integrity: sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g==}
+
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
@@ -3841,6 +3853,9 @@ packages:
peerDependencies:
'@types/react': ^19
+ bun-types@1.3.3:
+ resolution: {integrity: sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ==}
+
bundle-name@4.1.0:
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
engines: {node: '>=18'}
@@ -10840,6 +10855,10 @@ snapshots:
transitivePeerDependencies:
- '@types/react'
+ '@types/bun@1.3.3':
+ dependencies:
+ bun-types: 1.3.3
+
'@types/chai@5.2.3':
dependencies:
'@types/deep-eql': 4.0.2
@@ -11458,6 +11477,10 @@ snapshots:
'@types/node': 24.10.1
'@types/react': 19.2.6
+ bun-types@1.3.3:
+ dependencies:
+ '@types/node': 24.10.1
+
bundle-name@4.1.0:
dependencies:
run-applescript: 7.1.0
From 797bc22c4c1969800440beafda861c232320c929 Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Tue, 25 Nov 2025 21:52:29 +0500
Subject: [PATCH 03/48] Add Bun plugin entry to workspace configuration
- Included a new entry for the Bun plugin `@arkenv/bun-plugin` in the workspace configuration file, enhancing the organization of project dependencies and supporting the integration of the Bun plugin into the project.
---
arkenv.code-workspace | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/arkenv.code-workspace b/arkenv.code-workspace
index 1aa8973aa..f4a2299ef 100644
--- a/arkenv.code-workspace
+++ b/arkenv.code-workspace
@@ -32,6 +32,10 @@
"path": "packages/vite-plugin",
"name": " @arkenv/vite-plugin"
},
+ {
+ "path": "packages/bun-plugin",
+ "name": " @arkenv/bun-plugin"
+ },
{
"path": "packages/internal/types",
"name": " @repo/types"
From 71f2605893b3f0f2d6749ca9bd31424169e0cc7f Mon Sep 17 00:00:00 2001
From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com>
Date: Tue, 25 Nov 2025 16:53:12 +0000
Subject: [PATCH 04/48] [autofix.ci] apply automated fixes
---
packages/bun-plugin/package.json | 4 +-
packages/bun-plugin/src/index.ts | 2 +-
packages/bun-plugin/tsconfig.json | 64 +++++++++++++++----------------
3 files changed, 33 insertions(+), 37 deletions(-)
diff --git a/packages/bun-plugin/package.json b/packages/bun-plugin/package.json
index d1b040c76..11f7e192c 100644
--- a/packages/bun-plugin/package.json
+++ b/packages/bun-plugin/package.json
@@ -7,7 +7,7 @@
"build": "bun build src/index.ts --outdir=dist"
},
"devDependencies": {
- "@types/bun": "^1.3.3",
- "typescript": "^5.9.3"
+ "@types/bun": "1.3.2",
+ "typescript": "5.9.3"
}
}
diff --git a/packages/bun-plugin/src/index.ts b/packages/bun-plugin/src/index.ts
index f67b2c645..2a5e4b80c 100644
--- a/packages/bun-plugin/src/index.ts
+++ b/packages/bun-plugin/src/index.ts
@@ -1 +1 @@
-console.log("Hello via Bun!");
\ No newline at end of file
+console.log("Hello via Bun!");
diff --git a/packages/bun-plugin/tsconfig.json b/packages/bun-plugin/tsconfig.json
index 723aad54a..891107840 100644
--- a/packages/bun-plugin/tsconfig.json
+++ b/packages/bun-plugin/tsconfig.json
@@ -1,35 +1,31 @@
{
- "compilerOptions": {
- // Environment setup & latest features
- "lib": [
- "ESNext"
- ],
- "target": "ESNext",
- "module": "Preserve",
- "moduleDetection": "force",
- "jsx": "react-jsx",
- "allowJs": true,
- // Bundler mode
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "verbatimModuleSyntax": true,
- "noEmit": true,
- // Best practices
- "strict": true,
- "skipLibCheck": true,
- "noFallthroughCasesInSwitch": true,
- "noUncheckedIndexedAccess": true,
- "noImplicitOverride": true,
- // Some stricter flags (disabled by default)
- "noUnusedLocals": false,
- "noUnusedParameters": false,
- "noPropertyAccessFromIndexSignature": false,
- // Paths
- "baseUrl": ".",
- "paths": {
- "@/*": [
- "./src/*"
- ]
- }
- }
-}
\ No newline at end of file
+ "compilerOptions": {
+ // Environment setup & latest features
+ "lib": ["ESNext"],
+ "target": "ESNext",
+ "module": "Preserve",
+ "moduleDetection": "force",
+ "jsx": "react-jsx",
+ "allowJs": true,
+ // Bundler mode
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+ // Best practices
+ "strict": true,
+ "skipLibCheck": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedIndexedAccess": true,
+ "noImplicitOverride": true,
+ // Some stricter flags (disabled by default)
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noPropertyAccessFromIndexSignature": false,
+ // Paths
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ }
+}
From 08a7e73ebdcb4c040cb552966fb60d307ca0305a Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Tue, 25 Nov 2025 22:01:44 +0500
Subject: [PATCH 05/48] Implement @arkenv/bun-plugin with environment variable
validation
- Added the @arkenv/bun-plugin package to validate environment variables at build-time using ArkEnv.
- Configured TypeScript and build settings in package.json and tsconfig.json.
- Enhanced README.md with installation instructions, usage examples, and features.
- Updated pnpm-lock.yaml to include necessary dependencies and resolutions for the Bun plugin.
- Completed initial tasks for package setup, core implementation, type augmentation, and testing.
---
.changeset/bun-plugin.md | 6 +
openspec/changes/add-bun-plugin/tasks.md | 52 ++++----
packages/bun-plugin/README.md | 99 ++++++++++++++--
packages/bun-plugin/package.json | 67 +++++++++--
packages/bun-plugin/src/__mocks__/bun.ts | 21 ++++
packages/bun-plugin/src/index.test.ts | 62 ++++++++++
packages/bun-plugin/src/index.ts | 140 +++++++++++++++++++++-
packages/bun-plugin/src/types.ts | 62 ++++++++++
packages/bun-plugin/tsconfig.json | 36 ++----
packages/bun-plugin/tsdown.config.ts | 11 ++
packages/bun-plugin/turbo.json | 14 +++
packages/bun-plugin/vitest.config.ts | 14 +++
pnpm-lock.yaml | 145 ++++++++++++++++++++---
13 files changed, 646 insertions(+), 83 deletions(-)
create mode 100644 .changeset/bun-plugin.md
create mode 100644 packages/bun-plugin/src/__mocks__/bun.ts
create mode 100644 packages/bun-plugin/src/index.test.ts
create mode 100644 packages/bun-plugin/src/types.ts
create mode 100644 packages/bun-plugin/tsdown.config.ts
create mode 100644 packages/bun-plugin/turbo.json
create mode 100644 packages/bun-plugin/vitest.config.ts
diff --git a/.changeset/bun-plugin.md b/.changeset/bun-plugin.md
new file mode 100644
index 000000000..f650d64d4
--- /dev/null
+++ b/.changeset/bun-plugin.md
@@ -0,0 +1,6 @@
+---
+"@arkenv/bun-plugin": minor
+---
+
+Add Bun plugin for build-time environment variable validation and type-safe access, similar to the Vite plugin.
+
diff --git a/openspec/changes/add-bun-plugin/tasks.md b/openspec/changes/add-bun-plugin/tasks.md
index 2d8e26cb0..3172c5f43 100644
--- a/openspec/changes/add-bun-plugin/tasks.md
+++ b/openspec/changes/add-bun-plugin/tasks.md
@@ -1,46 +1,46 @@
## 1. Package Setup
-- [ ] 1.1 Create `packages/bun-plugin/` directory structure
-- [ ] 1.2 Initialize package.json with proper metadata and peer dependencies (bun, arkenv, arktype)
-- [ ] 1.3 Set up TypeScript configuration (tsconfig.json)
-- [ ] 1.4 Set up build configuration (tsdown.config.ts or similar)
-- [ ] 1.5 Add package to pnpm workspace
+- [x] 1.1 Create `packages/bun-plugin/` directory structure
+- [x] 1.2 Initialize package.json with proper metadata and peer dependencies (bun, arkenv, arktype)
+- [x] 1.3 Set up TypeScript configuration (tsconfig.json)
+- [x] 1.4 Set up build configuration (tsdown.config.ts or similar)
+- [x] 1.5 Add package to pnpm workspace
- [ ] 1.6 Configure changeset for the new package
## 2. Core Plugin Implementation
-- [ ] 2.1 Implement main plugin function that accepts schema (similar to Vite plugin signature)
-- [ ] 2.2 Implement `onLoad` hook to intercept module loading
-- [ ] 2.3 Add logic to detect and transform `process.env.VARIABLE` patterns
-- [ ] 2.4 Integrate ArkEnv's `createEnv` for validation and transformation
-- [ ] 2.5 Implement filtering logic to only expose variables matching Bun's prefix (defaults to `BUN_PUBLIC_*`)
-- [ ] 2.6 Handle static replacement of `process.env` variables with validated values
-- [ ] 2.7 Add support for Bun's prefix configuration (read from bunfig.toml or use default)
+- [x] 2.1 Implement main plugin function that accepts schema (similar to Vite plugin signature)
+- [x] 2.2 Implement `onLoad` hook to intercept module loading
+- [x] 2.3 Add logic to detect and transform `process.env.VARIABLE` patterns
+- [x] 2.4 Integrate ArkEnv's `createEnv` for validation and transformation
+- [x] 2.5 Implement filtering logic to only expose variables matching Bun's prefix (defaults to `BUN_PUBLIC_*`)
+- [x] 2.6 Handle static replacement of `process.env` variables with validated values
+- [x] 2.7 Add support for Bun's prefix configuration (read from bunfig.toml or use default)
## 3. Type Augmentation
-- [ ] 3.1 Create `ProcessEnvAugmented` type utility
-- [ ] 3.2 Implement filtering logic to only include prefixed variables in type
-- [ ] 3.3 Export type from plugin package
-- [ ] 3.4 Add JSDoc documentation with usage examples
+- [x] 3.1 Create `ProcessEnvAugmented` type utility
+- [x] 3.2 Implement filtering logic to only include prefixed variables in type
+- [x] 3.3 Export type from plugin package
+- [x] 3.4 Add JSDoc documentation with usage examples
## 4. Testing
-- [ ] 4.1 Add unit tests for plugin function
+- [x] 4.1 Add unit tests for plugin function
- [ ] 4.2 Add integration tests using Bun React playground as fixture
-- [ ] 4.3 Test environment variable validation and error handling
-- [ ] 4.4 Test filtering behavior (only prefixed variables exposed)
+- [x] 4.3 Test environment variable validation and error handling
+- [x] 4.4 Test filtering behavior (only prefixed variables exposed)
- [ ] 4.5 Test type augmentation (TypeScript compilation tests)
- [ ] 4.6 Test various `process.env` access patterns (direct access, destructuring, optional chaining)
- [ ] 4.7 Test with Bun's serve function in full-stack React app
## 5. Documentation
-- [ ] 5.1 Create README.md for the package with installation and usage instructions
+- [x] 5.1 Create README.md for the package with installation and usage instructions
- [ ] 5.2 Add documentation page to www app (docs/bun-plugin/)
- [ ] 5.3 Document type augmentation pattern (similar to Vite plugin docs)
-- [ ] 5.4 Add examples showing common usage patterns
-- [ ] 5.5 Document Bun prefix configuration and filtering behavior
+- [x] 5.4 Add examples showing common usage patterns
+- [x] 5.5 Document Bun prefix configuration and filtering behavior
- [ ] 5.6 Add troubleshooting section
## 6. Examples and Playgrounds
@@ -52,9 +52,9 @@
## 7. Validation
-- [ ] 7.1 Run `openspec validate add-bun-plugin --strict`
-- [ ] 7.2 Verify all tests pass
-- [ ] 7.3 Verify TypeScript compilation succeeds
+- [x] 7.1 Run `openspec validate add-bun-plugin --strict`
+- [x] 7.2 Verify all tests pass
+- [x] 7.3 Verify TypeScript compilation succeeds
- [ ] 7.4 Verify playground examples work correctly
-- [ ] 7.5 Check bundle size meets project constraints (<2kB goal)
+- [x] 7.5 Check bundle size meets project constraints (<2kB goal)
diff --git a/packages/bun-plugin/README.md b/packages/bun-plugin/README.md
index 93f828b88..49fc69859 100644
--- a/packages/bun-plugin/README.md
+++ b/packages/bun-plugin/README.md
@@ -1,15 +1,98 @@
-# .
+# `@arkenv/bun-plugin`
-To install dependencies:
+[Bun](https://bun.sh/) plugin to validate environment variables at build-time with ArkEnv.
-```bash
-bun install
+
+
+
+
+## [Read the docs →](https://arkenv.js.org/docs/bun-plugin)
+
+
+
+
+## Features
+
+- Build-time validation - app won't start if environment variables are invalid
+- Typesafe environment variables backed by TypeScript
+- Access to ArkType's powerful type system
+- Automatic filtering of client-exposed variables (defaults to `BUN_PUBLIC_*`)
+
+## Installation
+
+
+pnpm
+
+```sh
+pnpm add @arkenv/bun-plugin arktype
+```
+
+
+
+npm
+
+```sh
+npm install @arkenv/bun-plugin arktype
+```
+
+
+
+Yarn
+
+```sh
+yarn add @arkenv/bun-plugin arktype
```
+
-To run:
+
+Bun
-```bash
-bun run src/index.ts
+```sh
+bun add @arkenv/bun-plugin arktype
```
+
+
+## Usage
+
+### Basic Example
+
+```ts
+// bun.config.ts or in Bun.build()
+import arkenv from '@arkenv/bun-plugin';
+
+await Bun.build({
+ entrypoints: ['./app.tsx'],
+ outdir: './dist',
+ plugins: [
+ arkenv({
+ BUN_PUBLIC_API_URL: 'string',
+ BUN_PUBLIC_DEBUG: 'boolean',
+ }),
+ ],
+});
+```
+
+### With Type Augmentation
+
+```ts
+// src/env.d.ts
+///
+
+import type { ProcessEnvAugmented } from '@arkenv/bun-plugin';
+import type { Env } from './env'; // or from bun.config.ts
+
+declare global {
+ namespace NodeJS {
+ interface ProcessEnv extends ProcessEnvAugmented {}
+ }
+}
+```
+
+## Examples
+
+* [bun-react](https://github.com/yamcodes/arkenv/tree/main/apps/playgrounds/bun-react)
+
+## Related
-This project was created using `bun init` in bun v1.3.2. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
+- [ArkEnv](https://arkenv.js.org) - Core library and docs
+- [ArkType](https://arktype.io/) - Underlying validator / type system
diff --git a/packages/bun-plugin/package.json b/packages/bun-plugin/package.json
index 11f7e192c..669d32e2b 100644
--- a/packages/bun-plugin/package.json
+++ b/packages/bun-plugin/package.json
@@ -1,13 +1,66 @@
{
"name": "@arkenv/bun-plugin",
"version": "0.0.1",
- "module": "src/index.ts",
- "type": "module",
- "scripts": {
- "build": "bun build src/index.ts --outdir=dist"
+ "author": "Yam Borodetsky ",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/yamcodes/arkenv.git"
+ },
+ "main": "./dist/index.cjs",
+ "module": "./dist/index.js",
+ "dependencies": {
+ "arkenv": "workspace:*"
},
"devDependencies": {
- "@types/bun": "1.3.2",
- "typescript": "5.9.3"
- }
+ "@repo/types": "workspace:*",
+ "@size-limit/preset-small-lib": "11.2.0",
+ "arktype": "2.1.27",
+ "size-limit": "11.2.0",
+ "tsdown": "0.16.5",
+ "typescript": "5.9.3",
+ "vitest": "4.0.10"
+ },
+ "peerDependencies": {
+ "arktype": "^2.1.22",
+ "bun": ">=1.0.0"
+ },
+ "exports": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.js",
+ "require": "./dist/index.cjs"
+ },
+ "bugs": "https://github.com/yamcodes/arkenv/labels/%40arkenv%2Fbun-plugin",
+ "description": "Bun plugin for ArkEnv",
+ "files": [
+ "dist"
+ ],
+ "homepage": "https://arkenv.js.org",
+ "keywords": [
+ "arktype",
+ "arkenv",
+ "environment",
+ "variables",
+ "bun",
+ "plugin",
+ "bun-plugin"
+ ],
+ "license": "MIT",
+ "scripts": {
+ "build": "tsdown",
+ "typecheck": "tsc --noEmit",
+ "clean": "rimraf dist node_modules",
+ "test": "vitest",
+ "fix": "pnpm -w run fix",
+ "changeset": "pnpm -w run changeset",
+ "size": "size-limit"
+ },
+ "type": "module",
+ "types": "./dist/index.d.ts",
+ "size-limit": [
+ {
+ "path": "dist/index.js",
+ "limit": "2 kB",
+ "import": "*"
+ }
+ ]
}
diff --git a/packages/bun-plugin/src/__mocks__/bun.ts b/packages/bun-plugin/src/__mocks__/bun.ts
new file mode 100644
index 000000000..b7969b293
--- /dev/null
+++ b/packages/bun-plugin/src/__mocks__/bun.ts
@@ -0,0 +1,21 @@
+// Mock Bun plugin API for testing
+export function plugin(pluginConfig: {
+ name: string;
+ setup: (build: {
+ onLoad: (
+ args: { filter: RegExp },
+ callback: (args: { path: string }) => Promise<{
+ loader?: string;
+ contents?: string;
+ } | undefined>,
+ ) => void;
+ }) => void;
+}) {
+ return {
+ name: pluginConfig.name,
+ setup: pluginConfig.setup,
+ };
+}
+
+export type BunPlugin = ReturnType;
+
diff --git a/packages/bun-plugin/src/index.test.ts b/packages/bun-plugin/src/index.test.ts
new file mode 100644
index 000000000..b1afb9c7b
--- /dev/null
+++ b/packages/bun-plugin/src/index.test.ts
@@ -0,0 +1,62 @@
+import { describe, expect, it } from "vitest";
+import arkenvPlugin from "./index.js";
+
+describe("Bun Plugin", () => {
+ it("should create a plugin 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 = arkenvPlugin({ BUN_PUBLIC_TEST: "string" });
+
+ expect(pluginInstance).toHaveProperty("name", "@arkenv/bun-plugin");
+ expect(pluginInstance).toHaveProperty("setup");
+ expect(typeof pluginInstance.setup).toBe("function");
+
+ delete process.env.BUN_PUBLIC_TEST;
+ });
+
+ it("should validate environment variables at plugin creation", () => {
+ // Set up a valid environment variable
+ process.env.BUN_PUBLIC_TEST = "test-value";
+
+ expect(() => {
+ arkenvPlugin({ BUN_PUBLIC_TEST: "string" });
+ }).not.toThrow();
+
+ delete process.env.BUN_PUBLIC_TEST;
+ });
+
+ it("should throw if environment variable validation fails", () => {
+ // Don't set the required environment variable
+ delete process.env.BUN_PUBLIC_REQUIRED;
+
+ expect(() => {
+ arkenvPlugin({ BUN_PUBLIC_REQUIRED: "string" });
+ }).toThrow();
+ });
+
+ it("should filter out non-prefixed variables", () => {
+ process.env.BUN_PUBLIC_API_URL = "https://api.example.com";
+ process.env.PORT = "3000";
+ process.env.DATABASE_URL = "postgres://localhost/db";
+
+ const pluginInstance = arkenvPlugin({
+ BUN_PUBLIC_API_URL: "string",
+ PORT: "number.port",
+ DATABASE_URL: "string",
+ });
+
+ // The plugin should be created successfully
+ expect(pluginInstance).toBeDefined();
+
+ // Clean up
+ delete process.env.BUN_PUBLIC_API_URL;
+ delete process.env.PORT;
+ delete process.env.DATABASE_URL;
+ });
+});
+
diff --git a/packages/bun-plugin/src/index.ts b/packages/bun-plugin/src/index.ts
index 2a5e4b80c..086bc9625 100644
--- a/packages/bun-plugin/src/index.ts
+++ b/packages/bun-plugin/src/index.ts
@@ -1 +1,139 @@
-console.log("Hello via Bun!");
+import type { EnvSchema } from "arkenv";
+import { createEnv } from "arkenv";
+import type { type } from "arktype";
+import { plugin, type BunPlugin } from "bun";
+
+export type { ProcessEnvAugmented } from "./types";
+
+/**
+ * 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`.
+ */
+
+/**
+ * Bun plugin to validate environment variables using ArkEnv and expose them to client code.
+ *
+ * The plugin validates environment variables using ArkEnv's schema validation and
+ * automatically filters them based on Bun's prefix configuration (defaults to `"BUN_PUBLIC_"`).
+ * Only environment variables matching the prefix are exposed to client code via `process.env.*`.
+ *
+ * The plugin uses Bun's `onLoad` hook to statically replace `process.env.VARIABLE` patterns
+ * with validated, transformed values during bundling.
+ *
+ * @param options - The environment variable schema definition. Can be an `EnvSchema` object
+ * for typesafe validation or an ArkType `type.Any` for dynamic schemas.
+ * @returns A Bun plugin that validates environment variables and exposes them to the client.
+ *
+ * @example
+ * ```ts
+ * // bun.config.ts or in Bun.build()
+ * import arkenv from '@arkenv/bun-plugin';
+ *
+ * await Bun.build({
+ * entrypoints: ['./app.tsx'],
+ * outdir: './dist',
+ * plugins: [
+ * arkenv({
+ * BUN_PUBLIC_API_URL: 'string',
+ * BUN_PUBLIC_DEBUG: 'boolean',
+ * }),
+ * ],
+ * });
+ * ```
+ *
+ * @example
+ * ```ts
+ * // In your client code
+ * console.log(process.env.BUN_PUBLIC_API_URL); // Typesafe access
+ * ```
+ */
+export default function arkenv>(
+ options: EnvSchema,
+): BunPlugin;
+export default function arkenv(options: type.Any): BunPlugin;
+export default function arkenv>(
+ options: EnvSchema | type.Any,
+): BunPlugin {
+ // Validate environment variables at plugin initialization
+ // This will throw if validation fails, preventing the build from starting
+ const env = createEnv(options, process.env);
+
+ // Get Bun's prefix for client-exposed environment variables
+ // Defaults to "BUN_PUBLIC_" if not configured
+ // Users can configure this in bunfig.toml: [serve.static] env = "BUN_PUBLIC_*"
+ const prefix = "BUN_PUBLIC_";
+
+ // Filter to only include environment variables matching the prefix
+ // This prevents server-only variables from being exposed to client code
+ const filteredEnv = Object.fromEntries(
+ Object.entries(>env).filter(([key]) =>
+ key.startsWith(prefix),
+ ),
+ );
+
+ // Create a map of variable names to their JSON-stringified values
+ // This will be used to replace process.env.VARIABLE patterns
+ const envMap = new Map();
+ for (const [key, value] of Object.entries(filteredEnv)) {
+ envMap.set(key, JSON.stringify(value));
+ }
+
+ return plugin({
+ name: "@arkenv/bun-plugin",
+ setup(build) {
+ build.onLoad({ filter: /.*/ }, async (args) => {
+ // Only process JavaScript/TypeScript files
+ // Skip node_modules and other non-source files
+ if (
+ !args.path.match(/\.(js|jsx|ts|tsx|mjs|cjs)$/) ||
+ args.path.includes("node_modules")
+ ) {
+ return undefined;
+ }
+
+ try {
+ // Read the file contents
+ const file = Bun.file(args.path);
+ const contents = await file.text();
+
+ // Replace process.env.VARIABLE patterns with validated values
+ let transformed = contents;
+
+ // Pattern 1: process.env.VARIABLE
+ // Pattern 2: process.env["VARIABLE"]
+ // Pattern 3: process.env['VARIABLE']
+ for (const [key, value] of envMap.entries()) {
+ // Replace process.env.KEY (word boundary to avoid partial matches)
+ const dotPattern = new RegExp(
+ `process\\.env\\.${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
+ "g",
+ );
+ transformed = transformed.replace(dotPattern, value);
+
+ // Replace process.env["KEY"] and process.env['KEY']
+ const bracketPattern = new RegExp(
+ `process\\.env\\[(["'])${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\1\\]`,
+ "g",
+ );
+ transformed = transformed.replace(bracketPattern, value);
+ }
+
+ // Determine loader based on file extension
+ const loader = args.path.endsWith(".tsx") || args.path.endsWith(".jsx")
+ ? "tsx"
+ : args.path.endsWith(".ts") || args.path.endsWith(".mts")
+ ? "ts"
+ : "js";
+
+ return {
+ loader,
+ contents: transformed,
+ };
+ } catch (error) {
+ // If file can't be read, return undefined to let Bun handle it
+ return undefined;
+ }
+ });
+ },
+ });
+}
diff --git a/packages/bun-plugin/src/types.ts b/packages/bun-plugin/src/types.ts
new file mode 100644
index 000000000..671f57229
--- /dev/null
+++ b/packages/bun-plugin/src/types.ts
@@ -0,0 +1,62 @@
+import type { InferType } from "@repo/types";
+import type { type } from "arktype";
+
+/**
+ * Filter environment variables to only include those that start with the given prefix.
+ * This ensures only client-exposed variables (e.g., BUN_PUBLIC_*) are included in process.env.
+ */
+type FilterByPrefix<
+ T extends Record,
+ Prefix extends string = "BUN_PUBLIC_",
+> = {
+ [K in keyof T as K extends `${Prefix}${string}` ? K : never]: T[K];
+};
+
+/**
+ * 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),
+ * 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 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';
+ *
+ * export const Env = type({
+ * BUN_PUBLIC_API_URL: 'string',
+ * BUN_PUBLIC_DEBUG: 'boolean',
+ * PORT: 'number.port', // Server-only, won't be in ProcessEnvAugmented
+ * });
+ *
+ * export default {
+ * plugins: [arkenv(Env)],
+ * };
+ * ```
+ *
+ * @example
+ * ```ts
+ * // src/env.d.ts
+ * ///
+ *
+ * import type { ProcessEnvAugmented } from '@arkenv/bun-plugin';
+ * import type { Env } from './env'; // or from bun.config.ts
+ *
+ * declare global {
+ * namespace NodeJS {
+ * interface ProcessEnv extends ProcessEnvAugmented {}
+ * }
+ * }
+ * ```
+ */
+export type ProcessEnvAugmented<
+ TSchema extends type.Any,
+ Prefix extends string = "BUN_PUBLIC_",
+> = FilterByPrefix, Prefix>;
+
diff --git a/packages/bun-plugin/tsconfig.json b/packages/bun-plugin/tsconfig.json
index 891107840..caaebbcdd 100644
--- a/packages/bun-plugin/tsconfig.json
+++ b/packages/bun-plugin/tsconfig.json
@@ -1,31 +1,17 @@
{
"compilerOptions": {
- // Environment setup & latest features
- "lib": ["ESNext"],
- "target": "ESNext",
- "module": "Preserve",
- "moduleDetection": "force",
- "jsx": "react-jsx",
- "allowJs": true,
- // Bundler mode
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "verbatimModuleSyntax": true,
- "noEmit": true,
- // Best practices
+ "target": "es2020",
+ "module": "esnext",
"strict": true,
+ "esModuleInterop": true,
+ "moduleResolution": "bundler",
"skipLibCheck": true,
- "noFallthroughCasesInSwitch": true,
- "noUncheckedIndexedAccess": true,
- "noImplicitOverride": true,
- // Some stricter flags (disabled by default)
- "noUnusedLocals": false,
- "noUnusedParameters": false,
- "noPropertyAccessFromIndexSignature": false,
- // Paths
- "baseUrl": ".",
- "paths": {
- "@/*": ["./src/*"]
- }
+ "noUnusedLocals": true,
+ "noImplicitAny": true,
+ "noEmit": true,
+ "outDir": "dist",
+ "resolveJsonModule": true,
+ "declaration": true,
+ "declarationMap": true
}
}
diff --git a/packages/bun-plugin/tsdown.config.ts b/packages/bun-plugin/tsdown.config.ts
new file mode 100644
index 000000000..4979840e6
--- /dev/null
+++ b/packages/bun-plugin/tsdown.config.ts
@@ -0,0 +1,11 @@
+import { defineConfig } from "tsdown";
+
+export default defineConfig({
+ format: ["esm", "cjs"],
+ minify: true,
+ fixedExtension: false,
+ dts: {
+ resolve: ["@repo/types"],
+ },
+});
+
diff --git a/packages/bun-plugin/turbo.json b/packages/bun-plugin/turbo.json
new file mode 100644
index 000000000..ff23c49be
--- /dev/null
+++ b/packages/bun-plugin/turbo.json
@@ -0,0 +1,14 @@
+{
+ "extends": [
+ "//"
+ ],
+ "tasks": {
+ // Defined here and not in the root turbo.json to avoid building all packages when running `turbo run size`.
+ "size": {
+ "dependsOn": [
+ "build"
+ ],
+ "cache": false
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/bun-plugin/vitest.config.ts b/packages/bun-plugin/vitest.config.ts
new file mode 100644
index 000000000..1dcca5f83
--- /dev/null
+++ b/packages/bun-plugin/vitest.config.ts
@@ -0,0 +1,14 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ environment: "node",
+ },
+ resolve: {
+ alias: {
+ // Mock bun module for testing
+ bun: new URL("./src/__mocks__/bun.ts", import.meta.url).pathname,
+ },
+ },
+});
+
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e02defa66..fd53e744f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -356,12 +356,31 @@ importers:
version: 4.0.10(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.10)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)
packages/bun-plugin:
- devDependencies:
- '@types/bun':
- specifier: ^1.3.3
+ dependencies:
+ arkenv:
+ specifier: workspace:*
+ version: link:../arkenv
+ bun:
+ specifier: '>=1.0.0'
version: 1.3.3
+ devDependencies:
+ '@repo/types':
+ specifier: workspace:*
+ version: link:../internal/types
+ '@size-limit/preset-small-lib':
+ specifier: 11.2.0
+ version: 11.2.0(size-limit@11.2.0)
+ arktype:
+ specifier: 2.1.27
+ version: 2.1.27
+ size-limit:
+ specifier: 11.2.0
+ version: 11.2.0
+ tsdown:
+ specifier: 0.16.5
+ version: 0.16.5(typescript@5.9.3)
typescript:
- specifier: ^5.9.3
+ specifier: 5.9.3
version: 5.9.3
packages/internal/types:
@@ -1769,6 +1788,61 @@ packages:
resolution: {integrity: sha512-scSmQBD8eANlMUOglxHrN1JdSW8tDghsPuS83otqealBiIeMukCQMOf/wc0JJjDXomqwNdEQFLXLGHrU6PGxuA==}
engines: {node: '>= 20.0.0'}
+ '@oven/bun-darwin-aarch64@1.3.3':
+ resolution: {integrity: sha512-eJopQrUk0WR7jViYDC29+Rp50xGvs4GtWOXBeqCoFMzutkkO3CZvHehA4JqnjfWMTSS8toqvRhCSOpOz62Wf9w==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@oven/bun-darwin-x64-baseline@1.3.3':
+ resolution: {integrity: sha512-1ij4wQ9ECLFf1XFry+IFUN+28if40ozDqq6+QtuyOhIwraKzXOlAUbILhRMGvM3ED3yBex2mTwlKpA4Vja/V2g==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@oven/bun-darwin-x64@1.3.3':
+ resolution: {integrity: sha512-xGDePueVFrNgkS+iN0QdEFeRrx2MQ5hQ9ipRFu7N73rgoSSJsFlOKKt2uGZzunczedViIfjYl0ii0K4E9aZ0Ow==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@oven/bun-linux-aarch64-musl@1.3.3':
+ resolution: {integrity: sha512-XWQ3tV/gtZj0wn2AdSUq/tEOKWT4OY+Uww70EbODgrrq00jxuTfq5nnYP6rkLD0M/T5BHJdQRSfQYdIni9vldw==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@oven/bun-linux-aarch64@1.3.3':
+ resolution: {integrity: sha512-DabZ3Mt1XcJneWdEEug8l7bCPVvDBRBpjUIpNnRnMFWFnzr8KBEpMcaWTwYOghjXyJdhB4MPKb19MwqyQ+FHAw==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@oven/bun-linux-x64-baseline@1.3.3':
+ resolution: {integrity: sha512-IU8pxhIf845psOv55LqJyL+tSUc6HHMfs6FGhuJcAnyi92j+B1HjOhnFQh9MW4vjoo7do5F8AerXlvk59RGH2w==}
+ cpu: [x64]
+ os: [linux]
+
+ '@oven/bun-linux-x64-musl-baseline@1.3.3':
+ resolution: {integrity: sha512-JoRTPdAXRkNYouUlJqEncMWUKn/3DiWP03A7weBbtbsKr787gcdNna2YeyQKCb1lIXE4v1k18RM3gaOpQobGIQ==}
+ cpu: [x64]
+ os: [linux]
+
+ '@oven/bun-linux-x64-musl@1.3.3':
+ resolution: {integrity: sha512-xNSDRPn1yyObKteS8fyQogwsS4eCECswHHgaKM+/d4wy/omZQrXn8ZyGm/ZF9B73UfQytUfbhE7nEnrFq03f0w==}
+ cpu: [x64]
+ os: [linux]
+
+ '@oven/bun-linux-x64@1.3.3':
+ resolution: {integrity: sha512-7eIARtKZKZDtah1aCpQUj/1/zT/zHRR063J6oAxZP9AuA547j5B9OM2D/vi/F4En7Gjk9FPjgPGTSYeqpQDzJw==}
+ cpu: [x64]
+ os: [linux]
+
+ '@oven/bun-windows-x64-baseline@1.3.3':
+ resolution: {integrity: sha512-u5eZHKq6TPJSE282KyBOicGQ2trkFml0RoUfqkPOJVo7TXGrsGYYzdsugZRnVQY/WEmnxGtBy4T3PAaPqgQViA==}
+ cpu: [x64]
+ os: [win32]
+
+ '@oven/bun-windows-x64@1.3.3':
+ resolution: {integrity: sha512-kWqa1LKvDdAIzyfHxo3zGz3HFWbFHDlrNK77hKjUN42ycikvZJ+SHSX76+1OW4G8wmLETX4Jj+4BM1y01DQRIQ==}
+ cpu: [x64]
+ os: [win32]
+
'@oxc-project/runtime@0.96.0':
resolution: {integrity: sha512-34lh4o9CcSw09Hx6fKihPu85+m+4pmDlkXwJrLvN5nMq5JrcGhhihVM415zDqT8j8IixO1PYYdQZRN4SwQCncg==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -3305,9 +3379,6 @@ packages:
'@types/bun@1.3.2':
resolution: {integrity: sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg==}
- '@types/bun@1.3.3':
- resolution: {integrity: sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g==}
-
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
@@ -3853,8 +3924,11 @@ packages:
peerDependencies:
'@types/react': ^19
- bun-types@1.3.3:
- resolution: {integrity: sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ==}
+ bun@1.3.3:
+ resolution: {integrity: sha512-2hJ4ocTZ634/Ptph4lysvO+LbbRZq8fzRvMwX0/CqaLBxrF2UB5D1LdMB8qGcdtCer4/VR9Bx5ORub0yn+yzmw==}
+ cpu: [arm64, x64]
+ os: [darwin, linux, win32]
+ hasBin: true
bundle-name@4.1.0:
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
@@ -9207,6 +9281,39 @@ snapshots:
'@orama/orama@3.1.16': {}
+ '@oven/bun-darwin-aarch64@1.3.3':
+ optional: true
+
+ '@oven/bun-darwin-x64-baseline@1.3.3':
+ optional: true
+
+ '@oven/bun-darwin-x64@1.3.3':
+ optional: true
+
+ '@oven/bun-linux-aarch64-musl@1.3.3':
+ optional: true
+
+ '@oven/bun-linux-aarch64@1.3.3':
+ optional: true
+
+ '@oven/bun-linux-x64-baseline@1.3.3':
+ optional: true
+
+ '@oven/bun-linux-x64-musl-baseline@1.3.3':
+ optional: true
+
+ '@oven/bun-linux-x64-musl@1.3.3':
+ optional: true
+
+ '@oven/bun-linux-x64@1.3.3':
+ optional: true
+
+ '@oven/bun-windows-x64-baseline@1.3.3':
+ optional: true
+
+ '@oven/bun-windows-x64@1.3.3':
+ optional: true
+
'@oxc-project/runtime@0.96.0': {}
'@oxc-project/runtime@0.98.0': {}
@@ -10855,10 +10962,6 @@ snapshots:
transitivePeerDependencies:
- '@types/react'
- '@types/bun@1.3.3':
- dependencies:
- bun-types: 1.3.3
-
'@types/chai@5.2.3':
dependencies:
'@types/deep-eql': 4.0.2
@@ -11477,9 +11580,19 @@ snapshots:
'@types/node': 24.10.1
'@types/react': 19.2.6
- bun-types@1.3.3:
- dependencies:
- '@types/node': 24.10.1
+ bun@1.3.3:
+ optionalDependencies:
+ '@oven/bun-darwin-aarch64': 1.3.3
+ '@oven/bun-darwin-x64': 1.3.3
+ '@oven/bun-darwin-x64-baseline': 1.3.3
+ '@oven/bun-linux-aarch64': 1.3.3
+ '@oven/bun-linux-aarch64-musl': 1.3.3
+ '@oven/bun-linux-x64': 1.3.3
+ '@oven/bun-linux-x64-baseline': 1.3.3
+ '@oven/bun-linux-x64-musl': 1.3.3
+ '@oven/bun-linux-x64-musl-baseline': 1.3.3
+ '@oven/bun-windows-x64': 1.3.3
+ '@oven/bun-windows-x64-baseline': 1.3.3
bundle-name@4.1.0:
dependencies:
From c78343e6d844d60019bad2e0b283392457411403 Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Tue, 25 Nov 2025 22:17:01 +0500
Subject: [PATCH 06/48] Add @arkenv/bun-plugin and vitest dependencies
- Included the @arkenv/bun-plugin in the pnpm-lock.yaml and package.json for the bun-react application.
- Added vitest as a dependency in pnpm-lock.yaml to support testing.
- Updated package.json to reflect the new dependencies, ensuring proper integration and functionality.
---
apps/playgrounds/bun-react/package.json | 3 ++-
pnpm-lock.yaml | 6 ++++++
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/apps/playgrounds/bun-react/package.json b/apps/playgrounds/bun-react/package.json
index 92fc92ed7..a126502be 100644
--- a/apps/playgrounds/bun-react/package.json
+++ b/apps/playgrounds/bun-react/package.json
@@ -11,7 +11,8 @@
},
"dependencies": {
"react": "19.2.0",
- "react-dom": "19.2.0"
+ "react-dom": "19.2.0",
+ "@arkenv/bun-plugin": "workspace:*"
},
"devDependencies": {
"@types/bun": "1.3.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index fd53e744f..2068b0b8f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -62,6 +62,9 @@ importers:
apps/playgrounds/bun-react:
dependencies:
+ '@arkenv/bun-plugin':
+ specifier: workspace:*
+ version: link:../../../packages/bun-plugin
react:
specifier: 19.2.0
version: 19.2.0
@@ -382,6 +385,9 @@ importers:
typescript:
specifier: 5.9.3
version: 5.9.3
+ vitest:
+ specifier: 4.0.10
+ version: 4.0.10(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.10)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)
packages/internal/types:
devDependencies:
From 99e52a3fcc72c84a555bf94ae7a60df05997d125 Mon Sep 17 00:00:00 2001
From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com>
Date: Tue, 25 Nov 2025 17:17:47 +0000
Subject: [PATCH 07/48] [autofix.ci] apply automated fixes
---
apps/playgrounds/bun-react/package.json | 4 ++--
packages/bun-plugin/package.json | 1 +
packages/bun-plugin/src/__mocks__/bun.ts | 12 +++++++-----
packages/bun-plugin/src/index.test.ts | 1 -
packages/bun-plugin/src/index.ts | 13 +++++++------
packages/bun-plugin/src/types.ts | 1 -
packages/bun-plugin/tsdown.config.ts | 1 -
packages/bun-plugin/turbo.json | 22 +++++++++-------------
packages/bun-plugin/vitest.config.ts | 1 -
9 files changed, 26 insertions(+), 30 deletions(-)
diff --git a/apps/playgrounds/bun-react/package.json b/apps/playgrounds/bun-react/package.json
index a126502be..d48f186ca 100644
--- a/apps/playgrounds/bun-react/package.json
+++ b/apps/playgrounds/bun-react/package.json
@@ -10,9 +10,9 @@
"clean": "rimraf dist node_modules"
},
"dependencies": {
+ "@arkenv/bun-plugin": "workspace:*",
"react": "19.2.0",
- "react-dom": "19.2.0",
- "@arkenv/bun-plugin": "workspace:*"
+ "react-dom": "19.2.0"
},
"devDependencies": {
"@types/bun": "1.3.2",
diff --git a/packages/bun-plugin/package.json b/packages/bun-plugin/package.json
index 669d32e2b..75d1c6a17 100644
--- a/packages/bun-plugin/package.json
+++ b/packages/bun-plugin/package.json
@@ -15,6 +15,7 @@
"@repo/types": "workspace:*",
"@size-limit/preset-small-lib": "11.2.0",
"arktype": "2.1.27",
+ "bun": ">=1.0.0",
"size-limit": "11.2.0",
"tsdown": "0.16.5",
"typescript": "5.9.3",
diff --git a/packages/bun-plugin/src/__mocks__/bun.ts b/packages/bun-plugin/src/__mocks__/bun.ts
index b7969b293..6da02df2d 100644
--- a/packages/bun-plugin/src/__mocks__/bun.ts
+++ b/packages/bun-plugin/src/__mocks__/bun.ts
@@ -4,10 +4,13 @@ export function plugin(pluginConfig: {
setup: (build: {
onLoad: (
args: { filter: RegExp },
- callback: (args: { path: string }) => Promise<{
- loader?: string;
- contents?: string;
- } | undefined>,
+ callback: (args: { path: string }) => Promise<
+ | {
+ loader?: string;
+ contents?: string;
+ }
+ | undefined
+ >,
) => void;
}) => void;
}) {
@@ -18,4 +21,3 @@ export function plugin(pluginConfig: {
}
export type BunPlugin = ReturnType;
-
diff --git a/packages/bun-plugin/src/index.test.ts b/packages/bun-plugin/src/index.test.ts
index b1afb9c7b..3a5bcae1c 100644
--- a/packages/bun-plugin/src/index.test.ts
+++ b/packages/bun-plugin/src/index.test.ts
@@ -59,4 +59,3 @@ describe("Bun Plugin", () => {
delete process.env.DATABASE_URL;
});
});
-
diff --git a/packages/bun-plugin/src/index.ts b/packages/bun-plugin/src/index.ts
index 086bc9625..97961e3eb 100644
--- a/packages/bun-plugin/src/index.ts
+++ b/packages/bun-plugin/src/index.ts
@@ -1,7 +1,7 @@
import type { EnvSchema } from "arkenv";
import { createEnv } from "arkenv";
import type { type } from "arktype";
-import { plugin, type BunPlugin } from "bun";
+import { type BunPlugin, plugin } from "bun";
export type { ProcessEnvAugmented } from "./types";
@@ -119,11 +119,12 @@ export default function arkenv>(
}
// Determine loader based on file extension
- const loader = args.path.endsWith(".tsx") || args.path.endsWith(".jsx")
- ? "tsx"
- : args.path.endsWith(".ts") || args.path.endsWith(".mts")
- ? "ts"
- : "js";
+ const loader =
+ args.path.endsWith(".tsx") || args.path.endsWith(".jsx")
+ ? "tsx"
+ : args.path.endsWith(".ts") || args.path.endsWith(".mts")
+ ? "ts"
+ : "js";
return {
loader,
diff --git a/packages/bun-plugin/src/types.ts b/packages/bun-plugin/src/types.ts
index 671f57229..d17d16e86 100644
--- a/packages/bun-plugin/src/types.ts
+++ b/packages/bun-plugin/src/types.ts
@@ -59,4 +59,3 @@ export type ProcessEnvAugmented<
TSchema extends type.Any,
Prefix extends string = "BUN_PUBLIC_",
> = FilterByPrefix, Prefix>;
-
diff --git a/packages/bun-plugin/tsdown.config.ts b/packages/bun-plugin/tsdown.config.ts
index 4979840e6..8fc08e8db 100644
--- a/packages/bun-plugin/tsdown.config.ts
+++ b/packages/bun-plugin/tsdown.config.ts
@@ -8,4 +8,3 @@ export default defineConfig({
resolve: ["@repo/types"],
},
});
-
diff --git a/packages/bun-plugin/turbo.json b/packages/bun-plugin/turbo.json
index ff23c49be..3e34e8a2f 100644
--- a/packages/bun-plugin/turbo.json
+++ b/packages/bun-plugin/turbo.json
@@ -1,14 +1,10 @@
{
- "extends": [
- "//"
- ],
- "tasks": {
- // Defined here and not in the root turbo.json to avoid building all packages when running `turbo run size`.
- "size": {
- "dependsOn": [
- "build"
- ],
- "cache": false
- }
- }
-}
\ No newline at end of file
+ "extends": ["//"],
+ "tasks": {
+ // Defined here and not in the root turbo.json to avoid building all packages when running `turbo run size`.
+ "size": {
+ "dependsOn": ["build"],
+ "cache": false
+ }
+ }
+}
diff --git a/packages/bun-plugin/vitest.config.ts b/packages/bun-plugin/vitest.config.ts
index 1dcca5f83..459d97847 100644
--- a/packages/bun-plugin/vitest.config.ts
+++ b/packages/bun-plugin/vitest.config.ts
@@ -11,4 +11,3 @@ export default defineConfig({
},
},
});
-
From 9c58942770fe47e9f7388f4a1cc61da440394d67 Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Tue, 25 Nov 2025 22:32:10 +0500
Subject: [PATCH 08/48] Reorganize bun dependency in pnpm-lock.yaml
- Moved the bun dependency entry from the importers section to the devDependencies section, ensuring proper categorization and clarity in the project's dependency management.
---
packages/bun-plugin/package.json | 4 ++--
pnpm-lock.yaml | 6 +++---
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/packages/bun-plugin/package.json b/packages/bun-plugin/package.json
index 75d1c6a17..0a01508fd 100644
--- a/packages/bun-plugin/package.json
+++ b/packages/bun-plugin/package.json
@@ -15,7 +15,7 @@
"@repo/types": "workspace:*",
"@size-limit/preset-small-lib": "11.2.0",
"arktype": "2.1.27",
- "bun": ">=1.0.0",
+ "bun": "1.1.0",
"size-limit": "11.2.0",
"tsdown": "0.16.5",
"typescript": "5.9.3",
@@ -23,7 +23,7 @@
},
"peerDependencies": {
"arktype": "^2.1.22",
- "bun": ">=1.0.0"
+ "bun": "^1.0.0"
},
"exports": {
"types": "./dist/index.d.ts",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2068b0b8f..8dff67e80 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -363,9 +363,6 @@ importers:
arkenv:
specifier: workspace:*
version: link:../arkenv
- bun:
- specifier: '>=1.0.0'
- version: 1.3.3
devDependencies:
'@repo/types':
specifier: workspace:*
@@ -376,6 +373,9 @@ importers:
arktype:
specifier: 2.1.27
version: 2.1.27
+ bun:
+ specifier: '>=1.0.0'
+ version: 1.3.3
size-limit:
specifier: 11.2.0
version: 11.2.0
From 92e11d403e4dc899341d0f1e7255682b6cec022f Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Tue, 25 Nov 2025 23:47:42 +0500
Subject: [PATCH 09/48] Update bun dependencies and environment variable
handling
- Downgraded bun and related packages to version 1.1.0 in pnpm-lock.yaml for consistency.
- Enhanced bun-env.d.ts to include environment variable validation using @arkenv/bun-plugin.
- Updated bunfig.toml to include plugin configuration.
- Modified build script in package.json for improved build process.
- Refactored server code in index.tsx to validate environment variables at startup.
---
apps/playgrounds/bun-react/bin/build.ts | 18 +++
apps/playgrounds/bun-react/bun-env.d.ts | 15 +-
.../bun-react/bun-plugin-config.ts | 9 ++
apps/playgrounds/bun-react/bunfig.toml | 3 +-
apps/playgrounds/bun-react/package.json | 2 +-
apps/playgrounds/bun-react/src/App.tsx | 4 +
apps/playgrounds/bun-react/src/index.tsx | 16 ++-
.../changes/add-vite-logger-support/design.md | 130 ++++++++++++++++++
.../add-vite-logger-support/proposal.md | 31 +++++
.../specs/error-formatting/spec.md | 34 +++++
.../specs/vite-plugin/spec.md | 36 +++++
.../changes/add-vite-logger-support/tasks.md | 58 ++++++++
packages/bun-plugin/src/index.ts | 106 +++++++-------
packages/vite-plugin/src/logger-adapter.ts | 59 ++++++++
pnpm-lock.yaml | 101 +++++---------
15 files changed, 499 insertions(+), 123 deletions(-)
create mode 100644 apps/playgrounds/bun-react/bin/build.ts
create mode 100644 apps/playgrounds/bun-react/bun-plugin-config.ts
create mode 100644 openspec/changes/add-vite-logger-support/design.md
create mode 100644 openspec/changes/add-vite-logger-support/proposal.md
create mode 100644 openspec/changes/add-vite-logger-support/specs/error-formatting/spec.md
create mode 100644 openspec/changes/add-vite-logger-support/specs/vite-plugin/spec.md
create mode 100644 openspec/changes/add-vite-logger-support/tasks.md
create mode 100644 packages/vite-plugin/src/logger-adapter.ts
diff --git a/apps/playgrounds/bun-react/bin/build.ts b/apps/playgrounds/bun-react/bin/build.ts
new file mode 100644
index 000000000..3b2e55ed6
--- /dev/null
+++ b/apps/playgrounds/bun-react/bin/build.ts
@@ -0,0 +1,18 @@
+import arkenv from "@arkenv/bun-plugin";
+
+await Bun.build({
+ entrypoints: ["./src/index.html"],
+ outdir: "./dist",
+ sourcemap: true,
+ target: "browser",
+ minify: true,
+ define: {
+ "process.env.NODE_ENV": "production",
+ },
+ plugins: [
+ arkenv({
+ BUN_PUBLIC_API_URL: "string",
+ BUN_PUBLIC_DEBUG: "boolean",
+ }),
+ ],
+});
diff --git a/apps/playgrounds/bun-react/bun-env.d.ts b/apps/playgrounds/bun-react/bun-env.d.ts
index 809de985d..7db01f321 100644
--- a/apps/playgrounds/bun-react/bun-env.d.ts
+++ b/apps/playgrounds/bun-react/bun-env.d.ts
@@ -1,4 +1,17 @@
-// Generated by `bun init`
+///
+
+// Import the Env schema type from env.ts
+// Note: In a real project, you might want to export Env from a separate env.ts file
+// and import it like: typeof import("./env").Env
+type ProcessEnvAugmented = import("@arkenv/bun-plugin").ProcessEnvAugmented<
+ typeof import("./bun-env").Env
+>;
+
+declare global {
+ namespace NodeJS {
+ interface ProcessEnv extends ProcessEnvAugmented {}
+ }
+}
declare module "*.svg" {
/**
diff --git a/apps/playgrounds/bun-react/bun-plugin-config.ts b/apps/playgrounds/bun-react/bun-plugin-config.ts
new file mode 100644
index 000000000..f8774d820
--- /dev/null
+++ b/apps/playgrounds/bun-react/bun-plugin-config.ts
@@ -0,0 +1,9 @@
+import arkenv from "@arkenv/bun-plugin";
+
+// Configure the ArkEnv Bun plugin with your environment variable schema
+// This file is referenced in bunfig.toml under [serve.static] plugins
+export default arkenv({
+ BUN_PUBLIC_API_URL: "string",
+ BUN_PUBLIC_DEBUG: "boolean",
+});
+
diff --git a/apps/playgrounds/bun-react/bunfig.toml b/apps/playgrounds/bun-react/bunfig.toml
index 9819bf6de..42a89901c 100644
--- a/apps/playgrounds/bun-react/bunfig.toml
+++ b/apps/playgrounds/bun-react/bunfig.toml
@@ -1,2 +1,3 @@
[serve.static]
-env = "BUN_PUBLIC_*"
\ No newline at end of file
+env = "BUN_PUBLIC_*"
+plugins = ["./bun-plugin-config.ts"]
\ No newline at end of file
diff --git a/apps/playgrounds/bun-react/package.json b/apps/playgrounds/bun-react/package.json
index d48f186ca..67db12d1c 100644
--- a/apps/playgrounds/bun-react/package.json
+++ b/apps/playgrounds/bun-react/package.json
@@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "bun --hot src/index.tsx",
- "build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='BUN_PUBLIC_*'",
+ "build": "bun bin/build.ts",
"start": "NODE_ENV=production bun src/index.tsx",
"clean": "rimraf dist node_modules"
},
diff --git a/apps/playgrounds/bun-react/src/App.tsx b/apps/playgrounds/bun-react/src/App.tsx
index 5c178f495..afb0e83dd 100644
--- a/apps/playgrounds/bun-react/src/App.tsx
+++ b/apps/playgrounds/bun-react/src/App.tsx
@@ -17,6 +17,10 @@ export function App() {
Edit src/App.tsx and save to test HMR
+
+ process.env.vars
+ {process.env.}
+
);
}
diff --git a/apps/playgrounds/bun-react/src/index.tsx b/apps/playgrounds/bun-react/src/index.tsx
index bd8df61e4..fa002c3b4 100644
--- a/apps/playgrounds/bun-react/src/index.tsx
+++ b/apps/playgrounds/bun-react/src/index.tsx
@@ -1,19 +1,31 @@
import { serve } from "bun";
+import arkenv from "@arkenv/bun-plugin";
import index from "./index.html";
+// Validate environment variables at server startup
+// This ensures the server won't start if required env vars are missing
+arkenv({
+ BUN_PUBLIC_API_URL: "string",
+ BUN_PUBLIC_DEBUG: "boolean",
+});
+
+// Note: Plugins are also configured in bunfig.toml under [serve.static]
+// for bundling client code. The validation above ensures env vars are present
+// before the server starts.
+
const server = serve({
routes: {
// Serve index.html for all unmatched routes.
"/*": index,
"/api/hello": {
- async GET(req) {
+ async GET() {
return Response.json({
message: "Hello, world!",
method: "GET",
});
},
- async PUT(req) {
+ async PUT() {
return Response.json({
message: "Hello, world!",
method: "PUT",
diff --git a/openspec/changes/add-vite-logger-support/design.md b/openspec/changes/add-vite-logger-support/design.md
new file mode 100644
index 000000000..40d53b692
--- /dev/null
+++ b/openspec/changes/add-vite-logger-support/design.md
@@ -0,0 +1,130 @@
+# Design: Pluggable Logger Support
+
+## Context
+
+The Vite plugin currently uses ArkEnv's `ArkEnvError` which formats errors using ANSI color codes via `styleText`. Vite provides a built-in logger (using picocolors internally) that should be used for better integration with Vite's build output. The goal is to make the implementation minimal on the Vite plugin side while supporting pluggable loggers in the core library for future extensibility.
+
+## Goals / Non-Goals
+
+### Goals
+- Support Vite's logger API for error formatting in the Vite plugin
+- Maintain backward compatibility (logger is optional, defaults to current ANSI behavior)
+- Keep Vite plugin implementation minimal (pass logger from Vite to core library)
+- Design extensible logger interface for future logger support (e.g., other build tools)
+- Zero new external dependencies
+
+### Non-Goals
+- Supporting all possible logger APIs (focus on Vite's logger first)
+- Changing the error message format (only the styling mechanism changes)
+- Adding logger support to browser environments (logger only applies in Node/build environments)
+
+## Decisions
+
+### Decision: Logger Abstraction Interface
+
+**What**: Define a simple logger interface that accepts a color name and text, returning styled text.
+
+**Why**: This abstraction allows plugging in different loggers (Vite's logger, future loggers) while keeping the core library decoupled from specific logger implementations.
+
+**Alternatives considered**:
+1. **Direct picocolors dependency**: Would add an external dependency and doesn't align with zero-dependency goal
+2. **Pass full logger object**: Too complex, we only need the styling functionality
+3. **No abstraction**: Would require custom code in Vite plugin, violating minimal implementation goal
+
+**Implementation**:
+```typescript
+type LoggerStyle = (color: "red" | "yellow" | "cyan", text: string) => string;
+```
+
+### Decision: Optional Logger Parameter
+
+**What**: Make logger parameter optional throughout the codebase, defaulting to current `styleText` behavior.
+
+**Why**: Maintains backward compatibility and allows gradual adoption. Existing code continues to work without changes.
+
+**Alternatives considered**:
+1. **Required logger**: Would be a breaking change
+2. **Global logger configuration**: Adds complexity and global state
+
+**Implementation**: Logger is optional at all levels:
+- `styleText(color, text, logger?)`
+- `formatErrors(errors, logger?)`
+- `ArkEnvError(errors, message?, logger?)`
+- `createEnv(schema, env, logger?)`
+
+### Decision: Vite Logger Adapter
+
+**What**: Create a thin adapter function that converts Vite's logger API to our logger interface.
+
+**Why**: Vite's logger has methods like `logger.error()`, `logger.warn()`, `logger.info()` that accept strings. We need to extract the styling functionality (picocolors) from these methods. The adapter provides a clean interface conversion.
+
+**Alternatives considered**:
+1. **Direct picocolors usage**: Would require adding picocolors as a dependency
+2. **String manipulation**: Too fragile, doesn't leverage Vite's logger properly
+3. **Custom logger implementation**: Defeats the purpose of using Vite's logger
+
+**Implementation**: Adapter function in vite-plugin package that wraps Vite's logger:
+```typescript
+function createViteLoggerAdapter(logger: Logger): LoggerStyle {
+ // Extract picocolors from Vite's logger
+ // Return function matching LoggerStyle interface
+}
+```
+
+### Decision: Error Formatting Flow
+
+**What**: When validation fails in Vite plugin, catch the error, extract errors, and use Vite's logger to format and display them before failing the build.
+
+**Why**: Follows Vite's best practices by using Vite's logger for build output. The error is still thrown to fail the build, but formatting uses Vite's logger.
+
+**Alternatives considered**:
+1. **Prevent error throwing**: Would require significant refactoring
+2. **Custom error handling**: Doesn't use Vite's logger, violates minimal implementation goal
+
+**Implementation**:
+- Vite plugin catches `ArkEnvError` or validation errors
+- Extracts error information
+- Uses Vite's logger to format and display errors
+- Calls `this.error()` to fail the build with Rollup-style error
+
+## Risks / Trade-offs
+
+### Risk: Logger API Changes
+**Mitigation**: The logger interface is simple and stable. Vite's logger API is unlikely to change significantly.
+
+### Risk: Performance Impact
+**Mitigation**: Logger is only used during error cases (validation failures), which are rare. No performance impact on successful builds.
+
+### Trade-off: Abstraction Overhead
+**Trade-off**: Adding abstraction layer adds some complexity, but enables future extensibility and keeps Vite plugin minimal.
+
+**Mitigation**: The abstraction is simple (single function type) and well-documented.
+
+## Migration Plan
+
+1. **Phase 1**: Add logger support to core library (backward compatible)
+ - Extend `styleText` to accept optional logger
+ - Extend `formatErrors` and `ArkEnvError` to accept optional logger
+ - Extend `createEnv` to accept optional logger
+ - All changes are optional, defaults to current behavior
+
+2. **Phase 2**: Implement Vite logger adapter
+ - Create adapter function in vite-plugin package
+ - Adapter converts Vite's logger to our logger interface
+
+3. **Phase 3**: Update Vite plugin to use logger
+ - Catch validation errors
+ - Use Vite's logger (via adapter) to format errors
+ - Display formatted errors using Vite's logger methods
+ - Fail build with `this.error()`
+
+4. **Testing**:
+ - Unit tests for logger adapter
+ - Integration tests for Vite plugin with logger
+ - Verify backward compatibility (no logger passed)
+
+## Open Questions
+
+- How to extract picocolors from Vite's logger? (May need to inspect Vite's logger implementation)
+- Should we support other logger APIs in the future? (Yes, but out of scope for this change)
+
diff --git a/openspec/changes/add-vite-logger-support/proposal.md b/openspec/changes/add-vite-logger-support/proposal.md
new file mode 100644
index 000000000..88f91ab32
--- /dev/null
+++ b/openspec/changes/add-vite-logger-support/proposal.md
@@ -0,0 +1,31 @@
+# Change: Add Vite Logger Support for Pretty Printing
+
+## Why
+
+Currently, the Vite plugin uses ArkEnv's error formatting which relies on ANSI color codes via the `styleText` utility. Issue #206 requests using Vite's built-in logger (which uses picocolors internally) for better integration with Vite's build output and to follow Vite's best practices.
+
+The implementation should be minimal on the Vite plugin side and adhere to Vite's best practices. We should strive to make the plugin work with a standard API logger, and "bring that logger" from the Vite plugin to the core arkenv library, rather than doing a lot of custom code on the Vite plugin side. In essence, our current `styleText` util should be grown to support Vite's logger (based on picocolors internally) and others in the future.
+
+## What Changes
+
+- **ADDED**: Logger abstraction interface to support pluggable loggers (Vite's logger, and others in the future)
+- **MODIFIED**: `styleText` utility to accept an optional logger function for styling text
+- **MODIFIED**: `formatErrors` function to accept an optional logger for error formatting
+- **MODIFIED**: `ArkEnvError` class to accept an optional logger for error message formatting
+- **MODIFIED**: `createEnv` function to accept an optional logger parameter
+- **MODIFIED**: Vite plugin to use Vite's logger for error formatting when validation fails
+- **ADDED**: Logger adapter utilities to convert Vite's logger API to the standard logger interface
+
+## Impact
+
+- **Affected specs**:
+ - `error-formatting` (new capability)
+ - `vite-plugin` (modified to use logger)
+- **Affected code**:
+ - `packages/arkenv/src/utils/style-text.ts` - Extended to support logger
+ - `packages/arkenv/src/errors.ts` - Modified to accept logger
+ - `packages/arkenv/src/create-env.ts` - Modified to accept and pass logger
+ - `packages/vite-plugin/src/index.ts` - Modified to use Vite's logger
+- **Breaking changes**: None (logger parameter is optional, defaults to current behavior)
+- **Dependencies**: No new external dependencies (uses Vite's logger when available)
+
diff --git a/openspec/changes/add-vite-logger-support/specs/error-formatting/spec.md b/openspec/changes/add-vite-logger-support/specs/error-formatting/spec.md
new file mode 100644
index 000000000..d82761373
--- /dev/null
+++ b/openspec/changes/add-vite-logger-support/specs/error-formatting/spec.md
@@ -0,0 +1,34 @@
+## ADDED Requirements
+
+### Requirement: Pluggable Logger Support for Error Formatting
+
+The error formatting system SHALL support pluggable loggers for styling error messages. When a logger is provided, it SHALL be used for styling text instead of the default ANSI color codes. When no logger is provided, the system SHALL default to the current ANSI color code behavior for backward compatibility.
+
+#### Scenario: Error formatting with logger
+- **WHEN** a logger function is provided to `formatErrors` or `ArkEnvError`
+- **AND** the logger function accepts a color name and text string
+- **THEN** the logger function is used to style error messages
+- **AND** the formatted errors use the logger's styling instead of ANSI codes
+
+#### Scenario: Error formatting without logger (backward compatibility)
+- **WHEN** no logger is provided to `formatErrors` or `ArkEnvError`
+- **THEN** the system uses the default ANSI color code behavior
+- **AND** error messages are formatted with ANSI codes as before
+- **AND** existing code continues to work without changes
+
+#### Scenario: Logger passed through createEnv
+- **WHEN** a logger is provided to `createEnv`
+- **AND** validation fails
+- **THEN** the logger is passed to `ArkEnvError` for error formatting
+- **AND** error messages use the logger's styling
+
+### Requirement: Logger Interface
+
+The logger interface SHALL be a simple function type that accepts a color name and text string, returning styled text. The logger SHALL support the colors "red", "yellow", and "cyan" to match the current `styleText` utility.
+
+#### Scenario: Logger function signature
+- **WHEN** a logger function is provided
+- **THEN** it accepts parameters `(color: "red" | "yellow" | "cyan", text: string)`
+- **AND** it returns a string with styled text
+- **AND** the function can be used interchangeably with the default `styleText` behavior
+
diff --git a/openspec/changes/add-vite-logger-support/specs/vite-plugin/spec.md b/openspec/changes/add-vite-logger-support/specs/vite-plugin/spec.md
new file mode 100644
index 000000000..3818de5df
--- /dev/null
+++ b/openspec/changes/add-vite-logger-support/specs/vite-plugin/spec.md
@@ -0,0 +1,36 @@
+## MODIFIED Requirements
+
+### Requirement: Vite Plugin Environment Variable Validation and Exposure
+
+The Vite plugin SHALL validate environment variables at build-time and expose them to client code through Vite's `define` option. The plugin SHALL only expose environment variables that match Vite's configured prefix (defaults to `VITE_`), filtering out server-only variables automatically. When validation fails, the plugin SHALL use Vite's logger to format and display error messages, following Vite's best practices for build output.
+
+#### Scenario: Plugin validates and exposes client-safe variables
+- **WHEN** a user configures the Vite plugin with a schema containing environment variables
+- **AND** the schema includes variables with the Vite prefix (e.g., `VITE_API_URL`, `VITE_DEBUG`)
+- **THEN** the plugin validates all variables in the schema at build-time
+- **AND** the plugin filters the validated results to only include variables matching the configured prefix
+- **AND** filtered variables are exposed to client code via `import.meta.env.*`
+- **AND** server-only variables without the prefix are filtered out and not exposed
+
+#### Scenario: Plugin respects Vite prefix configuration
+- **WHEN** a user configures `envPrefix` in their Vite config
+- **AND** they pass a schema to the Vite plugin
+- **THEN** the plugin uses the configured prefix to filter variables after validation
+- **AND** only variables matching the configured prefix are validated and exposed
+- **AND** the filtering happens automatically without requiring schema changes
+
+#### Scenario: Plugin uses Vite logger for error formatting
+- **WHEN** environment variable validation fails in the Vite plugin
+- **THEN** the plugin uses Vite's logger to format and display error messages
+- **AND** error messages are styled using Vite's logger (which uses picocolors internally)
+- **AND** the plugin displays formatted errors using Vite's logger methods (e.g., `logger.error()`)
+- **AND** the build fails with a Rollup-style error using `this.error()`
+- **AND** the error output integrates seamlessly with Vite's build output
+
+#### Scenario: Plugin passes Vite logger to core library
+- **WHEN** the Vite plugin needs to validate environment variables
+- **THEN** it extracts Vite's logger from the resolved config
+- **AND** it creates a logger adapter that converts Vite's logger API to the standard logger interface
+- **AND** it passes the logger adapter to `createEnv` for error formatting
+- **AND** the implementation is minimal (no custom error formatting code in the plugin)
+
diff --git a/openspec/changes/add-vite-logger-support/tasks.md b/openspec/changes/add-vite-logger-support/tasks.md
new file mode 100644
index 000000000..e603930ee
--- /dev/null
+++ b/openspec/changes/add-vite-logger-support/tasks.md
@@ -0,0 +1,58 @@
+## 1. Core Library Changes
+
+- [ ] 1.1 Extend `styleText` utility to accept optional logger parameter
+ - Add `LoggerStyle` type definition
+ - Make logger parameter optional
+ - Use logger if provided, otherwise use current ANSI behavior
+ - Update tests to cover logger parameter
+
+- [ ] 1.2 Extend `formatErrors` function to accept optional logger
+ - Add optional logger parameter
+ - Pass logger to `styleText` calls
+ - Update tests to verify logger usage
+
+- [ ] 1.3 Extend `ArkEnvError` class to accept optional logger
+ - Add optional logger parameter to constructor
+ - Pass logger to `formatErrors` and `styleText` calls
+ - Update tests to verify logger usage
+
+- [ ] 1.4 Extend `createEnv` function to accept optional logger
+ - Add optional logger parameter
+ - Pass logger to `ArkEnvError` when validation fails
+ - Update tests to verify logger propagation
+
+## 2. Vite Plugin Changes
+
+- [ ] 2.1 Create Vite logger adapter utility
+ - Create adapter function that converts Vite's logger to `LoggerStyle` interface
+ - Extract picocolors functionality from Vite's logger
+ - Add tests for adapter function
+
+- [ ] 2.2 Update Vite plugin to use logger
+ - Access Vite's logger from resolved config in `configResolved` hook
+ - Create logger adapter from Vite's logger
+ - Pass logger adapter to `createEnv` call
+ - Handle validation errors and use Vite's logger for formatting
+ - Display errors using Vite's logger methods
+ - Fail build with `this.error()` after displaying formatted errors
+
+- [ ] 2.3 Add integration tests for Vite plugin with logger
+ - Test error formatting with Vite's logger
+ - Verify error output format
+ - Test build failure behavior
+
+## 3. Validation
+
+- [ ] 3.1 Verify backward compatibility
+ - Run existing tests to ensure no regressions
+ - Verify default behavior when no logger is provided
+
+- [ ] 3.2 Test Vite plugin error output
+ - Create test fixture with invalid environment variables
+ - Verify error messages are formatted using Vite's logger
+ - Verify error output matches Vite's build output style
+
+- [ ] 3.3 Validate OpenSpec proposal
+ - Run `openspec validate add-vite-logger-support --strict`
+ - Fix any validation issues
+
diff --git a/packages/bun-plugin/src/index.ts b/packages/bun-plugin/src/index.ts
index 97961e3eb..bbcc6d685 100644
--- a/packages/bun-plugin/src/index.ts
+++ b/packages/bun-plugin/src/index.ts
@@ -1,7 +1,7 @@
import type { EnvSchema } from "arkenv";
import { createEnv } from "arkenv";
import type { type } from "arktype";
-import { type BunPlugin, plugin } from "bun";
+import type { BunPlugin } from "bun";
export type { ProcessEnvAugmented } from "./types";
@@ -78,63 +78,61 @@ export default function arkenv>(
envMap.set(key, JSON.stringify(value));
}
- return plugin({
- name: "@arkenv/bun-plugin",
- setup(build) {
- build.onLoad({ filter: /.*/ }, async (args) => {
- // Only process JavaScript/TypeScript files
- // Skip node_modules and other non-source files
- if (
- !args.path.match(/\.(js|jsx|ts|tsx|mjs|cjs)$/) ||
- args.path.includes("node_modules")
- ) {
- return undefined;
- }
+ return {
+ name: "@arkenv/bun-plugin",
+ setup(build) {
+ // Only process JavaScript/TypeScript source files
+ build.onLoad({ filter: /\.(js|jsx|ts|tsx|mjs|cjs)$/ }, (args) => {
+ // Skip node_modules and other non-source files
+ if (args.path.includes("node_modules")) {
+ return undefined;
+ }
- try {
- // Read the file contents
- const file = Bun.file(args.path);
- const contents = await file.text();
+ try {
+ // Read the file contents synchronously
+ const file = Bun.file(args.path);
+ const arrayBuffer = file.arrayBufferSync();
+ const contents = new TextDecoder().decode(arrayBuffer);
- // Replace process.env.VARIABLE patterns with validated values
- let transformed = contents;
+ // Replace process.env.VARIABLE patterns with validated values
+ let transformed = contents;
- // Pattern 1: process.env.VARIABLE
- // Pattern 2: process.env["VARIABLE"]
- // Pattern 3: process.env['VARIABLE']
- for (const [key, value] of envMap.entries()) {
- // Replace process.env.KEY (word boundary to avoid partial matches)
- const dotPattern = new RegExp(
- `process\\.env\\.${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
- "g",
- );
- transformed = transformed.replace(dotPattern, value);
+ // Pattern 1: process.env.VARIABLE
+ // Pattern 2: process.env["VARIABLE"]
+ // Pattern 3: process.env['VARIABLE']
+ for (const [key, value] of envMap.entries()) {
+ // Replace process.env.KEY (word boundary to avoid partial matches)
+ const dotPattern = new RegExp(
+ `process\\.env\\.${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
+ "g",
+ );
+ transformed = transformed.replace(dotPattern, value);
- // Replace process.env["KEY"] and process.env['KEY']
- const bracketPattern = new RegExp(
- `process\\.env\\[(["'])${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\1\\]`,
- "g",
- );
- transformed = transformed.replace(bracketPattern, value);
- }
+ // Replace process.env["KEY"] and process.env['KEY']
+ const bracketPattern = new RegExp(
+ `process\\.env\\[(["'])${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\1\\]`,
+ "g",
+ );
+ transformed = transformed.replace(bracketPattern, value);
+ }
- // Determine loader based on file extension
- const loader =
- args.path.endsWith(".tsx") || args.path.endsWith(".jsx")
- ? "tsx"
- : args.path.endsWith(".ts") || args.path.endsWith(".mts")
- ? "ts"
- : "js";
+ // Determine loader based on file extension
+ const loader =
+ args.path.endsWith(".tsx") || args.path.endsWith(".jsx")
+ ? "tsx"
+ : args.path.endsWith(".ts") || args.path.endsWith(".mts")
+ ? "ts"
+ : "js";
- return {
- loader,
- contents: transformed,
- };
- } catch (error) {
- // If file can't be read, return undefined to let Bun handle it
- return undefined;
- }
- });
- },
- });
+ return {
+ loader,
+ contents: transformed,
+ };
+ } catch (error) {
+ // If file can't be read, return undefined to let Bun handle it
+ return undefined;
+ }
+ });
+ },
+ } satisfies BunPlugin;
}
diff --git a/packages/vite-plugin/src/logger-adapter.ts b/packages/vite-plugin/src/logger-adapter.ts
new file mode 100644
index 000000000..a258995e9
--- /dev/null
+++ b/packages/vite-plugin/src/logger-adapter.ts
@@ -0,0 +1,59 @@
+import type { LoggerStyle } from "arkenv";
+import type { Logger } from "vite";
+
+/**
+ * Create a logger adapter that converts Vite's logger to ArkEnv's LoggerStyle interface
+ * Vite's logger uses picocolors internally. We try to access it through the logger instance.
+ * @param logger - Vite's logger instance
+ * @returns A LoggerStyle function that can be used with ArkEnv's error formatting
+ */
+export function createViteLoggerAdapter(logger: Logger): LoggerStyle {
+ // Try to access picocolors through Vite's logger internal structure
+ // Vite's logger may have colors exposed or we can access them through the logger instance
+ const loggerWithColors = logger as {
+ colors?: {
+ red?: (text: string) => string;
+ yellow?: (text: string) => string;
+ cyan?: (text: string) => string;
+ };
+ };
+
+ // If colors are directly available, use them
+ if (loggerWithColors.colors) {
+ const colors = loggerWithColors.colors;
+ return (color: "red" | "yellow" | "cyan", text: string): string => {
+ const colorFn = colors[color];
+ if (colorFn) {
+ return colorFn(text);
+ }
+ return text;
+ };
+ }
+
+ // Try to access picocolors through require (if available in Vite's context)
+ // This is a fallback that works if picocolors is available in the module resolution
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ const picocolors = require("picocolors");
+ if (picocolors) {
+ return (color: "red" | "yellow" | "cyan", text: string): string => {
+ switch (color) {
+ case "red":
+ return picocolors.red(text);
+ case "yellow":
+ return picocolors.yellow(text);
+ case "cyan":
+ return picocolors.cyan(text);
+ default:
+ return text;
+ }
+ };
+ }
+ } catch {
+ // picocolors not available, fall through to plain text
+ }
+
+ // Final fallback: return plain text
+ // This maintains functionality even if colors can't be accessed
+ return (_color: "red" | "yellow" | "cyan", text: string): string => text;
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8dff67e80..3919f0f0f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -374,8 +374,8 @@ importers:
specifier: 2.1.27
version: 2.1.27
bun:
- specifier: '>=1.0.0'
- version: 1.3.3
+ specifier: 1.1.0
+ version: 1.1.0
size-limit:
specifier: 11.2.0
version: 11.2.0
@@ -1794,58 +1794,43 @@ packages:
resolution: {integrity: sha512-scSmQBD8eANlMUOglxHrN1JdSW8tDghsPuS83otqealBiIeMukCQMOf/wc0JJjDXomqwNdEQFLXLGHrU6PGxuA==}
engines: {node: '>= 20.0.0'}
- '@oven/bun-darwin-aarch64@1.3.3':
- resolution: {integrity: sha512-eJopQrUk0WR7jViYDC29+Rp50xGvs4GtWOXBeqCoFMzutkkO3CZvHehA4JqnjfWMTSS8toqvRhCSOpOz62Wf9w==}
+ '@oven/bun-darwin-aarch64@1.1.0':
+ resolution: {integrity: sha512-8hDDBdQVHfFJ1KHXeHIB78SFs7rYG1Ff4n8SDv006ENc3uEIqynFVbHCKbqa5aE7zLkdBizoBzL50W76zAPmCQ==}
cpu: [arm64]
os: [darwin]
- '@oven/bun-darwin-x64-baseline@1.3.3':
- resolution: {integrity: sha512-1ij4wQ9ECLFf1XFry+IFUN+28if40ozDqq6+QtuyOhIwraKzXOlAUbILhRMGvM3ED3yBex2mTwlKpA4Vja/V2g==}
+ '@oven/bun-darwin-x64-baseline@1.1.0':
+ resolution: {integrity: sha512-61rkVhpzU3PfoXRLrIpgKzLys4Qk9bvCI5DVM5cZgVxGq62jR+FwldCVcCoyfMdblGg07x3aUY9BXMwtSxvN0A==}
cpu: [x64]
os: [darwin]
- '@oven/bun-darwin-x64@1.3.3':
- resolution: {integrity: sha512-xGDePueVFrNgkS+iN0QdEFeRrx2MQ5hQ9ipRFu7N73rgoSSJsFlOKKt2uGZzunczedViIfjYl0ii0K4E9aZ0Ow==}
+ '@oven/bun-darwin-x64@1.1.0':
+ resolution: {integrity: sha512-KNEtlnEok6LB9ZNMe2aSxRVO8ps22hn4qTTjJc/s0josPNlIk4PvV2xusbUEGQS4XSNGcEH/Gh7VYE/W3u1gjg==}
cpu: [x64]
os: [darwin]
- '@oven/bun-linux-aarch64-musl@1.3.3':
- resolution: {integrity: sha512-XWQ3tV/gtZj0wn2AdSUq/tEOKWT4OY+Uww70EbODgrrq00jxuTfq5nnYP6rkLD0M/T5BHJdQRSfQYdIni9vldw==}
+ '@oven/bun-linux-aarch64@1.1.0':
+ resolution: {integrity: sha512-UZhZU2mkC+OnbDB/Lq8MTJWiwhDMrnZvcW99cc+fymU5w/bOvpSuFOhpC/IeI1Dp8WGlkjDGyottSr++juWXOw==}
cpu: [arm64]
os: [linux]
- '@oven/bun-linux-aarch64@1.3.3':
- resolution: {integrity: sha512-DabZ3Mt1XcJneWdEEug8l7bCPVvDBRBpjUIpNnRnMFWFnzr8KBEpMcaWTwYOghjXyJdhB4MPKb19MwqyQ+FHAw==}
- cpu: [arm64]
- os: [linux]
-
- '@oven/bun-linux-x64-baseline@1.3.3':
- resolution: {integrity: sha512-IU8pxhIf845psOv55LqJyL+tSUc6HHMfs6FGhuJcAnyi92j+B1HjOhnFQh9MW4vjoo7do5F8AerXlvk59RGH2w==}
- cpu: [x64]
- os: [linux]
-
- '@oven/bun-linux-x64-musl-baseline@1.3.3':
- resolution: {integrity: sha512-JoRTPdAXRkNYouUlJqEncMWUKn/3DiWP03A7weBbtbsKr787gcdNna2YeyQKCb1lIXE4v1k18RM3gaOpQobGIQ==}
+ '@oven/bun-linux-x64-baseline@1.1.0':
+ resolution: {integrity: sha512-eAxK+x5dEzoVGR0f4FiiLYgtzHu8xB8uWB+Uc9h/6C46vODlOPsBT2Pwv6y5uPAX+dyKGHeMQsP+r2OFBkbkTA==}
cpu: [x64]
os: [linux]
- '@oven/bun-linux-x64-musl@1.3.3':
- resolution: {integrity: sha512-xNSDRPn1yyObKteS8fyQogwsS4eCECswHHgaKM+/d4wy/omZQrXn8ZyGm/ZF9B73UfQytUfbhE7nEnrFq03f0w==}
+ '@oven/bun-linux-x64@1.1.0':
+ resolution: {integrity: sha512-o2D9LGXyJqHTQ5tTF4GSctL20ZFw0EGwrhhCLPM4cGzHrhji3EP9euz28aREM+iYUqAWC6BOGz77up5yeMbBQw==}
cpu: [x64]
os: [linux]
- '@oven/bun-linux-x64@1.3.3':
- resolution: {integrity: sha512-7eIARtKZKZDtah1aCpQUj/1/zT/zHRR063J6oAxZP9AuA547j5B9OM2D/vi/F4En7Gjk9FPjgPGTSYeqpQDzJw==}
- cpu: [x64]
- os: [linux]
-
- '@oven/bun-windows-x64-baseline@1.3.3':
- resolution: {integrity: sha512-u5eZHKq6TPJSE282KyBOicGQ2trkFml0RoUfqkPOJVo7TXGrsGYYzdsugZRnVQY/WEmnxGtBy4T3PAaPqgQViA==}
+ '@oven/bun-windows-x64-baseline@1.1.0':
+ resolution: {integrity: sha512-PqQToENBnHkzdcUnkCYI86397oNvs8Z7PZT8iyQEMSr/oYTMNCSArgrpFSEG6seN5HBPkSyhbj0fuU3U5oOhtQ==}
cpu: [x64]
os: [win32]
- '@oven/bun-windows-x64@1.3.3':
- resolution: {integrity: sha512-kWqa1LKvDdAIzyfHxo3zGz3HFWbFHDlrNK77hKjUN42ycikvZJ+SHSX76+1OW4G8wmLETX4Jj+4BM1y01DQRIQ==}
+ '@oven/bun-windows-x64@1.1.0':
+ resolution: {integrity: sha512-SStu+4Zq0S/nfwaSz9QcBnk9CYjlSckzkzChoFt+vdIXfiaCT+uq0I57OXifRb67uOVtXl6N3PqmCivJPTh/kw==}
cpu: [x64]
os: [win32]
@@ -3930,8 +3915,8 @@ packages:
peerDependencies:
'@types/react': ^19
- bun@1.3.3:
- resolution: {integrity: sha512-2hJ4ocTZ634/Ptph4lysvO+LbbRZq8fzRvMwX0/CqaLBxrF2UB5D1LdMB8qGcdtCer4/VR9Bx5ORub0yn+yzmw==}
+ bun@1.1.0:
+ resolution: {integrity: sha512-vEfq5NXOWriLzVtA/gMC69MR/W48KE0G7SarZUbu7H89KH9u7FFozTvn8noyLKqpI1sRa2qu/laE21K+htFxCQ==}
cpu: [arm64, x64]
os: [darwin, linux, win32]
hasBin: true
@@ -9287,37 +9272,28 @@ snapshots:
'@orama/orama@3.1.16': {}
- '@oven/bun-darwin-aarch64@1.3.3':
- optional: true
-
- '@oven/bun-darwin-x64-baseline@1.3.3':
- optional: true
-
- '@oven/bun-darwin-x64@1.3.3':
- optional: true
-
- '@oven/bun-linux-aarch64-musl@1.3.3':
+ '@oven/bun-darwin-aarch64@1.1.0':
optional: true
- '@oven/bun-linux-aarch64@1.3.3':
+ '@oven/bun-darwin-x64-baseline@1.1.0':
optional: true
- '@oven/bun-linux-x64-baseline@1.3.3':
+ '@oven/bun-darwin-x64@1.1.0':
optional: true
- '@oven/bun-linux-x64-musl-baseline@1.3.3':
+ '@oven/bun-linux-aarch64@1.1.0':
optional: true
- '@oven/bun-linux-x64-musl@1.3.3':
+ '@oven/bun-linux-x64-baseline@1.1.0':
optional: true
- '@oven/bun-linux-x64@1.3.3':
+ '@oven/bun-linux-x64@1.1.0':
optional: true
- '@oven/bun-windows-x64-baseline@1.3.3':
+ '@oven/bun-windows-x64-baseline@1.1.0':
optional: true
- '@oven/bun-windows-x64@1.3.3':
+ '@oven/bun-windows-x64@1.1.0':
optional: true
'@oxc-project/runtime@0.96.0': {}
@@ -11586,19 +11562,16 @@ snapshots:
'@types/node': 24.10.1
'@types/react': 19.2.6
- bun@1.3.3:
+ bun@1.1.0:
optionalDependencies:
- '@oven/bun-darwin-aarch64': 1.3.3
- '@oven/bun-darwin-x64': 1.3.3
- '@oven/bun-darwin-x64-baseline': 1.3.3
- '@oven/bun-linux-aarch64': 1.3.3
- '@oven/bun-linux-aarch64-musl': 1.3.3
- '@oven/bun-linux-x64': 1.3.3
- '@oven/bun-linux-x64-baseline': 1.3.3
- '@oven/bun-linux-x64-musl': 1.3.3
- '@oven/bun-linux-x64-musl-baseline': 1.3.3
- '@oven/bun-windows-x64': 1.3.3
- '@oven/bun-windows-x64-baseline': 1.3.3
+ '@oven/bun-darwin-aarch64': 1.1.0
+ '@oven/bun-darwin-x64': 1.1.0
+ '@oven/bun-darwin-x64-baseline': 1.1.0
+ '@oven/bun-linux-aarch64': 1.1.0
+ '@oven/bun-linux-x64': 1.1.0
+ '@oven/bun-linux-x64-baseline': 1.1.0
+ '@oven/bun-windows-x64': 1.1.0
+ '@oven/bun-windows-x64-baseline': 1.1.0
bundle-name@4.1.0:
dependencies:
From c43242587f410a8a3ed4cf88215febc1a1cbad10 Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Tue, 25 Nov 2025 23:50:17 +0500
Subject: [PATCH 10/48] Remove unnecessary blank line in bun-plugin-config.ts
for cleaner code.
---
apps/playgrounds/bun-react/bun-plugin-config.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/apps/playgrounds/bun-react/bun-plugin-config.ts b/apps/playgrounds/bun-react/bun-plugin-config.ts
index f8774d820..5c44ba893 100644
--- a/apps/playgrounds/bun-react/bun-plugin-config.ts
+++ b/apps/playgrounds/bun-react/bun-plugin-config.ts
@@ -6,4 +6,3 @@ export default arkenv({
BUN_PUBLIC_API_URL: "string",
BUN_PUBLIC_DEBUG: "boolean",
});
-
From 17c64563873696d9e1fb0c091dd38bbb7453db66 Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Wed, 26 Nov 2025 23:58:08 +0500
Subject: [PATCH 11/48] Add Bun Plugin for ArkEnv with Configuration Patterns
- Introduced two primary configuration patterns for the Bun plugin: Direct Reference for `Bun.build()` and Package Reference for `Bun.serve()`.
- Implemented schema discovery for environment variable validation and transformation during the bundling phase.
- Enhanced error handling to provide descriptive messages when schema files are not found.
- Updated documentation to reflect new usage patterns and scenarios for the plugin.
---
openspec/changes/add-bun-plugin/design.md | 208 +++++++++---------
openspec/changes/add-bun-plugin/proposal.md | 159 +++++++++++--
.../add-bun-plugin/specs/bun-plugin/spec.md | 37 +++-
openspec/changes/add-bun-plugin/tasks.md | 203 ++++++++++++-----
4 files changed, 430 insertions(+), 177 deletions(-)
diff --git a/openspec/changes/add-bun-plugin/design.md b/openspec/changes/add-bun-plugin/design.md
index 90b65f633..8093da630 100644
--- a/openspec/changes/add-bun-plugin/design.md
+++ b/openspec/changes/add-bun-plugin/design.md
@@ -1,138 +1,144 @@
-# Design: Bun Plugin for ArkEnv
+
+### Requirement: Bun Plugin Configuration Patterns
-## Context
+The Bun plugin SHALL support two primary configuration patterns depending on the usage context:
-Bun provides a universal plugin API that can be used to extend both the runtime and bundler. For ArkEnv, we need to intercept environment variable access during Bun's bundling phase to:
+1. **Direct Reference (Bun.build)**: The plugin SHALL be configurable by passing a configured plugin instance directly in the `plugins` array when using `Bun.build()`.
+2. **Package Reference (Bun.serve)**: The plugin SHALL be configurable via a package name in `bunfig.toml` when using `Bun.serve()` for full-stack applications, with convention-based schema discovery.
-1. Validate environment variables using ArkEnv's schema
-2. Transform values (string to boolean, apply defaults, etc.)
-3. Filter to only expose prefixed variables (defaults to `BUN_PUBLIC_*`) to client code
-4. Statically replace `process.env.VARIABLE` with validated values
-5. Provide TypeScript type augmentation
+A future, more advanced configuration pattern using a custom static file MAY be supported, but is not required for the initial version.
-This is similar to how the Vite plugin works, but uses Bun's plugin API instead of Vite's `config` hook.
+#### Scenario: Plugin configuration with Bun.build
-## Goals
+- **WHEN** a user wants to build an application using `Bun.build()`
+- **AND** they configure the plugin with an environment variable schema
+- **THEN** they can pass a configured plugin instance directly in the `plugins` array
+- **AND** the plugin validates and transforms environment variables during the build process
-- Provide build-time environment variable validation for Bun applications
-- Support full-stack React apps using Bun's `serve` function
-- Filter environment variables based on Bun's prefix (defaults to `BUN_PUBLIC_*`)
-- Provide type-safe access to environment variables via type augmentation
-- Match the developer experience of the Vite plugin
+#### Scenario: Plugin configuration with Bun.serve via package reference
-## Non-Goals
+- **WHEN** a user wants to use `Bun.serve()` for a full-stack application
+- **AND** they configure `bunfig.toml` with:
+ - a `[serve.static]` section
+ - a `plugins` array that includes the package name `bun-plugin-arkenv`
+- **AND** their project contains an ArkEnv schema file in one of the supported default locations (for example, `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`)
+- **AND** that schema file exports a schema using `defineEnv` (via a default export or an `env` named export)
+- **THEN** the plugin SHALL locate the schema file via this convention-based search
+- **AND** it SHALL load the schema at startup
+- **AND** it SHALL use that schema to validate and transform environment variables during the bundling phase
-- Runtime environment variable validation (handled by core `arkenv` package)
-- Support for Bun's native plugin API (onBeforeParse) - JavaScript plugin is sufficient
-- Custom prefix configuration in the plugin (uses Bun's default `BUN_PUBLIC_*` or bunfig.toml configuration)
+#### Scenario: Bun.serve configuration fails when no schema file is found
-## Decisions
+- **WHEN** a user configures `bunfig.toml` with `[serve.static].plugins = ["bun-plugin-arkenv"]`
+- **AND** there is no schema file in any of the supported default locations
+- **THEN** the plugin SHALL fail fast with a clear, descriptive error message
+- **AND** the error message SHALL list the paths that were checked
+- **AND** the error message SHALL show an example of a minimal `env` schema file the user can create
+
-### Decision: Use `onLoad` Hook for Environment Variable Transformation
+
+### Decision: Configuration Modes and Schema Discovery
-**What**: Use Bun's `onLoad` plugin hook to intercept module loading and transform `process.env` access.
+**What**: The plugin supports two core configuration modes:
-**Why**:
-- `onLoad` allows us to transform module contents before parsing
-- We can replace `process.env.VARIABLE` with validated, transformed values
-- This matches the static replacement behavior needed for Bun's bundler
-- Similar pattern to how other Bun plugins transform code
+1. **Direct Reference (Bun.build)** – pass a configured plugin instance directly in the `plugins` array.
+2. **Package Reference with Convention (Bun.serve)** – declare `bun-plugin-arkenv` in `bunfig.toml` and let the plugin discover the ArkEnv schema file using a set of well-known paths.
-**Alternatives considered**:
-- `onResolve`: Only changes module paths, doesn't allow content transformation
-- `onStart`: Runs once at bundle start, too early for per-module transformation
-- `onBeforeParse`: Requires native plugin (NAPI), adds unnecessary complexity
-
-### Decision: Filter Based on Bun's Prefix Configuration
-
-**What**: Filter environment variables to only expose those matching Bun's configured prefix (defaults to `BUN_PUBLIC_*`).
+A third, more advanced mode using a custom static plugin file may be added later for projects with non-standard layouts, but is not required for the initial release.
**Why**:
-- Matches Bun's default behavior for client-exposed environment variables
-- Prevents server-only variables from being exposed to client code
-- Consistent with Vite plugin's filtering behavior
-- Users can configure prefix via `bunfig.toml` (e.g., `[serve.static] env = "BUN_PUBLIC_*"`)
-
-**Alternatives considered**:
-- No filtering: Would expose all variables, including server-only ones (security risk)
-- Custom prefix in plugin: Adds complexity, Bun's configuration is authoritative
-
-### Decision: Type Augmentation Similar to Vite Plugin
-**What**: Provide `ProcessEnvAugmented` type similar to Vite's `ImportMetaEnvAugmented` for type-safe `process.env` access.
-
-**Why**:
-- Provides consistent developer experience with Vite plugin
-- Enables TypeScript type checking for environment variables
-- Users can augment `process.env` types in their TypeScript declaration files
+- `Bun.build()` accepts plugins directly as JavaScript objects, which is ideal for explicit, programmatic configuration.
+- `Bun.serve()` uses `bunfig.toml` where `plugins` is a list of strings (module specifiers) and does not support passing options inline.
+- By using a package name (`"bun-plugin-arkenv"`) plus convention-based schema discovery, we avoid forcing users to create extra “config glue” files for the common case, while still keeping an escape hatch for advanced setups.
**Implementation**:
-- Create `ProcessEnvAugmented` type
-- Filter schema to only include variables matching prefix
-- Export type from plugin package for user augmentation
-### Decision: Package Structure
+**Pattern 1: Bun.build (Direct Reference)**
-**What**: Create new package `packages/bun-plugin/` following the same structure as `packages/vite-plugin/`.
+```ts
+// build.ts
+import { defineEnv } from "arkenv";
+import { createArkEnvBunPlugin } from "@arkenv/bun-plugin";
-**Why**:
-- Consistent with existing monorepo structure
-- Allows independent versioning and publishing
-- Keeps plugin-specific code separate from core library
+const env = defineEnv({
+ BUN_PUBLIC_API_URL: "string",
+ BUN_PUBLIC_DEBUG: "boolean",
+});
-**Structure**:
+await Bun.build({
+ entrypoints: ["./app.tsx"],
+ outdir: "./dist",
+ plugins: [createArkEnvBunPlugin(env)],
+});
```
-packages/bun-plugin/
-├── src/
-│ ├── index.ts # Main plugin export
-│ └── types.ts # Type augmentation utilities
-├── package.json
-├── tsconfig.json
-└── README.md
+
+**Pattern 2: Bun.serve (Package Reference with Convention)**
+
+```toml
+# bunfig.toml
+[serve.static]
+env = "BUN_PUBLIC_*"
+plugins = ["bun-plugin-arkenv"]
```
-## Risks / Trade-offs
+With a schema file at a conventional path, for example:
+
+```ts
+// src/env.ts
+import { defineEnv } from "arkenv";
-### Risk: Bun Plugin API Changes
+const env = defineEnv({
+ BUN_PUBLIC_API_URL: "string",
+ BUN_PUBLIC_DEBUG: "boolean",
+});
-**Mitigation**:
-- Follow Bun's official plugin API documentation
-- Test against multiple Bun versions if possible
-- Document minimum Bun version requirement
+export default env;
+```
-### Risk: Static Replacement Complexity
+At startup, `bun-plugin-arkenv`:
-**Mitigation**:
-- Use regex or AST transformation to replace `process.env.VARIABLE` patterns
-- Test thoroughly with various access patterns (destructuring, optional chaining, etc.)
-- Provide clear error messages if transformation fails
+- Searches for a schema file in a small set of well-known locations (for example: `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`).
+- Imports the first one it finds.
+- Reads the default export (or an `env` named export) as the ArkEnv schema.
+- Creates a Bun plugin via `createArkEnvBunPlugin(schema)` and registers it with `Bun.plugin(...)`.
-### Risk: Type Augmentation Complexity
+If no schema file is found, or the module does not export a usable schema, the plugin fails fast with a clear error message that lists the paths checked and shows a minimal example.
-**Mitigation**:
-- Reuse patterns from Vite plugin's type augmentation
-- Leverage existing `@repo/types` package for type inference
-- Provide clear documentation and examples
+**Future / Advanced Mode (Custom Static File)**
-## Migration Plan
+In future iterations we MAY support a custom plugin entry file pattern for advanced layouts, for example:
-### For New Users
-- Install `@arkenv/bun-plugin` package
-- Configure plugin in Bun build/serve configuration
-- Add type augmentation to TypeScript declaration file
-- Define environment variable schema
+```toml
+[serve.static]
+plugins = ["./arkenv.bun-plugin.ts"]
+```
-### For Existing Bun Users
-- No breaking changes to core `arkenv` package
-- Plugin is opt-in, existing code continues to work
-- Users can migrate gradually by adding plugin to their build configuration
+```ts
+// arkenv.bun-plugin.ts
+import { Bun } from "bun";
+import { buildArkEnvBunPlugin } from "bun-plugin-arkenv";
+
+Bun.plugin(
+ await buildArkEnvBunPlugin({
+ schemaPath: "./config/env/app.env.ts",
+ // future options (prefix overrides, strictness, etc.)
+ }),
+);
+```
-## Open Questions
+This keeps the default experience zero-config for most users, while still allowing power users to override the schema location and other options when needed.
-- Should the plugin support custom prefix configuration, or always use Bun's configured prefix?
- - **Decision**: Use Bun's configuration (bunfig.toml), no custom prefix option
-- How should the plugin handle environment variables accessed via destructuring (e.g., `const { VAR } = process.env`)?
- - **Decision**: Transform during `onLoad` before parsing, handle common patterns
-- Should the plugin validate all variables in the schema, or only those matching the prefix?
- - **Decision**: Validate all variables in schema, but only expose prefixed ones to client code (similar to Vite plugin)
+**Alternatives considered**:
+- **Static file reference as the only pattern** (previous design): Forces every project to create a separate `bun-plugin-config.ts` file even in simple setups, increasing boilerplate.
+- **Schema definition via JSON/YAML or bunfig.toml**: Reduces type safety and breaks the “define once in TypeScript” story that ArkEnv aims for.
+- **Only programmatic configuration** (no bunfig path): Would make full-stack `Bun.serve()` setups awkward compared to other Bun plugins that integrate via `bunfig.toml`.
+
+
+
+**Usage Patterns**:
+- **Bun.build**: Pass a configured plugin instance directly in the `plugins` array (standard Bun plugin API), for example `plugins: [createArkEnvBunPlugin(env)]`.
+- **Bun.serve (default)**: Configure `bunfig.toml` with `[serve.static].plugins = ["bun-plugin-arkenv"]`. The plugin discovers the ArkEnv schema file from a small set of conventional locations (for example `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`) and uses it automatically.
+- **Bun.serve (advanced, future)**: Optionally support a custom plugin entry file referenced from `bunfig.toml` (for example `plugins = ["./arkenv.bun-plugin.ts"]`) for projects that need a non-standard schema location or additional configuration.
+
diff --git a/openspec/changes/add-bun-plugin/proposal.md b/openspec/changes/add-bun-plugin/proposal.md
index e52f314d7..a7ce88570 100644
--- a/openspec/changes/add-bun-plugin/proposal.md
+++ b/openspec/changes/add-bun-plugin/proposal.md
@@ -1,3 +1,142 @@
+
+### Requirement: Bun Plugin Configuration Patterns
+
+The Bun plugin SHALL support two primary configuration patterns depending on the usage context:
+
+1. **Direct Reference (Bun.build)**: The plugin SHALL be configurable by passing a configured plugin instance directly in the `plugins` array when using `Bun.build()`.
+2. **Package Reference (Bun.serve)**: The plugin SHALL be configurable via a package name in `bunfig.toml` when using `Bun.serve()` for full-stack applications, with convention-based schema discovery.
+
+A future, more advanced configuration pattern using a custom static file MAY be supported, but is not required for the initial version.
+
+#### Scenario: Plugin configuration with Bun.build
+
+- **WHEN** a user wants to build an application using `Bun.build()`
+- **AND** they configure the plugin with an environment variable schema
+- **THEN** they can pass a configured plugin instance directly in the `plugins` array
+- **AND** the plugin validates and transforms environment variables during the build process
+
+#### Scenario: Plugin configuration with Bun.serve via package reference
+
+- **WHEN** a user wants to use `Bun.serve()` for a full-stack application
+- **AND** they configure `bunfig.toml` with:
+ - a `[serve.static]` section
+ - a `plugins` array that includes the package name `bun-plugin-arkenv`
+- **AND** their project contains an ArkEnv schema file in one of the supported default locations (for example, `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`)
+- **AND** that schema file exports a schema using `defineEnv` (via a default export or an `env` named export)
+- **THEN** the plugin SHALL locate the schema file via this convention-based search
+- **AND** it SHALL load the schema at startup
+- **AND** it SHALL use that schema to validate and transform environment variables during the bundling phase
+
+#### Scenario: Bun.serve configuration fails when no schema file is found
+
+- **WHEN** a user configures `bunfig.toml` with `[serve.static].plugins = ["bun-plugin-arkenv"]`
+- **AND** there is no schema file in any of the supported default locations
+- **THEN** the plugin SHALL fail fast with a clear, descriptive error message
+- **AND** the error message SHALL list the paths that were checked
+- **AND** the error message SHALL show an example of a minimal `env` schema file the user can create
+
+
+
+### Decision: Configuration Modes and Schema Discovery
+
+**What**: The plugin supports two core configuration modes:
+
+1. **Direct Reference (Bun.build)** – pass a configured plugin instance directly in the `plugins` array.
+2. **Package Reference with Convention (Bun.serve)** – declare `bun-plugin-arkenv` in `bunfig.toml` and let the plugin discover the ArkEnv schema file using a set of well-known paths.
+
+A third, more advanced mode using a custom static plugin file may be added later for projects with non-standard layouts, but is not required for the initial release.
+
+**Why**:
+
+- `Bun.build()` accepts plugins directly as JavaScript objects, which is ideal for explicit, programmatic configuration.
+- `Bun.serve()` uses `bunfig.toml` where `plugins` is a list of strings (module specifiers) and does not support passing options inline.
+- By using a package name (`"bun-plugin-arkenv"`) plus convention-based schema discovery, we avoid forcing users to create extra “config glue” files for the common case, while still keeping an escape hatch for advanced setups.
+
+**Implementation**:
+
+**Pattern 1: Bun.build (Direct Reference)**
+
+```ts
+// build.ts
+import { defineEnv } from "arkenv";
+import { createArkEnvBunPlugin } from "@arkenv/bun-plugin";
+
+const env = defineEnv({
+ BUN_PUBLIC_API_URL: "string",
+ BUN_PUBLIC_DEBUG: "boolean",
+});
+
+await Bun.build({
+ entrypoints: ["./app.tsx"],
+ outdir: "./dist",
+ plugins: [createArkEnvBunPlugin(env)],
+});
+```
+
+**Pattern 2: Bun.serve (Package Reference with Convention)**
+
+```toml
+# bunfig.toml
+[serve.static]
+env = "BUN_PUBLIC_*"
+plugins = ["bun-plugin-arkenv"]
+```
+
+With a schema file at a conventional path, for example:
+
+```ts
+// src/env.ts
+import { defineEnv } from "arkenv";
+
+const env = defineEnv({
+ BUN_PUBLIC_API_URL: "string",
+ BUN_PUBLIC_DEBUG: "boolean",
+});
+
+export default env;
+```
+
+At startup, `bun-plugin-arkenv`:
+
+- Searches for a schema file in a small set of well-known locations (for example: `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`).
+- Imports the first one it finds.
+- Reads the default export (or an `env` named export) as the ArkEnv schema.
+- Creates a Bun plugin via `createArkEnvBunPlugin(schema)` and registers it with `Bun.plugin(...)`.
+
+If no schema file is found, or the module does not export a usable schema, the plugin fails fast with a clear error message that lists the paths checked and shows a minimal example.
+
+**Future / Advanced Mode (Custom Static File)**
+
+In future iterations we MAY support a custom plugin entry file pattern for advanced layouts, for example:
+
+```toml
+[serve.static]
+plugins = ["./arkenv.bun-plugin.ts"]
+```
+
+```ts
+// arkenv.bun-plugin.ts
+import { Bun } from "bun";
+import { buildArkEnvBunPlugin } from "bun-plugin-arkenv";
+
+Bun.plugin(
+ await buildArkEnvBunPlugin({
+ schemaPath: "./config/env/app.env.ts",
+ // future options (prefix overrides, strictness, etc.)
+ }),
+);
+```
+
+This keeps the default experience zero-config for most users, while still allowing power users to override the schema location and other options when needed.
+
+**Alternatives considered**:
+
+- **Static file reference as the only pattern** (previous design): Forces every project to create a separate `bun-plugin-config.ts` file even in simple setups, increasing boilerplate.
+- **Schema definition via JSON/YAML or bunfig.toml**: Reduces type safety and breaks the “define once in TypeScript” story that ArkEnv aims for.
+- **Only programmatic configuration** (no bunfig path): Would make full-stack `Bun.serve()` setups awkward compared to other Bun plugins that integrate via `bunfig.toml`.
+
+
+
# Change: Add Bun Plugin for ArkEnv
## Why
@@ -29,19 +168,7 @@ The plugin will work very similarly to the Vite plugin:
- Replaces `process.env.VARIABLE` with validated, transformed values (e.g., string to boolean, default values)
- Provides TypeScript type augmentation for type-safe access
-## Impact
-
-- **Affected specs**: New capability `bun-plugin`
-- **Affected code**:
- - New package: `packages/bun-plugin/` (or similar structure)
- - Documentation files (README, docs)
- - Example Bun React playground updates
- - Type augmentation utilities (similar to `packages/vite-plugin/src/types.ts`)
-- **User-facing**: Bun users can now validate environment variables at build-time with full type safety, similar to Vite users
-
-## References
-
-- [Bun Plugin API Documentation](https://bun.com/docs/bundler/plugins) - Official Bun plugin API reference
-- Existing Vite plugin implementation (`packages/vite-plugin/`) - Reference for similar functionality
-- Bun React playground (`apps/playgrounds/bun-react/`) - Target environment for testing
-
+**Usage Patterns**:
+- **Bun.build**: Pass a configured plugin instance directly in the `plugins` array (standard Bun plugin API), for example `plugins: [createArkEnvBunPlugin(env)]`.
+- **Bun.serve (default)**: Configure `bunfig.toml` with `[serve.static].plugins = ["bun-plugin-arkenv"]`. The plugin discovers the ArkEnv schema file from a small set of conventional locations (for example `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`) and uses it automatically.
+- **Bun.serve (advanced, future)**: Optionally support a custom plugin entry file referenced from `bunfig.toml` (for example `plugins = ["./arkenv.bun-plugin.ts"]`) for projects that need a non-standard schema location or additional configuration.
diff --git a/openspec/changes/add-bun-plugin/specs/bun-plugin/spec.md b/openspec/changes/add-bun-plugin/specs/bun-plugin/spec.md
index 90be5bb07..91810f762 100644
--- a/openspec/changes/add-bun-plugin/specs/bun-plugin/spec.md
+++ b/openspec/changes/add-bun-plugin/specs/bun-plugin/spec.md
@@ -2,6 +2,42 @@
## ADDED Requirements
+### Requirement: Bun Plugin Configuration Patterns
+
+The Bun plugin SHALL support two primary configuration patterns depending on the usage context:
+
+1. **Direct Reference (Bun.build)**: The plugin SHALL be configurable by passing a configured plugin instance directly in the `plugins` array when using `Bun.build()`.
+2. **Package Reference (Bun.serve)**: The plugin SHALL be configurable via a package name in `bunfig.toml` when using `Bun.serve()` for full-stack applications, with convention-based schema discovery.
+
+A future, more advanced configuration pattern using a custom static file MAY be supported, but is not required for the initial version.
+
+#### Scenario: Plugin configuration with Bun.build
+
+- **WHEN** a user wants to build an application using `Bun.build()`
+- **AND** they configure the plugin with an environment variable schema
+- **THEN** they can pass a configured plugin instance directly in the `plugins` array
+- **AND** the plugin validates and transforms environment variables during the build process
+
+#### Scenario: Plugin configuration with Bun.serve via package reference
+
+- **WHEN** a user wants to use `Bun.serve()` for a full-stack application
+- **AND** they configure `bunfig.toml` with:
+ - a `[serve.static]` section
+ - a `plugins` array that includes the package name `bun-plugin-arkenv`
+- **AND** their project contains an ArkEnv schema file in one of the supported default locations (for example, `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`)
+- **AND** that schema file exports a schema using `defineEnv` (via a default export or an `env` named export)
+- **THEN** the plugin SHALL locate the schema file via this convention-based search
+- **AND** it SHALL load the schema at startup
+- **AND** it SHALL use that schema to validate and transform environment variables during the bundling phase
+
+#### Scenario: Bun.serve configuration fails when no schema file is found
+
+- **WHEN** a user configures `bunfig.toml` with `[serve.static].plugins = ["bun-plugin-arkenv"]`
+- **AND** there is no schema file in any of the supported default locations
+- **THEN** the plugin SHALL fail fast with a clear, descriptive error message
+- **AND** the error message SHALL list the paths that were checked
+- **AND** the error message SHALL show an example of a minimal `env` schema file the user can create
+
### Requirement: Bun Plugin Environment Variable Validation and Transformation
The Bun plugin SHALL validate environment variables at build-time using ArkEnv's schema validation 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).
@@ -65,4 +101,3 @@ The Bun plugin SHALL work correctly with Bun's `serve` function for full-stack R
- **AND** the plugin transforms `process.env` access in both server and client code
- **AND** only prefixed variables are exposed to client code
- **AND** the server starts successfully with validated environment variables
-
diff --git a/openspec/changes/add-bun-plugin/tasks.md b/openspec/changes/add-bun-plugin/tasks.md
index 3172c5f43..226b2b65d 100644
--- a/openspec/changes/add-bun-plugin/tasks.md
+++ b/openspec/changes/add-bun-plugin/tasks.md
@@ -1,60 +1,145 @@
-## 1. Package Setup
-
-- [x] 1.1 Create `packages/bun-plugin/` directory structure
-- [x] 1.2 Initialize package.json with proper metadata and peer dependencies (bun, arkenv, arktype)
-- [x] 1.3 Set up TypeScript configuration (tsconfig.json)
-- [x] 1.4 Set up build configuration (tsdown.config.ts or similar)
-- [x] 1.5 Add package to pnpm workspace
-- [ ] 1.6 Configure changeset for the new package
-
-## 2. Core Plugin Implementation
-
-- [x] 2.1 Implement main plugin function that accepts schema (similar to Vite plugin signature)
-- [x] 2.2 Implement `onLoad` hook to intercept module loading
-- [x] 2.3 Add logic to detect and transform `process.env.VARIABLE` patterns
-- [x] 2.4 Integrate ArkEnv's `createEnv` for validation and transformation
-- [x] 2.5 Implement filtering logic to only expose variables matching Bun's prefix (defaults to `BUN_PUBLIC_*`)
-- [x] 2.6 Handle static replacement of `process.env` variables with validated values
-- [x] 2.7 Add support for Bun's prefix configuration (read from bunfig.toml or use default)
-
-## 3. Type Augmentation
-
-- [x] 3.1 Create `ProcessEnvAugmented` type utility
-- [x] 3.2 Implement filtering logic to only include prefixed variables in type
-- [x] 3.3 Export type from plugin package
-- [x] 3.4 Add JSDoc documentation with usage examples
-
-## 4. Testing
-
-- [x] 4.1 Add unit tests for plugin function
-- [ ] 4.2 Add integration tests using Bun React playground as fixture
-- [x] 4.3 Test environment variable validation and error handling
-- [x] 4.4 Test filtering behavior (only prefixed variables exposed)
-- [ ] 4.5 Test type augmentation (TypeScript compilation tests)
-- [ ] 4.6 Test various `process.env` access patterns (direct access, destructuring, optional chaining)
-- [ ] 4.7 Test with Bun's serve function in full-stack React app
-
-## 5. Documentation
-
-- [x] 5.1 Create README.md for the package with installation and usage instructions
-- [ ] 5.2 Add documentation page to www app (docs/bun-plugin/)
-- [ ] 5.3 Document type augmentation pattern (similar to Vite plugin docs)
-- [x] 5.4 Add examples showing common usage patterns
-- [x] 5.5 Document Bun prefix configuration and filtering behavior
-- [ ] 5.6 Add troubleshooting section
-
-## 6. Examples and Playgrounds
-
-- [ ] 6.1 Update Bun React playground to use the plugin
-- [ ] 6.2 Add example showing environment variable validation
-- [ ] 6.3 Add example showing type augmentation setup
-- [ ] 6.4 Verify playground builds and runs correctly with plugin
-
-## 7. Validation
-
-- [x] 7.1 Run `openspec validate add-bun-plugin --strict`
-- [x] 7.2 Verify all tests pass
-- [x] 7.3 Verify TypeScript compilation succeeds
-- [ ] 7.4 Verify playground examples work correctly
-- [x] 7.5 Check bundle size meets project constraints (<2kB goal)
+
+### Requirement: Bun Plugin Configuration Patterns
+The Bun plugin SHALL support two primary configuration patterns depending on the usage context:
+
+1. **Direct Reference (Bun.build)**: The plugin SHALL be configurable by passing a configured plugin instance directly in the `plugins` array when using `Bun.build()`.
+2. **Package Reference (Bun.serve)**: The plugin SHALL be configurable via a package name in `bunfig.toml` when using `Bun.serve()` for full-stack applications, with convention-based schema discovery.
+
+A future, more advanced configuration pattern using a custom static file MAY be supported, but is not required for the initial version.
+
+#### Scenario: Plugin configuration with Bun.build
+
+- **WHEN** a user wants to build an application using `Bun.build()`
+- **AND** they configure the plugin with an environment variable schema
+- **THEN** they can pass a configured plugin instance directly in the `plugins` array
+- **AND** the plugin validates and transforms environment variables during the build process
+
+#### Scenario: Plugin configuration with Bun.serve via package reference
+
+- **WHEN** a user wants to use `Bun.serve()` for a full-stack application
+- **AND** they configure `bunfig.toml` with:
+ - a `[serve.static]` section
+ - a `plugins` array that includes the package name `bun-plugin-arkenv`
+- **AND** their project contains an ArkEnv schema file in one of the supported default locations (for example, `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`)
+- **AND** that schema file exports a schema using `defineEnv` (via a default export or an `env` named export)
+- **THEN** the plugin SHALL locate the schema file via this convention-based search
+- **AND** it SHALL load the schema at startup
+- **AND** it SHALL use that schema to validate and transform environment variables during the bundling phase
+
+#### Scenario: Bun.serve configuration fails when no schema file is found
+
+- **WHEN** a user configures `bunfig.toml` with `[serve.static].plugins = ["bun-plugin-arkenv"]`
+- **AND** there is no schema file in any of the supported default locations
+- **THEN** the plugin SHALL fail fast with a clear, descriptive error message
+- **AND** the error message SHALL list the paths that were checked
+- **AND** the error message SHALL show an example of a minimal `env` schema file the user can create
+
+### Requirement: Bun Plugin Environment Variable Validation and Transformation
+
+
+
+### Decision: Configuration Modes and Schema Discovery
+
+**What**: The plugin supports two core configuration modes:
+
+1. **Direct Reference (Bun.build)** – pass a configured plugin instance directly in the `plugins` array.
+2. **Package Reference with Convention (Bun.serve)** – declare `bun-plugin-arkenv` in `bunfig.toml` and let the plugin discover the ArkEnv schema file using a set of well-known paths.
+
+A third, more advanced mode using a custom static plugin file may be added later for projects with non-standard layouts, but is not required for the initial release.
+
+**Why**:
+
+- `Bun.build()` accepts plugins directly as JavaScript objects, which is ideal for explicit, programmatic configuration.
+- `Bun.serve()` uses `bunfig.toml` where `plugins` is a list of strings (module specifiers) and does not support passing options inline.
+- By using a package name (`"bun-plugin-arkenv"`) plus convention-based schema discovery, we avoid forcing users to create extra “config glue” files for the common case, while still keeping an escape hatch for advanced setups.
+
+**Implementation**:
+
+**Pattern 1: Bun.build (Direct Reference)**
+
+```ts
+// build.ts
+import { defineEnv } from "arkenv";
+import { createArkEnvBunPlugin } from "@arkenv/bun-plugin";
+
+const env = defineEnv({
+ BUN_PUBLIC_API_URL: "string",
+ BUN_PUBLIC_DEBUG: "boolean",
+});
+
+await Bun.build({
+ entrypoints: ["./app.tsx"],
+ outdir: "./dist",
+ plugins: [createArkEnvBunPlugin(env)],
+});
+```
+
+**Pattern 2: Bun.serve (Package Reference with Convention)**
+
+```toml
+# bunfig.toml
+[serve.static]
+env = "BUN_PUBLIC_*"
+plugins = ["bun-plugin-arkenv"]
+```
+
+With a schema file at a conventional path, for example:
+
+```ts
+// src/env.ts
+import { defineEnv } from "arkenv";
+
+const env = defineEnv({
+ BUN_PUBLIC_API_URL: "string",
+ BUN_PUBLIC_DEBUG: "boolean",
+});
+
+export default env;
+```
+
+At startup, `bun-plugin-arkenv`:
+
+- Searches for a schema file in a small set of well-known locations (for example: `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`).
+- Imports the first one it finds.
+- Reads the default export (or an `env` named export) as the ArkEnv schema.
+- Creates a Bun plugin via `createArkEnvBunPlugin(schema)` and registers it with `Bun.plugin(...)`.
+
+If no schema file is found, or the module does not export a usable schema, the plugin fails fast with a clear error message that lists the paths checked and shows a minimal example.
+
+**Future / Advanced Mode (Custom Static File)**
+
+In future iterations we MAY support a custom plugin entry file pattern for advanced layouts, for example:
+
+```toml
+[serve.static]
+plugins = ["./arkenv.bun-plugin.ts"]
+```
+
+```ts
+// arkenv.bun-plugin.ts
+import { Bun } from "bun";
+import { buildArkEnvBunPlugin } from "bun-plugin-arkenv";
+
+Bun.plugin(
+ await buildArkEnvBunPlugin({
+ schemaPath: "./config/env/app.env.ts",
+ // future options (prefix overrides, strictness, etc.)
+ }),
+);
+```
+
+This keeps the default experience zero-config for most users, while still allowing power users to override the schema location and other options when needed.
+
+**Alternatives considered**:
+
+- **Static file reference as the only pattern** (previous design): Forces every project to create a separate `bun-plugin-config.ts` file even in simple setups, increasing boilerplate.
+- **Schema definition via JSON/YAML or bunfig.toml**: Reduces type safety and breaks the “define once in TypeScript” story that ArkEnv aims for.
+- **Only programmatic configuration** (no bunfig path): Would make full-stack `Bun.serve()` setups awkward compared to other Bun plugins that integrate via `bunfig.toml`.
+
+
+
+**Usage Patterns**:
+- **Bun.build**: Pass a configured plugin instance directly in the `plugins` array (standard Bun plugin API), for example `plugins: [createArkEnvBunPlugin(env)]`.
+- **Bun.serve (default)**: Configure `bunfig.toml` with `[serve.static].plugins = ["bun-plugin-arkenv"]`. The plugin discovers the ArkEnv schema file from a small set of conventional locations (for example `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`) and uses it automatically.
+- **Bun.serve (advanced, future)**: Optionally support a custom plugin entry file referenced from `bunfig.toml` (for example `plugins = ["./arkenv.bun-plugin.ts"]`) for projects that need a non-standard schema location or additional configuration.
From 25d9fb470d0421cd9fd32ebdbc4fb211d07cc964 Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Thu, 27 Nov 2025 18:52:39 +0500
Subject: [PATCH 12/48] chore: add `@types/bun` to bun-plugin dev dependencies
---
packages/bun-plugin/package.json | 1 +
pnpm-lock.yaml | 235 ++++++++++++++++++++++++++-----
2 files changed, 201 insertions(+), 35 deletions(-)
diff --git a/packages/bun-plugin/package.json b/packages/bun-plugin/package.json
index 0a01508fd..8709bc115 100644
--- a/packages/bun-plugin/package.json
+++ b/packages/bun-plugin/package.json
@@ -14,6 +14,7 @@
"devDependencies": {
"@repo/types": "workspace:*",
"@size-limit/preset-small-lib": "11.2.0",
+ "@types/bun": "1.3.2",
"arktype": "2.1.27",
"bun": "1.1.0",
"size-limit": "11.2.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3919f0f0f..704e26324 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -105,7 +105,7 @@ importers:
version: link:../../../packages/vite-plugin
'@solidjs/start':
specifier: ^1.1.0
- version: 1.2.0(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vinxi@0.5.8(@types/node@24.10.1)(@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.51)(terser@5.44.1)(tsx@4.20.6))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6))
+ version: 1.2.0(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vinxi@0.5.8(@types/node@24.10.1)(@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.52)(terser@5.44.1)(tsx@4.20.6))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6))
arkenv:
specifier: workspace:*
version: link:../../../packages/arkenv
@@ -117,7 +117,7 @@ importers:
version: 1.9.10
vinxi:
specifier: ^0.5.7
- version: 0.5.8(@types/node@24.10.1)(@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.51)(terser@5.44.1)(tsx@4.20.6)
+ version: 0.5.8(@types/node@24.10.1)(@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.52)(terser@5.44.1)(tsx@4.20.6)
apps/playgrounds/vite:
dependencies:
@@ -139,7 +139,7 @@ importers:
devDependencies:
'@julr/vite-plugin-validate-env':
specifier: 2.2.0
- version: 2.2.0(rolldown-vite@7.2.7(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6))
+ version: 2.2.0(rolldown-vite@7.2.8(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6))
'@types/react':
specifier: 19.2.6
version: 19.2.6
@@ -148,7 +148,7 @@ importers:
version: 19.2.3(@types/react@19.2.6)
'@vitejs/plugin-react':
specifier: 5.1.1
- version: 5.1.1(rolldown-vite@7.2.7(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6))
+ version: 5.1.1(rolldown-vite@7.2.8(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6))
globals:
specifier: 16.5.0
version: 16.5.0
@@ -157,10 +157,10 @@ importers:
version: 5.9.3
vite:
specifier: npm:rolldown-vite@latest
- version: rolldown-vite@7.2.7(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)
+ version: rolldown-vite@7.2.8(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)
vite-tsconfig-paths:
specifier: 5.1.4
- version: 5.1.4(rolldown-vite@7.2.7(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6))(typescript@5.9.3)
+ version: 5.1.4(rolldown-vite@7.2.8(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6))(typescript@5.9.3)
apps/www:
dependencies:
@@ -370,6 +370,9 @@ importers:
'@size-limit/preset-small-lib':
specifier: 11.2.0
version: 11.2.0(size-limit@11.2.0)
+ '@types/bun':
+ specifier: 1.3.2
+ version: 1.3.2(@types/react@19.2.6)
arktype:
specifier: 2.1.27
version: 2.1.27
@@ -1838,8 +1841,8 @@ packages:
resolution: {integrity: sha512-34lh4o9CcSw09Hx6fKihPu85+m+4pmDlkXwJrLvN5nMq5JrcGhhihVM415zDqT8j8IixO1PYYdQZRN4SwQCncg==}
engines: {node: ^20.19.0 || >=22.12.0}
- '@oxc-project/runtime@0.98.0':
- resolution: {integrity: sha512-F0ldlBv2orG2YqNL0w77deq9yCaO4zEHbanGnW/jaJxGBR8ImekvZb8x42zAHvdzr8J76psibijvHtXfSjbEIQ==}
+ '@oxc-project/runtime@0.99.0':
+ resolution: {integrity: sha512-8iE5/4OK0SLHqWzRxSvI1gjFPmIH6718s8iwkuco95rBZsCZIHq+5wy4lYsASxnH+8FOhbGndiUrcwsVG5i2zw==}
engines: {node: ^20.19.0 || >=22.12.0}
'@oxc-project/types@0.97.0':
@@ -1848,6 +1851,9 @@ packages:
'@oxc-project/types@0.98.0':
resolution: {integrity: sha512-Vzmd6FsqVuz5HQVcRC/hrx7Ujo3WEVeQP7C2UNP5uy1hUY4SQvMB+93jxkI1KRHz9a/6cni3glPOtvteN+zpsw==}
+ '@oxc-project/types@0.99.0':
+ resolution: {integrity: sha512-LLDEhXB7g1m5J+woRSgfKsFPS3LhR9xRhTeIoEBm5WrkwMxn6eZ0Ld0c0K5eHB57ChZX6I3uSmmLjZ8pcjlRcw==}
+
'@paralleldrive/cuid2@2.3.1':
resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==}
@@ -2414,6 +2420,12 @@ packages:
cpu: [arm64]
os: [android]
+ '@rolldown/binding-android-arm64@1.0.0-beta.52':
+ resolution: {integrity: sha512-MBGIgysimZPqTDcLXI+i9VveijkP5C3EAncEogXhqfax6YXj1Tr2LY3DVuEOMIjWfMPMhtQSPup4fSTAmgjqIw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
'@rolldown/binding-darwin-arm64@1.0.0-beta.50':
resolution: {integrity: sha512-+JRqKJhoFlt5r9q+DecAGPLZ5PxeLva+wCMtAuoFMWPoZzgcYrr599KQ+Ix0jwll4B4HGP43avu9My8KtSOR+w==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -2426,6 +2438,12 @@ packages:
cpu: [arm64]
os: [darwin]
+ '@rolldown/binding-darwin-arm64@1.0.0-beta.52':
+ resolution: {integrity: sha512-MmKeoLnKu1d9j6r19K8B+prJnIZ7u+zQ+zGQ3YHXGnr41rzE3eqQLovlkvoZnRoxDGPA4ps0pGiwXy6YE3lJyg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
'@rolldown/binding-darwin-x64@1.0.0-beta.50':
resolution: {integrity: sha512-fFXDjXnuX7/gQZQm/1FoivVtRcyAzdjSik7Eo+9iwPQ9EgtA5/nB2+jmbzaKtMGG3q+BnZbdKHCtOacmNrkIDA==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -2438,6 +2456,12 @@ packages:
cpu: [x64]
os: [darwin]
+ '@rolldown/binding-darwin-x64@1.0.0-beta.52':
+ resolution: {integrity: sha512-qpHedvQBmIjT8zdnjN3nWPR2qjQyJttbXniCEKKdHeAbZG9HyNPBUzQF7AZZGwmS9coQKL+hWg9FhWzh2dZ2IA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
'@rolldown/binding-freebsd-x64@1.0.0-beta.50':
resolution: {integrity: sha512-F1b6vARy49tjmT/hbloplzgJS7GIvwWZqt+tAHEstCh0JIh9sa8FAMVqEmYxDviqKBaAI8iVvUREm/Kh/PD26Q==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -2450,6 +2474,12 @@ packages:
cpu: [x64]
os: [freebsd]
+ '@rolldown/binding-freebsd-x64@1.0.0-beta.52':
+ resolution: {integrity: sha512-dDp7WbPapj/NVW0LSiH/CLwMhmLwwKb3R7mh2kWX+QW85X1DGVnIEyKh9PmNJjB/+suG1dJygdtdNPVXK1hylg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.50':
resolution: {integrity: sha512-U6cR76N8T8M6lHj7EZrQ3xunLPxSvYYxA8vJsBKZiFZkT8YV4kjgCO3KwMJL0NOjQCPGKyiXO07U+KmJzdPGRw==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -2462,6 +2492,12 @@ packages:
cpu: [arm]
os: [linux]
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.52':
+ resolution: {integrity: sha512-9e4l6vy5qNSliDPqNfR6CkBOAx6PH7iDV4OJiEJzajajGrVy8gc/IKKJUsoE52G8ud8MX6r3PMl97NfwgOzB7g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.50':
resolution: {integrity: sha512-ONgyjofCrrE3bnh5GZb8EINSFyR/hmwTzZ7oVuyUB170lboza1VMCnb8jgE6MsyyRgHYmN8Lb59i3NKGrxrYjw==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -2474,6 +2510,12 @@ packages:
cpu: [arm64]
os: [linux]
+ '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.52':
+ resolution: {integrity: sha512-V48oDR84feRU2KRuzpALp594Uqlx27+zFsT6+BgTcXOtu7dWy350J1G28ydoCwKB+oxwsRPx2e7aeQnmd3YJbQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.50':
resolution: {integrity: sha512-L0zRdH2oDPkmB+wvuTl+dJbXCsx62SkqcEqdM+79LOcB+PxbAxxjzHU14BuZIQdXcAVDzfpMfaHWzZuwhhBTcw==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -2486,6 +2528,12 @@ packages:
cpu: [arm64]
os: [linux]
+ '@rolldown/binding-linux-arm64-musl@1.0.0-beta.52':
+ resolution: {integrity: sha512-ENLmSQCWqSA/+YN45V2FqTIemg7QspaiTjlm327eUAMeOLdqmSOVVyrQexJGNTQ5M8sDYCgVAig2Kk01Ggmqaw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.50':
resolution: {integrity: sha512-gyoI8o/TGpQd3OzkJnh1M2kxy1Bisg8qJ5Gci0sXm9yLFzEXIFdtc4EAzepxGvrT2ri99ar5rdsmNG0zP0SbIg==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -2498,6 +2546,12 @@ packages:
cpu: [x64]
os: [linux]
+ '@rolldown/binding-linux-x64-gnu@1.0.0-beta.52':
+ resolution: {integrity: sha512-klahlb2EIFltSUubn/VLjuc3qxp1E7th8ukayPfdkcKvvYcQ5rJztgx8JsJSuAKVzKtNTqUGOhy4On71BuyV8g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
'@rolldown/binding-linux-x64-musl@1.0.0-beta.50':
resolution: {integrity: sha512-zti8A7M+xFDpKlghpcCAzyOi+e5nfUl3QhU023ce5NCgUxRG5zGP2GR9LTydQ1rnIPwZUVBWd4o7NjZDaQxaXA==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -2510,6 +2564,12 @@ packages:
cpu: [x64]
os: [linux]
+ '@rolldown/binding-linux-x64-musl@1.0.0-beta.52':
+ resolution: {integrity: sha512-UuA+JqQIgqtkgGN2c/AQ5wi8M6mJHrahz/wciENPTeI6zEIbbLGoth5XN+sQe2pJDejEVofN9aOAp0kaazwnVg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
'@rolldown/binding-openharmony-arm64@1.0.0-beta.50':
resolution: {integrity: sha512-eZUssog7qljrrRU9Mi0eqYEPm3Ch0UwB+qlWPMKSUXHNqhm3TvDZarJQdTevGEfu3EHAXJvBIe0YFYr0TPVaMA==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -2522,6 +2582,12 @@ packages:
cpu: [arm64]
os: [openharmony]
+ '@rolldown/binding-openharmony-arm64@1.0.0-beta.52':
+ resolution: {integrity: sha512-1BNQW8u4ro8bsN1+tgKENJiqmvc+WfuaUhXzMImOVSMw28pkBKdfZtX2qJPADV3terx+vNJtlsgSGeb3+W6Jiw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
'@rolldown/binding-wasm32-wasi@1.0.0-beta.50':
resolution: {integrity: sha512-nmCN0nIdeUnmgeDXiQ+2HU6FT162o+rxnF7WMkBm4M5Ds8qTU7Dzv2Wrf22bo4ftnlrb2hKK6FSwAJSAe2FWLg==}
engines: {node: '>=14.0.0'}
@@ -2532,6 +2598,11 @@ packages:
engines: {node: '>=14.0.0'}
cpu: [wasm32]
+ '@rolldown/binding-wasm32-wasi@1.0.0-beta.52':
+ resolution: {integrity: sha512-K/p7clhCqJOQpXGykrFaBX2Dp9AUVIDHGc+PtFGBwg7V+mvBTv/tsm3LC3aUmH02H2y3gz4y+nUTQ0MLpofEEg==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.50':
resolution: {integrity: sha512-7kcNLi7Ua59JTTLvbe1dYb028QEPaJPJQHqkmSZ5q3tJueUeb6yjRtx8mw4uIqgWZcnQHAR3PrLN4XRJxvgIkA==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -2544,6 +2615,12 @@ packages:
cpu: [arm64]
os: [win32]
+ '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.52':
+ resolution: {integrity: sha512-a4EkXBtnYYsKipjS7QOhEBM4bU5IlR9N1hU+JcVEVeuTiaslIyhWVKsvf7K2YkQHyVAJ+7/A9BtrGqORFcTgng==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
'@rolldown/binding-win32-ia32-msvc@1.0.0-beta.50':
resolution: {integrity: sha512-lL70VTNvSCdSZkDPPVMwWn/M2yQiYvSoXw9hTLgdIWdUfC3g72UaruezusR6ceRuwHCY1Ayu2LtKqXkBO5LIwg==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -2556,6 +2633,12 @@ packages:
cpu: [ia32]
os: [win32]
+ '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.52':
+ resolution: {integrity: sha512-5ZXcYyd4GxPA6QfbGrNcQjmjbuLGvfz6728pZMsQvGHI+06LT06M6TPtXvFvLgXtexc+OqvFe1yAIXJU1gob/w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ia32]
+ os: [win32]
+
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.50':
resolution: {integrity: sha512-4qU4x5DXWB4JPjyTne/wBNPqkbQU8J45bl21geERBKtEittleonioACBL1R0PsBu0Aq21SwMK5a9zdBkWSlQtQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -2568,6 +2651,12 @@ packages:
cpu: [x64]
os: [win32]
+ '@rolldown/binding-win32-x64-msvc@1.0.0-beta.52':
+ resolution: {integrity: sha512-tzpnRQXJrSzb8Z9sm97UD3cY0toKOImx+xRKsDLX4zHaAlRXWh7jbaKBePJXEN7gNw7Nm03PBNwphdtA8KSUYQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
'@rolldown/pluginutils@1.0.0-beta.47':
resolution: {integrity: sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==}
@@ -2577,6 +2666,9 @@ packages:
'@rolldown/pluginutils@1.0.0-beta.51':
resolution: {integrity: sha512-51/8cNXMrqWqX3o8DZidhwz1uYq0BhHDDSfVygAND1Skx5s1TDw3APSSxCMcFFedwgqGcx34gRouwY+m404BBQ==}
+ '@rolldown/pluginutils@1.0.0-beta.52':
+ resolution: {integrity: sha512-/L0htLJZbaZFL1g9OHOblTxbCYIGefErJjtYOwgl9ZqNx27P3L0SDfjhhHIss32gu5NWgnxuT2a2Hnnv6QGHKA==}
+
'@rollup/plugin-alias@5.1.1':
resolution: {integrity: sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==}
engines: {node: '>=14.0.0'}
@@ -6315,8 +6407,8 @@ packages:
vue-tsc:
optional: true
- rolldown-vite@7.2.7:
- resolution: {integrity: sha512-N6a9KgNZ0xgCJ6/Ej2FQ7W8D3fOzDwFw7CLWZ2ubZknVrs9NdNkx25AFEuNbSwQO76VEHp4N7YatsZwp/ST1Gg==}
+ rolldown-vite@7.2.8:
+ resolution: {integrity: sha512-8wKihlF6EDF8grimwd7GPOhLkQkSIgj6Hlcp0CXhtO3HAXeUUqhgZmJmn07OF8e4PbTusMX6Yxmy1BptVRZsdw==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
@@ -6365,6 +6457,11 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
+ rolldown@1.0.0-beta.52:
+ resolution: {integrity: sha512-Hbnpljue+JhMJrlOjQ1ixp9me7sUec7OjFvS+A1Qm8k8Xyxmw3ZhxFu7LlSXW1s9AX3POE9W9o2oqCEeR5uDmg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+
rollup-plugin-visualizer@6.0.5:
resolution: {integrity: sha512-9+HlNgKCVbJDs8tVtjQ43US12eqaiHyyiLMdBwQ7vSZPiHMysGNo2E88TAp1si5wx8NAoYriI2A5kuKfIakmJg==}
engines: {node: '>=18'}
@@ -8859,13 +8956,13 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
- '@julr/vite-plugin-validate-env@2.2.0(rolldown-vite@7.2.7(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6))':
+ '@julr/vite-plugin-validate-env@2.2.0(rolldown-vite@7.2.8(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6))':
dependencies:
'@poppinss/cliui': 6.5.0
'@poppinss/validator-lite': 2.1.2
'@standard-schema/spec': 1.0.0
unconfig: 7.4.1
- vite: rolldown-vite@7.2.7(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)
+ vite: rolldown-vite@7.2.8(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)
'@manypkg/cli@0.25.1':
dependencies:
@@ -9298,12 +9395,14 @@ snapshots:
'@oxc-project/runtime@0.96.0': {}
- '@oxc-project/runtime@0.98.0': {}
+ '@oxc-project/runtime@0.99.0': {}
'@oxc-project/types@0.97.0': {}
'@oxc-project/types@0.98.0': {}
+ '@oxc-project/types@0.99.0': {}
+
'@paralleldrive/cuid2@2.3.1':
dependencies:
'@noble/hashes': 1.8.0
@@ -9845,60 +9944,90 @@ snapshots:
'@rolldown/binding-android-arm64@1.0.0-beta.51':
optional: true
+ '@rolldown/binding-android-arm64@1.0.0-beta.52':
+ optional: true
+
'@rolldown/binding-darwin-arm64@1.0.0-beta.50':
optional: true
'@rolldown/binding-darwin-arm64@1.0.0-beta.51':
optional: true
+ '@rolldown/binding-darwin-arm64@1.0.0-beta.52':
+ optional: true
+
'@rolldown/binding-darwin-x64@1.0.0-beta.50':
optional: true
'@rolldown/binding-darwin-x64@1.0.0-beta.51':
optional: true
+ '@rolldown/binding-darwin-x64@1.0.0-beta.52':
+ optional: true
+
'@rolldown/binding-freebsd-x64@1.0.0-beta.50':
optional: true
'@rolldown/binding-freebsd-x64@1.0.0-beta.51':
optional: true
+ '@rolldown/binding-freebsd-x64@1.0.0-beta.52':
+ optional: true
+
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.50':
optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.51':
optional: true
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.52':
+ optional: true
+
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.50':
optional: true
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.51':
optional: true
+ '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.52':
+ optional: true
+
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.50':
optional: true
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.51':
optional: true
+ '@rolldown/binding-linux-arm64-musl@1.0.0-beta.52':
+ optional: true
+
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.50':
optional: true
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.51':
optional: true
+ '@rolldown/binding-linux-x64-gnu@1.0.0-beta.52':
+ optional: true
+
'@rolldown/binding-linux-x64-musl@1.0.0-beta.50':
optional: true
'@rolldown/binding-linux-x64-musl@1.0.0-beta.51':
optional: true
+ '@rolldown/binding-linux-x64-musl@1.0.0-beta.52':
+ optional: true
+
'@rolldown/binding-openharmony-arm64@1.0.0-beta.50':
optional: true
'@rolldown/binding-openharmony-arm64@1.0.0-beta.51':
optional: true
+ '@rolldown/binding-openharmony-arm64@1.0.0-beta.52':
+ optional: true
+
'@rolldown/binding-wasm32-wasi@1.0.0-beta.50':
dependencies:
'@napi-rs/wasm-runtime': 1.0.7
@@ -9909,30 +10038,46 @@ snapshots:
'@napi-rs/wasm-runtime': 1.0.7
optional: true
+ '@rolldown/binding-wasm32-wasi@1.0.0-beta.52':
+ dependencies:
+ '@napi-rs/wasm-runtime': 1.0.7
+ optional: true
+
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.50':
optional: true
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.51':
optional: true
+ '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.52':
+ optional: true
+
'@rolldown/binding-win32-ia32-msvc@1.0.0-beta.50':
optional: true
'@rolldown/binding-win32-ia32-msvc@1.0.0-beta.51':
optional: true
+ '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.52':
+ optional: true
+
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.50':
optional: true
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.51':
optional: true
+ '@rolldown/binding-win32-x64-msvc@1.0.0-beta.52':
+ optional: true
+
'@rolldown/pluginutils@1.0.0-beta.47': {}
'@rolldown/pluginutils@1.0.0-beta.50': {}
'@rolldown/pluginutils@1.0.0-beta.51': {}
+ '@rolldown/pluginutils@1.0.0-beta.52': {}
+
'@rollup/plugin-alias@5.1.1(rollup@4.53.3)':
optionalDependencies:
rollup: 4.53.3
@@ -10729,11 +10874,11 @@ snapshots:
dependencies:
tslib: 2.8.1
- '@solidjs/start@1.2.0(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vinxi@0.5.8(@types/node@24.10.1)(@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.51)(terser@5.44.1)(tsx@4.20.6))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6))':
+ '@solidjs/start@1.2.0(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vinxi@0.5.8(@types/node@24.10.1)(@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.52)(terser@5.44.1)(tsx@4.20.6))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6))':
dependencies:
'@tanstack/server-functions-plugin': 1.121.21(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6))
- '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.8(@types/node@24.10.1)(@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.51)(terser@5.44.1)(tsx@4.20.6))
- '@vinxi/server-components': 0.5.1(vinxi@0.5.8(@types/node@24.10.1)(@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.51)(terser@5.44.1)(tsx@4.20.6))
+ '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.8(@types/node@24.10.1)(@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.52)(terser@5.44.1)(tsx@4.20.6))
+ '@vinxi/server-components': 0.5.1(vinxi@0.5.8(@types/node@24.10.1)(@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.52)(terser@5.44.1)(tsx@4.20.6))
cookie-es: 2.0.0
defu: 6.1.4
error-stack-parser: 2.1.4
@@ -10745,7 +10890,7 @@ snapshots:
source-map-js: 1.2.1
terracotta: 1.0.6(solid-js@1.9.10)
tinyglobby: 0.2.15
- vinxi: 0.5.8(@types/node@24.10.1)(@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.51)(terser@5.44.1)(tsx@4.20.6)
+ vinxi: 0.5.8(@types/node@24.10.1)(@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.52)(terser@5.44.1)(tsx@4.20.6)
vite-plugin-solid: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6))
transitivePeerDependencies:
- '@testing-library/jest-dom'
@@ -11106,7 +11251,7 @@ snapshots:
untun: 0.1.3
uqr: 0.1.2
- '@vinxi/plugin-directives@0.5.1(vinxi@0.5.8(@types/node@24.10.1)(@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.51)(terser@5.44.1)(tsx@4.20.6))':
+ '@vinxi/plugin-directives@0.5.1(vinxi@0.5.8(@types/node@24.10.1)(@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.52)(terser@5.44.1)(tsx@4.20.6))':
dependencies:
'@babel/parser': 7.28.5
acorn: 8.15.0
@@ -11117,20 +11262,20 @@ snapshots:
magicast: 0.2.11
recast: 0.23.11
tslib: 2.8.1
- vinxi: 0.5.8(@types/node@24.10.1)(@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.51)(terser@5.44.1)(tsx@4.20.6)
+ vinxi: 0.5.8(@types/node@24.10.1)(@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.52)(terser@5.44.1)(tsx@4.20.6)
- '@vinxi/server-components@0.5.1(vinxi@0.5.8(@types/node@24.10.1)(@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.51)(terser@5.44.1)(tsx@4.20.6))':
+ '@vinxi/server-components@0.5.1(vinxi@0.5.8(@types/node@24.10.1)(@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.52)(terser@5.44.1)(tsx@4.20.6))':
dependencies:
- '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.8(@types/node@24.10.1)(@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.51)(terser@5.44.1)(tsx@4.20.6))
+ '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.8(@types/node@24.10.1)(@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.52)(terser@5.44.1)(tsx@4.20.6))
acorn: 8.15.0
acorn-loose: 8.5.2
acorn-typescript: 1.4.13(acorn@8.15.0)
astring: 1.9.0
magicast: 0.2.11
recast: 0.23.11
- vinxi: 0.5.8(@types/node@24.10.1)(@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.51)(terser@5.44.1)(tsx@4.20.6)
+ vinxi: 0.5.8(@types/node@24.10.1)(@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.52)(terser@5.44.1)(tsx@4.20.6)
- '@vitejs/plugin-react@5.1.1(rolldown-vite@7.2.7(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6))':
+ '@vitejs/plugin-react@5.1.1(rolldown-vite@7.2.8(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6))':
dependencies:
'@babel/core': 7.28.5
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5)
@@ -11138,7 +11283,7 @@ snapshots:
'@rolldown/pluginutils': 1.0.0-beta.47
'@types/babel__core': 7.20.5
react-refresh: 0.18.0
- vite: rolldown-vite@7.2.7(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)
+ vite: rolldown-vite@7.2.8(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)
transitivePeerDependencies:
- supports-color
@@ -13669,7 +13814,7 @@ snapshots:
- '@babel/core'
- babel-plugin-macros
- nitropack@2.12.9(@vercel/blob@0.27.3)(rolldown@1.0.0-beta.51):
+ nitropack@2.12.9(@vercel/blob@0.27.3)(rolldown@1.0.0-beta.52):
dependencies:
'@cloudflare/kv-asset-handler': 0.4.0
'@rollup/plugin-alias': 5.1.1(rollup@4.53.3)
@@ -13722,7 +13867,7 @@ snapshots:
pretty-bytes: 7.1.0
radix3: 1.1.2
rollup: 4.53.3
- rollup-plugin-visualizer: 6.0.5(rolldown@1.0.0-beta.51)(rollup@4.53.3)
+ rollup-plugin-visualizer: 6.0.5(rolldown@1.0.0-beta.52)(rollup@4.53.3)
scule: 1.3.0
semver: 7.7.3
serve-placeholder: 2.0.2
@@ -14387,14 +14532,14 @@ snapshots:
transitivePeerDependencies:
- oxc-resolver
- rolldown-vite@7.2.7(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6):
+ rolldown-vite@7.2.8(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6):
dependencies:
- '@oxc-project/runtime': 0.98.0
+ '@oxc-project/runtime': 0.99.0
fdir: 6.5.0(picomatch@4.0.3)
lightningcss: 1.30.2
picomatch: 4.0.3
postcss: 8.5.6
- rolldown: 1.0.0-beta.51
+ rolldown: 1.0.0-beta.52
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 24.10.1
@@ -14444,14 +14589,34 @@ snapshots:
'@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.51
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.51
- rollup-plugin-visualizer@6.0.5(rolldown@1.0.0-beta.51)(rollup@4.53.3):
+ rolldown@1.0.0-beta.52:
+ dependencies:
+ '@oxc-project/types': 0.99.0
+ '@rolldown/pluginutils': 1.0.0-beta.52
+ optionalDependencies:
+ '@rolldown/binding-android-arm64': 1.0.0-beta.52
+ '@rolldown/binding-darwin-arm64': 1.0.0-beta.52
+ '@rolldown/binding-darwin-x64': 1.0.0-beta.52
+ '@rolldown/binding-freebsd-x64': 1.0.0-beta.52
+ '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.52
+ '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.52
+ '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.52
+ '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.52
+ '@rolldown/binding-linux-x64-musl': 1.0.0-beta.52
+ '@rolldown/binding-openharmony-arm64': 1.0.0-beta.52
+ '@rolldown/binding-wasm32-wasi': 1.0.0-beta.52
+ '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.52
+ '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.52
+ '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.52
+
+ rollup-plugin-visualizer@6.0.5(rolldown@1.0.0-beta.52)(rollup@4.53.3):
dependencies:
open: 8.4.2
picomatch: 4.0.3
source-map: 0.7.6
yargs: 17.7.2
optionalDependencies:
- rolldown: 1.0.0-beta.51
+ rolldown: 1.0.0-beta.52
rollup: 4.53.3
rollup@4.53.3:
@@ -15280,7 +15445,7 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
- vinxi@0.5.8(@types/node@24.10.1)(@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.51)(terser@5.44.1)(tsx@4.20.6):
+ vinxi@0.5.8(@types/node@24.10.1)(@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.52)(terser@5.44.1)(tsx@4.20.6):
dependencies:
'@babel/core': 7.28.5
'@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5)
@@ -15301,7 +15466,7 @@ snapshots:
hookable: 5.5.3
http-proxy: 1.18.1
micromatch: 4.0.8
- nitropack: 2.12.9(@vercel/blob@0.27.3)(rolldown@1.0.0-beta.51)
+ nitropack: 2.12.9(@vercel/blob@0.27.3)(rolldown@1.0.0-beta.52)
node-fetch-native: 1.6.7
path-to-regexp: 6.3.0
pathe: 1.1.2
@@ -15376,13 +15541,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
- vite-tsconfig-paths@5.1.4(rolldown-vite@7.2.7(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6))(typescript@5.9.3):
+ vite-tsconfig-paths@5.1.4(rolldown-vite@7.2.8(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6))(typescript@5.9.3):
dependencies:
debug: 4.4.3
globrex: 0.1.2
tsconfck: 3.1.6(typescript@5.9.3)
optionalDependencies:
- vite: rolldown-vite@7.2.7(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)
+ vite: rolldown-vite@7.2.8(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)
transitivePeerDependencies:
- supports-color
- typescript
From 6cd230b66f3823dd227ead32d9aa746311dccaca Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Thu, 27 Nov 2025 18:54:26 +0500
Subject: [PATCH 13/48] refactor: Use `await Bun.file().text()` for
asynchronous file content reading in `onLoad` hook.
---
packages/bun-plugin/src/index.ts | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/packages/bun-plugin/src/index.ts b/packages/bun-plugin/src/index.ts
index bbcc6d685..a231a55f5 100644
--- a/packages/bun-plugin/src/index.ts
+++ b/packages/bun-plugin/src/index.ts
@@ -82,17 +82,16 @@ export default function arkenv>(
name: "@arkenv/bun-plugin",
setup(build) {
// Only process JavaScript/TypeScript source files
- build.onLoad({ filter: /\.(js|jsx|ts|tsx|mjs|cjs)$/ }, (args) => {
+ build.onLoad({ filter: /\.(js|jsx|ts|tsx|mjs|cjs)$/ }, async (args) => {
// Skip node_modules and other non-source files
if (args.path.includes("node_modules")) {
return undefined;
}
try {
- // Read the file contents synchronously
+ // Read the file contents
const file = Bun.file(args.path);
- const arrayBuffer = file.arrayBufferSync();
- const contents = new TextDecoder().decode(arrayBuffer);
+ const contents = await file.text();
// Replace process.env.VARIABLE patterns with validated values
let transformed = contents;
From 351ce5bc8a4dd93562cb074c0bc079c13678fcba Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Thu, 27 Nov 2025 19:14:53 +0500
Subject: [PATCH 14/48] feat: Centralize Bun React playground environment
variable definitions into a dedicated `src/env.ts` file and update plugin
configuration.
---
apps/playgrounds/bun-react/bin/build.ts | 8 ++------
apps/playgrounds/bun-react/bun-env.d.ts | 4 +++-
apps/playgrounds/bun-react/bunfig.toml | 2 +-
apps/playgrounds/bun-react/src/App.tsx | 5 +++--
apps/playgrounds/bun-react/src/env.ts | 6 ++++++
apps/playgrounds/bun-react/src/index.tsx | 8 --------
6 files changed, 15 insertions(+), 18 deletions(-)
create mode 100644 apps/playgrounds/bun-react/src/env.ts
diff --git a/apps/playgrounds/bun-react/bin/build.ts b/apps/playgrounds/bun-react/bin/build.ts
index 3b2e55ed6..756864a68 100644
--- a/apps/playgrounds/bun-react/bin/build.ts
+++ b/apps/playgrounds/bun-react/bin/build.ts
@@ -1,4 +1,5 @@
import arkenv from "@arkenv/bun-plugin";
+import Env from "@/env";
await Bun.build({
entrypoints: ["./src/index.html"],
@@ -9,10 +10,5 @@ await Bun.build({
define: {
"process.env.NODE_ENV": "production",
},
- plugins: [
- arkenv({
- BUN_PUBLIC_API_URL: "string",
- BUN_PUBLIC_DEBUG: "boolean",
- }),
- ],
+ plugins: [arkenv(Env)],
});
diff --git a/apps/playgrounds/bun-react/bun-env.d.ts b/apps/playgrounds/bun-react/bun-env.d.ts
index 7db01f321..2a7db0f44 100644
--- a/apps/playgrounds/bun-react/bun-env.d.ts
+++ b/apps/playgrounds/bun-react/bun-env.d.ts
@@ -4,7 +4,7 @@
// Note: In a real project, you might want to export Env from a separate env.ts file
// and import it like: typeof import("./env").Env
type ProcessEnvAugmented = import("@arkenv/bun-plugin").ProcessEnvAugmented<
- typeof import("./bun-env").Env
+ typeof import("./src/env").default
>;
declare global {
@@ -13,6 +13,8 @@ declare global {
}
}
+export {};
+
declare module "*.svg" {
/**
* A path to the SVG file
diff --git a/apps/playgrounds/bun-react/bunfig.toml b/apps/playgrounds/bun-react/bunfig.toml
index 42a89901c..9521dc191 100644
--- a/apps/playgrounds/bun-react/bunfig.toml
+++ b/apps/playgrounds/bun-react/bunfig.toml
@@ -1,3 +1,3 @@
[serve.static]
env = "BUN_PUBLIC_*"
-plugins = ["./bun-plugin-config.ts"]
\ No newline at end of file
+plugins = ["./bun-plugin-config.ts"]
diff --git a/apps/playgrounds/bun-react/src/App.tsx b/apps/playgrounds/bun-react/src/App.tsx
index afb0e83dd..d9cb50b21 100644
--- a/apps/playgrounds/bun-react/src/App.tsx
+++ b/apps/playgrounds/bun-react/src/App.tsx
@@ -18,8 +18,9 @@ export function App() {
);
}
diff --git a/apps/playgrounds/bun-react/src/index.css b/apps/playgrounds/bun-react/src/index.css
index 09eaa7e9c..593d4ddfd 100644
--- a/apps/playgrounds/bun-react/src/index.css
+++ b/apps/playgrounds/bun-react/src/index.css
@@ -185,3 +185,37 @@ code {
animation: none !important;
}
}
+
+.env-table {
+ width: 100%;
+ max-width: 600px;
+ margin: 2rem auto;
+ border-collapse: separate;
+ border-spacing: 0;
+ background: #1a1a1a;
+ border-radius: 12px;
+ overflow: hidden;
+ border: 2px solid #fbf0df;
+}
+
+.env-table th,
+.env-table td {
+ padding: 1rem;
+ text-align: left;
+ border-bottom: 1px solid rgba(251, 240, 223, 0.1);
+}
+
+.env-table th {
+ background: rgba(251, 240, 223, 0.1);
+ color: #fbf0df;
+ font-weight: 600;
+}
+
+.env-table td {
+ color: #e0e0e0;
+ font-family: monospace;
+}
+
+.env-table tr:last-child td {
+ border-bottom: none;
+}
From ddbc0083653a0e9b3c042839ac6c48924f83427c Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Thu, 27 Nov 2025 19:34:04 +0500
Subject: [PATCH 17/48] feat: Add specific public Bun environment variables and
refactor SVG/CSS module type declarations.
---
apps/playgrounds/bun-react/bun-env.d.ts | 23 +++++++----------------
1 file changed, 7 insertions(+), 16 deletions(-)
diff --git a/apps/playgrounds/bun-react/bun-env.d.ts b/apps/playgrounds/bun-react/bun-env.d.ts
index 2a7db0f44..6270f7509 100644
--- a/apps/playgrounds/bun-react/bun-env.d.ts
+++ b/apps/playgrounds/bun-react/bun-env.d.ts
@@ -1,32 +1,23 @@
///
// Import the Env schema type from env.ts
-// Note: In a real project, you might want to export Env from a separate env.ts file
-// and import it like: typeof import("./env").Env
type ProcessEnvAugmented = import("@arkenv/bun-plugin").ProcessEnvAugmented<
typeof import("./src/env").default
>;
-declare global {
- namespace NodeJS {
- interface ProcessEnv extends ProcessEnvAugmented {}
+declare namespace NodeJS {
+ interface ProcessEnv extends ProcessEnvAugmented {
+ BUN_PUBLIC_API_URL: string;
+ BUN_PUBLIC_DEBUG: boolean;
}
}
-export {};
-
declare module "*.svg" {
- /**
- * A path to the SVG file
- */
- const path: `${string}.svg`;
- export = path;
+ const content: string;
+ export default content;
}
declare module "*.module.css" {
- /**
- * A record of class names to their corresponding CSS module classes
- */
const classes: { readonly [key: string]: string };
- export = classes;
+ export default classes;
}
From 408248ab0029c57349d5de26dd40ddd3025d1850 Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Thu, 27 Nov 2025 19:34:32 +0500
Subject: [PATCH 18/48] feat: add type column for environment variables display
in the UI
---
apps/playgrounds/bun-react/src/App.tsx | 3 +++
1 file changed, 3 insertions(+)
diff --git a/apps/playgrounds/bun-react/src/App.tsx b/apps/playgrounds/bun-react/src/App.tsx
index 418fc74b8..b5debfeec 100644
--- a/apps/playgrounds/bun-react/src/App.tsx
+++ b/apps/playgrounds/bun-react/src/App.tsx
@@ -22,16 +22,19 @@ export function App() {
Variable
Value
+
Type
BUN_PUBLIC_API_URL
{process.env.BUN_PUBLIC_API_URL}
+
{typeof process.env.BUN_PUBLIC_API_URL}
BUN_PUBLIC_DEBUG
{String(process.env.BUN_PUBLIC_DEBUG)}
+
{typeof process.env.BUN_PUBLIC_DEBUG}
From 0651540fd8817d5dda0c3a1ee75094f16b606f29 Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Thu, 27 Nov 2025 19:44:37 +0500
Subject: [PATCH 19/48] feat: enable sourcemap generation in tsdown
configuration
---
packages/bun-plugin/tsdown.config.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/bun-plugin/tsdown.config.ts b/packages/bun-plugin/tsdown.config.ts
index 8fc08e8db..4bee4f713 100644
--- a/packages/bun-plugin/tsdown.config.ts
+++ b/packages/bun-plugin/tsdown.config.ts
@@ -4,6 +4,7 @@ export default defineConfig({
format: ["esm", "cjs"],
minify: true,
fixedExtension: false,
+ sourcemap: true,
dts: {
resolve: ["@repo/types"],
},
From 46fe73b166dc584ff19b1c405f286a8858201121 Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Thu, 27 Nov 2025 20:00:58 +0500
Subject: [PATCH 20/48] feat: Add `preview` script, display `NODE_ENV` in the
app, and remove explicit `NODE_ENV` build definition.
---
apps/playgrounds/bun-react/bin/build.ts | 3 ---
apps/playgrounds/bun-react/package.json | 1 +
apps/playgrounds/bun-react/src/App.tsx | 2 ++
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/apps/playgrounds/bun-react/bin/build.ts b/apps/playgrounds/bun-react/bin/build.ts
index 756864a68..e84cccdec 100644
--- a/apps/playgrounds/bun-react/bin/build.ts
+++ b/apps/playgrounds/bun-react/bin/build.ts
@@ -7,8 +7,5 @@ await Bun.build({
sourcemap: true,
target: "browser",
minify: true,
- define: {
- "process.env.NODE_ENV": "production",
- },
plugins: [arkenv(Env)],
});
diff --git a/apps/playgrounds/bun-react/package.json b/apps/playgrounds/bun-react/package.json
index 67db12d1c..8007b7758 100644
--- a/apps/playgrounds/bun-react/package.json
+++ b/apps/playgrounds/bun-react/package.json
@@ -6,6 +6,7 @@
"scripts": {
"dev": "bun --hot src/index.tsx",
"build": "bun bin/build.ts",
+ "preview": "bun dist/index.html",
"start": "NODE_ENV=production bun src/index.tsx",
"clean": "rimraf dist node_modules"
},
diff --git a/apps/playgrounds/bun-react/src/App.tsx b/apps/playgrounds/bun-react/src/App.tsx
index b5debfeec..0aa16de31 100644
--- a/apps/playgrounds/bun-react/src/App.tsx
+++ b/apps/playgrounds/bun-react/src/App.tsx
@@ -38,6 +38,8 @@ export function App() {
+ {/* Print whether we are in "build" or in the dev server */}
+
Mode: {process.env.NODE_ENV}
);
}
From f53d087d2fc600af596b2ab71cd9270a327400ec Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Thu, 27 Nov 2025 20:13:14 +0500
Subject: [PATCH 21/48] feat: Update Bun plugin to use named export and support
direct registration via package name in bunfig.toml.
---
apps/playgrounds/bun-react/bin/build.ts | 2 +-
apps/playgrounds/bun-react/bun-plugin-config.ts | 4 +++-
apps/playgrounds/bun-react/bunfig.toml | 2 +-
packages/bun-plugin/src/index.ts | 13 ++++++++++---
4 files changed, 15 insertions(+), 6 deletions(-)
diff --git a/apps/playgrounds/bun-react/bin/build.ts b/apps/playgrounds/bun-react/bin/build.ts
index e84cccdec..f78fd12cd 100644
--- a/apps/playgrounds/bun-react/bin/build.ts
+++ b/apps/playgrounds/bun-react/bin/build.ts
@@ -1,4 +1,4 @@
-import arkenv from "@arkenv/bun-plugin";
+import { arkenv } from "@arkenv/bun-plugin";
import Env from "@/env";
await Bun.build({
diff --git a/apps/playgrounds/bun-react/bun-plugin-config.ts b/apps/playgrounds/bun-react/bun-plugin-config.ts
index 5c44ba893..cee080ab1 100644
--- a/apps/playgrounds/bun-react/bun-plugin-config.ts
+++ b/apps/playgrounds/bun-react/bun-plugin-config.ts
@@ -1,4 +1,4 @@
-import arkenv from "@arkenv/bun-plugin";
+import { arkenv } from "@arkenv/bun-plugin";
// Configure the ArkEnv Bun plugin with your environment variable schema
// This file is referenced in bunfig.toml under [serve.static] plugins
@@ -6,3 +6,5 @@ export default arkenv({
BUN_PUBLIC_API_URL: "string",
BUN_PUBLIC_DEBUG: "boolean",
});
+
+console.log("HELLO!");
diff --git a/apps/playgrounds/bun-react/bunfig.toml b/apps/playgrounds/bun-react/bunfig.toml
index 9521dc191..339d48643 100644
--- a/apps/playgrounds/bun-react/bunfig.toml
+++ b/apps/playgrounds/bun-react/bunfig.toml
@@ -1,3 +1,3 @@
[serve.static]
env = "BUN_PUBLIC_*"
-plugins = ["./bun-plugin-config.ts"]
+plugins = ["@arkenv/bun-plugin"]
diff --git a/packages/bun-plugin/src/index.ts b/packages/bun-plugin/src/index.ts
index a71d2a968..37ec89d08 100644
--- a/packages/bun-plugin/src/index.ts
+++ b/packages/bun-plugin/src/index.ts
@@ -47,11 +47,11 @@ export type { ProcessEnvAugmented } from "./types";
* console.log(process.env.BUN_PUBLIC_API_URL); // Typesafe access
* ```
*/
-export default function arkenv>(
+export function arkenv>(
options: EnvSchema,
): BunPlugin;
-export default function arkenv(options: type.Any): BunPlugin;
-export default function arkenv>(
+export function arkenv(options: type.Any): BunPlugin;
+export function arkenv>(
options: EnvSchema | type.Any,
): BunPlugin {
// Validate environment variables at plugin initialization
@@ -135,3 +135,10 @@ export default function arkenv>(
},
} satisfies BunPlugin;
}
+
+const staticArkEnv: BunPlugin = {
+ name: "@arkenv/bun-plugin",
+ setup(build) {
+ console.log("Hello wolrd " + build.config.plugins);
+ },
+} satisfies BunPlugin;
From f3432a75a3b95c50f7e6b562485bde7c02ebc697 Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Thu, 27 Nov 2025 20:16:43 +0500
Subject: [PATCH 22/48] feat: Add default static ArkEnv plugin with automatic
schema discovery and refactor core logic into helper functions.
---
packages/bun-plugin/src/index.ts | 206 +++++++++++++++++++------------
1 file changed, 127 insertions(+), 79 deletions(-)
diff --git a/packages/bun-plugin/src/index.ts b/packages/bun-plugin/src/index.ts
index 37ec89d08..6f6cb508e 100644
--- a/packages/bun-plugin/src/index.ts
+++ b/packages/bun-plugin/src/index.ts
@@ -1,14 +1,92 @@
import type { EnvSchema } from "arkenv";
import { createEnv } from "arkenv";
import type { type } from "arktype";
-import type { BunPlugin } from "bun";
+import type { BunPlugin, Loader } from "bun";
+import { join } from "node:path";
export type { ProcessEnvAugmented } from "./types";
/**
- * 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`.
+ * Helper to create the onLoad handler
*/
+function createOnLoadHandler(envMap: Map) {
+ return async (args: any) => {
+ // Skip node_modules and other non-source files
+ if (args.path.includes("node_modules")) {
+ return undefined;
+ }
+
+ try {
+ // Read the file contents
+ const file = Bun.file(args.path);
+ const contents = await file.text();
+
+ // Replace process.env.VARIABLE patterns with validated values
+ let transformed = contents;
+
+ // Pattern 1: process.env.VARIABLE
+ // Pattern 2: process.env["VARIABLE"]
+ // Pattern 3: process.env['VARIABLE']
+ for (const [key, value] of envMap.entries()) {
+ // Replace process.env.KEY (word boundary to avoid partial matches)
+ const dotPattern = new RegExp(
+ `process\\.env\\.${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
+ "g",
+ );
+ transformed = transformed.replace(dotPattern, value);
+
+ // Replace process.env["KEY"] and process.env['KEY']
+ const bracketPattern = new RegExp(
+ `process\\.env\\[(["'])${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\1\\]`,
+ "g",
+ );
+ transformed = transformed.replace(bracketPattern, value);
+ }
+
+ // Determine loader based on file extension
+ const loader = (
+ args.path.endsWith(".tsx") || args.path.endsWith(".jsx")
+ ? "tsx"
+ : args.path.endsWith(".ts") || args.path.endsWith(".mts")
+ ? "ts"
+ : "js"
+ ) as Loader;
+
+ return {
+ loader,
+ contents: transformed,
+ };
+ } catch (error) {
+ // If file can't be read, return undefined to let Bun handle it
+ return undefined;
+ }
+ };
+}
+
+/**
+ * Helper to process env schema and return envMap
+ */
+function processEnvSchema(options: EnvSchema | type.Any) {
+ // Validate environment variables
+ const env = createEnv(options, process.env);
+
+ // Get Bun's prefix for client-exposed environment variables
+ const prefix = "BUN_PUBLIC_";
+
+ // Filter to only include environment variables matching the prefix
+ const filteredEnv = Object.fromEntries(
+ Object.entries(>env).filter(([key]) =>
+ key.startsWith(prefix),
+ ),
+ );
+
+ // Create a map of variable names to their JSON-stringified values
+ const envMap = new Map();
+ for (const [key, value] of Object.entries(filteredEnv)) {
+ envMap.set(key, JSON.stringify(value));
+ }
+ return envMap;
+}
/**
* Bun plugin to validate environment variables using ArkEnv and expose them to client code.
@@ -27,7 +105,7 @@ export type { ProcessEnvAugmented } from "./types";
* @example
* ```ts
* // bun.config.ts or in Bun.build()
- * import arkenv from '@arkenv/bun-plugin';
+ * import { arkenv } from '@arkenv/bun-plugin';
*
* await Bun.build({
* entrypoints: ['./app.tsx'],
@@ -54,91 +132,61 @@ export function arkenv(options: type.Any): BunPlugin;
export function arkenv>(
options: EnvSchema | type.Any,
): BunPlugin {
- // Validate environment variables at plugin initialization
- // This will throw if validation fails, preventing the build from starting
- const env = createEnv(options, process.env);
-
- // Get Bun's prefix for client-exposed environment variables
- // Defaults to "BUN_PUBLIC_" if not configured
- // Users can configure this in bunfig.toml: [serve.static] env = "BUN_PUBLIC_*"
- const prefix = "BUN_PUBLIC_";
-
- // Filter to only include environment variables matching the prefix
- // This prevents server-only variables from being exposed to client code
- const filteredEnv = Object.fromEntries(
- Object.entries(>env).filter(([key]) =>
- key.startsWith(prefix),
- ),
- );
-
- // Create a map of variable names to their JSON-stringified values
- // This will be used to replace process.env.VARIABLE patterns
- const envMap = new Map();
- for (const [key, value] of Object.entries(filteredEnv)) {
- envMap.set(key, JSON.stringify(value));
- }
+ const envMap = processEnvSchema(options);
return {
name: "@arkenv/bun-plugin",
setup(build) {
// Only process JavaScript/TypeScript source files
- build.onLoad({ filter: /\.(js|jsx|ts|tsx|mjs|cjs)$/ }, async (args) => {
- // Skip node_modules and other non-source files
- if (args.path.includes("node_modules")) {
- return undefined;
- }
-
- try {
- // Read the file contents
- const file = Bun.file(args.path);
- const contents = await file.text();
-
- // Replace process.env.VARIABLE patterns with validated values
- let transformed = contents;
-
- // Pattern 1: process.env.VARIABLE
- // Pattern 2: process.env["VARIABLE"]
- // Pattern 3: process.env['VARIABLE']
- for (const [key, value] of envMap.entries()) {
- // Replace process.env.KEY (word boundary to avoid partial matches)
- const dotPattern = new RegExp(
- `process\\.env\\.${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
- "g",
- );
- transformed = transformed.replace(dotPattern, value);
-
- // Replace process.env["KEY"] and process.env['KEY']
- const bracketPattern = new RegExp(
- `process\\.env\\[(["'])${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\1\\]`,
- "g",
- );
- transformed = transformed.replace(bracketPattern, value);
- }
-
- // Determine loader based on file extension
- const loader =
- args.path.endsWith(".tsx") || args.path.endsWith(".jsx")
- ? "tsx"
- : args.path.endsWith(".ts") || args.path.endsWith(".mts")
- ? "ts"
- : "js";
-
- return {
- loader,
- contents: transformed,
- };
- } catch (error) {
- // If file can't be read, return undefined to let Bun handle it
- return undefined;
- }
- });
+ build.onLoad(
+ { filter: /\.(js|jsx|ts|tsx|mjs|cjs)$/ },
+ createOnLoadHandler(envMap),
+ );
},
} satisfies BunPlugin;
}
+/**
+ * Static ArkEnv plugin that automatically finds and loads the environment schema
+ * from `./src/env.ts` or `./env.ts`.
+ */
const staticArkEnv: BunPlugin = {
name: "@arkenv/bun-plugin",
- setup(build) {
- console.log("Hello wolrd " + build.config.plugins);
+ async setup(build) {
+ const cwd = process.cwd();
+ let schema: any;
+
+ const possiblePaths = [join(cwd, "src", "env.ts"), join(cwd, "env.ts")];
+
+ for (const p of possiblePaths) {
+ if (await Bun.file(p).exists()) {
+ try {
+ const mod = await import(p);
+ if (mod.default) {
+ schema = mod.default;
+ break;
+ }
+ } catch (e) {
+ console.error(`Failed to load env schema from ${p}:`, e);
+ }
+ }
+ }
+
+ if (!schema) {
+ console.warn(
+ "No env schema found in src/env.ts or env.ts. Skipping @arkenv/bun-plugin validation.",
+ );
+ return;
+ }
+
+ const envMap = processEnvSchema(schema);
+
+ // Only process JavaScript/TypeScript source files
+ build.onLoad(
+ { filter: /\.(js|jsx|ts|tsx|mjs|cjs)$/ },
+ createOnLoadHandler(envMap),
+ );
},
} satisfies BunPlugin;
+
+export default staticArkEnv;
From d0beeba4176a6b3b771b638383b089104f455585 Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Thu, 27 Nov 2025 20:16:52 +0500
Subject: [PATCH 23/48] chore: Delete ArkEnv Bun plugin configuration file.
---
apps/playgrounds/bun-react/bun-plugin-config.ts | 10 ----------
1 file changed, 10 deletions(-)
delete mode 100644 apps/playgrounds/bun-react/bun-plugin-config.ts
diff --git a/apps/playgrounds/bun-react/bun-plugin-config.ts b/apps/playgrounds/bun-react/bun-plugin-config.ts
deleted file mode 100644
index cee080ab1..000000000
--- a/apps/playgrounds/bun-react/bun-plugin-config.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { arkenv } from "@arkenv/bun-plugin";
-
-// Configure the ArkEnv Bun plugin with your environment variable schema
-// This file is referenced in bunfig.toml under [serve.static] plugins
-export default arkenv({
- BUN_PUBLIC_API_URL: "string",
- BUN_PUBLIC_DEBUG: "boolean",
-});
-
-console.log("HELLO!");
From 7b75358700ffcfea82b7b808b2cbe3ad4d50cef6 Mon Sep 17 00:00:00 2001
From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com>
Date: Thu, 27 Nov 2025 15:17:36 +0000
Subject: [PATCH 24/48] [autofix.ci] apply automated fixes
---
packages/bun-plugin/src/index.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/bun-plugin/src/index.ts b/packages/bun-plugin/src/index.ts
index 6f6cb508e..2cf20c8e9 100644
--- a/packages/bun-plugin/src/index.ts
+++ b/packages/bun-plugin/src/index.ts
@@ -1,8 +1,8 @@
+import { join } from "node:path";
import type { EnvSchema } from "arkenv";
import { createEnv } from "arkenv";
import type { type } from "arktype";
import type { BunPlugin, Loader } from "bun";
-import { join } from "node:path";
export type { ProcessEnvAugmented } from "./types";
From 08fcca6f2cba1383377ba1da76258f347eac24d7 Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Thu, 27 Nov 2025 20:28:38 +0500
Subject: [PATCH 25/48] feat: Refactor `arkenv` Bun plugin to support hybrid
zero-config and explicit schema usage.
---
apps/playgrounds/bun-react/bin/build.ts | 5 +-
packages/bun-plugin/src/index.ts | 122 ++++++++++--------------
2 files changed, 55 insertions(+), 72 deletions(-)
diff --git a/apps/playgrounds/bun-react/bin/build.ts b/apps/playgrounds/bun-react/bin/build.ts
index f78fd12cd..5f9e4c7fc 100644
--- a/apps/playgrounds/bun-react/bin/build.ts
+++ b/apps/playgrounds/bun-react/bin/build.ts
@@ -1,5 +1,4 @@
-import { arkenv } from "@arkenv/bun-plugin";
-import Env from "@/env";
+import arkenv from "@arkenv/bun-plugin";
await Bun.build({
entrypoints: ["./src/index.html"],
@@ -7,5 +6,5 @@ await Bun.build({
sourcemap: true,
target: "browser",
minify: true,
- plugins: [arkenv(Env)],
+ plugins: [arkenv],
});
diff --git a/packages/bun-plugin/src/index.ts b/packages/bun-plugin/src/index.ts
index 2cf20c8e9..c35e395dc 100644
--- a/packages/bun-plugin/src/index.ts
+++ b/packages/bun-plugin/src/index.ts
@@ -91,39 +91,23 @@ function processEnvSchema(options: EnvSchema | type.Any) {
/**
* Bun plugin to validate environment variables using ArkEnv and expose them to client code.
*
- * The plugin validates environment variables using ArkEnv's schema validation and
- * automatically filters them based on Bun's prefix configuration (defaults to `"BUN_PUBLIC_"`).
- * Only environment variables matching the prefix are exposed to client code via `process.env.*`.
+ * This is a hybrid export that can be used in two ways:
*
- * The plugin uses Bun's `onLoad` hook to statically replace `process.env.VARIABLE` patterns
- * with validated, transformed values during bundling.
+ * 1. **Zero-config (Static Analysis)**:
+ * Add it directly to `bunfig.toml`. It will automatically look for `./src/env.ts` or `./env.ts`.
+ * ```toml
+ * [serve.static]
+ * plugins = ["@arkenv/bun-plugin"]
+ * ```
*
- * @param options - The environment variable schema definition. Can be an `EnvSchema` object
- * for typesafe validation or an ArkType `type.Any` for dynamic schemas.
- * @returns A Bun plugin that validates environment variables and exposes them to the client.
- *
- * @example
- * ```ts
- * // bun.config.ts or in Bun.build()
- * import { arkenv } from '@arkenv/bun-plugin';
- *
- * await Bun.build({
- * entrypoints: ['./app.tsx'],
- * outdir: './dist',
- * plugins: [
- * arkenv({
- * BUN_PUBLIC_API_URL: 'string',
- * BUN_PUBLIC_DEBUG: 'boolean',
- * }),
- * ],
- * });
- * ```
- *
- * @example
- * ```ts
- * // In your client code
- * console.log(process.env.BUN_PUBLIC_API_URL); // Typesafe access
- * ```
+ * 2. **Manual Configuration**:
+ * Call it as a function in `Bun.build` with your schema.
+ * ```ts
+ * import arkenv from "@arkenv/bun-plugin";
+ * Bun.build({
+ * plugins: [arkenv(MySchema)]
+ * })
+ * ```
*/
export function arkenv>(
options: EnvSchema,
@@ -137,7 +121,6 @@ export function arkenv>(
return {
name: "@arkenv/bun-plugin",
setup(build) {
- // Only process JavaScript/TypeScript source files
build.onLoad(
{ filter: /\.(js|jsx|ts|tsx|mjs|cjs)$/ },
createOnLoadHandler(envMap),
@@ -146,47 +129,48 @@ export function arkenv>(
} satisfies BunPlugin;
}
-/**
- * Static ArkEnv plugin that automatically finds and loads the environment schema
- * from `./src/env.ts` or `./env.ts`.
- */
-const staticArkEnv: BunPlugin = {
- name: "@arkenv/bun-plugin",
- async setup(build) {
- const cwd = process.cwd();
- let schema: any;
-
- const possiblePaths = [join(cwd, "src", "env.ts"), join(cwd, "env.ts")];
-
- for (const p of possiblePaths) {
- if (await Bun.file(p).exists()) {
- try {
- const mod = await import(p);
- if (mod.default) {
- schema = mod.default;
- break;
- }
- } catch (e) {
- console.error(`Failed to load env schema from ${p}:`, e);
+// 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"]`
+const hybrid = arkenv as typeof arkenv & BunPlugin;
+
+Object.defineProperty(hybrid, "name", {
+ value: "@arkenv/bun-plugin",
+ writable: false,
+});
+
+hybrid.setup = async (build) => {
+ const cwd = process.cwd();
+ let schema: any;
+
+ const possiblePaths = [join(cwd, "src", "env.ts"), join(cwd, "env.ts")];
+
+ for (const p of possiblePaths) {
+ if (await Bun.file(p).exists()) {
+ try {
+ const mod = await import(p);
+ if (mod.default) {
+ schema = mod.default;
+ break;
}
+ } catch (e) {
+ console.error(`Failed to load env schema from ${p}:`, e);
}
}
+ }
- if (!schema) {
- console.warn(
- "No env schema found in src/env.ts or env.ts. Skipping @arkenv/bun-plugin validation.",
- );
- return;
- }
+ if (!schema) {
+ console.warn(
+ "No env schema found in src/env.ts or env.ts. Skipping @arkenv/bun-plugin validation.",
+ );
+ return;
+ }
- const envMap = processEnvSchema(schema);
+ const envMap = processEnvSchema(schema);
- // Only process JavaScript/TypeScript source files
- build.onLoad(
- { filter: /\.(js|jsx|ts|tsx|mjs|cjs)$/ },
- createOnLoadHandler(envMap),
- );
- },
-} satisfies BunPlugin;
+ build.onLoad(
+ { filter: /\.(js|jsx|ts|tsx|mjs|cjs)$/ },
+ createOnLoadHandler(envMap),
+ );
+};
-export default staticArkEnv;
+export default hybrid;
From bfe0a9e879442aa26ed17b61659df40bf40a90a4 Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Thu, 27 Nov 2025 20:40:33 +0500
Subject: [PATCH 26/48] feat: configure Bun build to define
process.env.NODE_ENV as production
---
apps/playgrounds/bun-react/bin/build.ts | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/apps/playgrounds/bun-react/bin/build.ts b/apps/playgrounds/bun-react/bin/build.ts
index 5f9e4c7fc..05765f20c 100644
--- a/apps/playgrounds/bun-react/bin/build.ts
+++ b/apps/playgrounds/bun-react/bin/build.ts
@@ -7,4 +7,8 @@ await Bun.build({
target: "browser",
minify: true,
plugins: [arkenv],
+ // define NODE_ENV=production
+ define: {
+ "process.env.NODE_ENV": '"production"',
+ },
});
From f7abcbef99025e6427ca0dae383dba16aa32a6cc Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Thu, 27 Nov 2025 22:57:00 +0500
Subject: [PATCH 27/48] refactor: Initialize Bun plugin environment schema
using `build.onStart` and refactor internal helpers.
---
apps/playgrounds/bun-react/src/index.tsx | 4 -
packages/bun-plugin/src/index.ts | 178 +++++++++++++++--------
2 files changed, 114 insertions(+), 68 deletions(-)
diff --git a/apps/playgrounds/bun-react/src/index.tsx b/apps/playgrounds/bun-react/src/index.tsx
index 3ae5570cd..05980fe74 100644
--- a/apps/playgrounds/bun-react/src/index.tsx
+++ b/apps/playgrounds/bun-react/src/index.tsx
@@ -1,10 +1,6 @@
import { serve } from "bun";
import index from "./index.html";
-// Note: Plugins are also configured in bunfig.toml under [serve.static]
-// for bundling client code. The validation above ensures env vars are present
-// before the server starts.
-
const server = serve({
routes: {
// Serve index.html for all unmatched routes.
diff --git a/packages/bun-plugin/src/index.ts b/packages/bun-plugin/src/index.ts
index c35e395dc..9b7cf13e5 100644
--- a/packages/bun-plugin/src/index.ts
+++ b/packages/bun-plugin/src/index.ts
@@ -2,15 +2,40 @@ import { join } from "node:path";
import type { EnvSchema } from "arkenv";
import { createEnv } from "arkenv";
import type { type } from "arktype";
-import type { BunPlugin, Loader } from "bun";
+import type { BunPlugin, Loader, PluginBuilder } from "bun";
export type { ProcessEnvAugmented } from "./types";
/**
- * Helper to create the onLoad handler
+ * Helper to process env schema and return envMap
+ */
+function processEnvSchema(options: EnvSchema | type.Any) {
+ // Validate environment variables
+ const env = createEnv(options, process.env);
+
+ // Get Bun's prefix for client-exposed environment variables
+ const prefix = "BUN_PUBLIC_";
+
+ // Filter to only include environment variables matching the prefix
+ const filteredEnv = Object.fromEntries(
+ Object.entries(>env).filter(([key]) =>
+ key.startsWith(prefix),
+ ),
+ );
+
+ // Create a map of variable names to their JSON-stringified values
+ const envMap = new Map();
+ for (const [key, value] of Object.entries(filteredEnv)) {
+ envMap.set(key, JSON.stringify(value));
+ }
+ return envMap;
+}
+
+/**
+ * Helper to register the onLoad handler
*/
-function createOnLoadHandler(envMap: Map) {
- return async (args: any) => {
+function registerLoader(build: PluginBuilder, envMap: Map) {
+ build.onLoad({ filter: /\.(js|jsx|ts|tsx|mjs|cjs)$/ }, async (args) => {
// Skip node_modules and other non-source files
if (args.path.includes("node_modules")) {
return undefined;
@@ -60,52 +85,37 @@ function createOnLoadHandler(envMap: Map) {
// If file can't be read, return undefined to let Bun handle it
return undefined;
}
- };
-}
-
-/**
- * Helper to process env schema and return envMap
- */
-function processEnvSchema(options: EnvSchema | type.Any) {
- // Validate environment variables
- const env = createEnv(options, process.env);
-
- // Get Bun's prefix for client-exposed environment variables
- const prefix = "BUN_PUBLIC_";
-
- // Filter to only include environment variables matching the prefix
- const filteredEnv = Object.fromEntries(
- Object.entries(>env).filter(([key]) =>
- key.startsWith(prefix),
- ),
- );
-
- // Create a map of variable names to their JSON-stringified values
- const envMap = new Map();
- for (const [key, value] of Object.entries(filteredEnv)) {
- envMap.set(key, JSON.stringify(value));
- }
- return envMap;
+ });
}
/**
- * Bun plugin to validate environment variables using ArkEnv and expose them to client code.
+ * Bun plugin to validate environment variables using ArkEnv and expose prefixed variables to client code.
*
- * This is a hybrid export that can be used in two ways:
+ * You can use this in one of two ways:
*
* 1. **Zero-config (Static Analysis)**:
- * Add it directly to `bunfig.toml`. It will automatically look for `./src/env.ts` or `./env.ts`.
+ * Automatically looks for `./src/env.ts` or `./env.ts`.
+ *
+ * In `bunfig.toml`:
* ```toml
* [serve.static]
* plugins = ["@arkenv/bun-plugin"]
* ```
+ * and in `Bun.build`:
+ * ```ts
+ * import arkenv from "@arkenv/bun-plugin";
+ * Bun.build({
+ * plugins: [arkenv]
+ * })
+ * ```
*
* 2. **Manual Configuration**:
* Call it as a function in `Bun.build` with your schema.
* ```ts
* import arkenv from "@arkenv/bun-plugin";
+ * import { Env } from "./src/env";
* Bun.build({
- * plugins: [arkenv(MySchema)]
+ * plugins: [arkenv(Env)]
* })
* ```
*/
@@ -121,16 +131,44 @@ export function arkenv>(
return {
name: "@arkenv/bun-plugin",
setup(build) {
- build.onLoad(
- { filter: /\.(js|jsx|ts|tsx|mjs|cjs)$/ },
- createOnLoadHandler(envMap),
- );
+ registerLoader(build, envMap);
},
} satisfies BunPlugin;
}
// 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"]`
+/**
+ * Bun plugin to validate environment variables using ArkEnv and expose prefixed variables to client code.
+ *
+ * You can use this in one of two ways:
+ *
+ * 1. **Zero-config (Static Analysis)**:
+ * Automatically looks for `./src/env.ts` or `./env.ts`.
+ *
+ * In `bunfig.toml`:
+ * ```toml
+ * [serve.static]
+ * plugins = ["@arkenv/bun-plugin"]
+ * ```
+ * and in `Bun.build`:
+ * ```ts
+ * import arkenv from "@arkenv/bun-plugin";
+ * Bun.build({
+ * plugins: [arkenv]
+ * })
+ * ```
+ *
+ * 2. **Manual Configuration**:
+ * Call it as a function in `Bun.build` with your schema.
+ * ```ts
+ * import arkenv from "@arkenv/bun-plugin";
+ * import { Env } from "./src/env";
+ * Bun.build({
+ * plugins: [arkenv(Env)]
+ * })
+ * ```
+ */
const hybrid = arkenv as typeof arkenv & BunPlugin;
Object.defineProperty(hybrid, "name", {
@@ -138,39 +176,51 @@ Object.defineProperty(hybrid, "name", {
writable: false,
});
-hybrid.setup = async (build) => {
- const cwd = process.cwd();
- let schema: any;
-
- const possiblePaths = [join(cwd, "src", "env.ts"), join(cwd, "env.ts")];
+hybrid.setup = (build) => {
+ const envMap = new Map();
- for (const p of possiblePaths) {
- if (await Bun.file(p).exists()) {
- try {
- const mod = await import(p);
- if (mod.default) {
- schema = mod.default;
- break;
+ build.onStart(async () => {
+ const cwd = process.cwd();
+ let schema: any;
+
+ const possiblePaths = [join(cwd, "src", "env.ts"), join(cwd, "env.ts")];
+
+ for (const p of possiblePaths) {
+ if (await Bun.file(p).exists()) {
+ try {
+ // Invalidate cache to support hot reloading of the schema file itself
+ // Note: Bun's require cache invalidation might be needed if using require
+ // For ESM import(), we might need a cache busting query param or similar if Bun caches it aggressively
+ // However, for now, we'll try standard import.
+ // To truly support hot reload of config, we might need to rely on Bun's watcher restarting the process
+ // when bunfig.toml or dependencies change.
+ const mod = await import(p);
+ if (mod.default) {
+ schema = mod.default;
+ break;
+ }
+ } catch (e) {
+ console.error(`Failed to load env schema from ${p}:`, e);
}
- } catch (e) {
- console.error(`Failed to load env schema from ${p}:`, e);
}
}
- }
- if (!schema) {
- console.warn(
- "No env schema found in src/env.ts or env.ts. Skipping @arkenv/bun-plugin validation.",
- );
- return;
- }
+ if (!schema) {
+ console.warn(
+ "No env schema found in src/env.ts or env.ts. Skipping @arkenv/bun-plugin validation.",
+ );
+ return;
+ }
- const envMap = processEnvSchema(schema);
+ // Update the shared envMap with new values
+ const newEnvMap = processEnvSchema(schema);
+ envMap.clear();
+ for (const [k, v] of newEnvMap) {
+ envMap.set(k, v);
+ }
+ });
- build.onLoad(
- { filter: /\.(js|jsx|ts|tsx|mjs|cjs)$/ },
- createOnLoadHandler(envMap),
- );
+ registerLoader(build, envMap);
};
export default hybrid;
From 3a590059501d519fbbbe78644c0a2b560ae8a936 Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Thu, 27 Nov 2025 23:00:11 +0500
Subject: [PATCH 28/48] feat: introduce a hybrid export for the Bun plugin,
clarify usage patterns, and simplify schema discovery.
---
openspec/changes/add-bun-plugin/design.md | 55 ++++++++++++++---------
1 file changed, 34 insertions(+), 21 deletions(-)
diff --git a/openspec/changes/add-bun-plugin/design.md b/openspec/changes/add-bun-plugin/design.md
index 8093da630..64f492027 100644
--- a/openspec/changes/add-bun-plugin/design.md
+++ b/openspec/changes/add-bun-plugin/design.md
@@ -54,14 +54,24 @@ A third, more advanced mode using a custom static plugin file may be added later
**Implementation**:
-**Pattern 1: Bun.build (Direct Reference)**
+
+
+**Unified Hybrid Export**
+
+The plugin uses a "Hybrid Export" pattern. The default export is both:
+1. A **Plugin Factory Function**: `arkenv(schema)` for manual configuration.
+2. A **Plugin Object**: `{ name, setup }` for zero-config static analysis.
+
+This allows a single import to serve both use cases without friction.
+
+**Pattern 1: Bun.build (Manual Configuration)**
```ts
// build.ts
-import { defineEnv } from "arkenv";
-import { createArkEnvBunPlugin } from "@arkenv/bun-plugin";
+import arkenv from "@arkenv/bun-plugin";
+import { type } from "arkenv";
-const env = defineEnv({
+const env = type({
BUN_PUBLIC_API_URL: "string",
BUN_PUBLIC_DEBUG: "boolean",
});
@@ -69,39 +79,42 @@ const env = defineEnv({
await Bun.build({
entrypoints: ["./app.tsx"],
outdir: "./dist",
- plugins: [createArkEnvBunPlugin(env)],
+ plugins: [arkenv(env)], // Call as function
});
```
-**Pattern 2: Bun.serve (Package Reference with Convention)**
+**Pattern 2: Bun.serve / bunfig.toml (Zero-Config)**
```toml
# bunfig.toml
[serve.static]
-env = "BUN_PUBLIC_*"
-plugins = ["bun-plugin-arkenv"]
+plugins = ["@arkenv/bun-plugin"]
```
-With a schema file at a conventional path, for example:
+**Pattern 3: Bun.build (Zero-Config)**
+
+Because the default export is also a valid plugin object, it can be passed directly to `Bun.build` to trigger auto-discovery:
```ts
-// src/env.ts
-import { defineEnv } from "arkenv";
+import arkenv from "@arkenv/bun-plugin";
-const env = defineEnv({
- BUN_PUBLIC_API_URL: "string",
- BUN_PUBLIC_DEBUG: "boolean",
+await Bun.build({
+ // ...
+ plugins: [arkenv], // Pass as object
});
-
-export default env;
```
-At startup, `bun-plugin-arkenv`:
+**Schema Discovery**
+
+In zero-config mode, the plugin searches for:
+1. `./src/env.ts`
+2. `./env.ts`
+
+It expects a `default` export containing the ArkEnv schema.
+
+**Hot Reloading**
-- Searches for a schema file in a small set of well-known locations (for example: `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`).
-- Imports the first one it finds.
-- Reads the default export (or an `env` named export) as the ArkEnv schema.
-- Creates a Bun plugin via `createArkEnvBunPlugin(schema)` and registers it with `Bun.plugin(...)`.
+The plugin uses the `onStart` hook to load/reload the schema at the beginning of every build/dev cycle. This ensures that changes to `env.ts` are picked up (provided the file watcher triggers a rebuild).
If no schema file is found, or the module does not export a usable schema, the plugin fails fast with a clear error message that lists the paths checked and shows a minimal example.
From e3c82b3a2e57b131b5df13dd96ed6373fae95afd Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Thu, 27 Nov 2025 23:38:40 +0500
Subject: [PATCH 29/48] feat: Add Bun plugin documentation and update the
install button to support the new plugin page.
---
apps/www/app/layout.tsx | 15 +---
apps/www/components/banner.tsx | 19 +++++
apps/www/components/docs/install-button.tsx | 22 +++---
apps/www/content/docs/bun-plugin/index.mdx | 78 +++++++++++++++++++++
apps/www/content/docs/bun-plugin/meta.json | 7 ++
apps/www/content/docs/meta.json | 2 +-
6 files changed, 121 insertions(+), 22 deletions(-)
create mode 100644 apps/www/components/banner.tsx
create mode 100644 apps/www/content/docs/bun-plugin/index.mdx
create mode 100644 apps/www/content/docs/bun-plugin/meta.json
diff --git a/apps/www/app/layout.tsx b/apps/www/app/layout.tsx
index a51daff8d..967b317d6 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 "~/components/banner";
import "./globals.css";
import { Analytics } from "@vercel/analytics/next";
import { SpeedInsights } from "@vercel/speed-insights/next";
@@ -48,18 +48,7 @@ export default function Layout({ children }: { children: ReactNode }) {
enableSystem: true,
}}
>
-
- 🎉 Announcing Vite support: check out the
-
- new docs
-
- !
-
+
{children}
diff --git a/apps/www/components/banner.tsx b/apps/www/components/banner.tsx
new file mode 100644
index 000000000..cd79057b6
--- /dev/null
+++ b/apps/www/components/banner.tsx
@@ -0,0 +1,19 @@
+"use client";
+
+import { Banner as FumadocsBanner } from "fumadocs-ui/components/banner";
+import { useRouter } from "next/navigation";
+
+export function Banner() {
+ const router = useRouter();
+ return (
+ router.push("/docs/bun-plugin")}
+ className="cursor-pointer"
+ >
+ 🎉 Official Bun support is here: ArkEnv at build time, fullstack dev
+ server
+
+ );
+}
diff --git a/apps/www/components/docs/install-button.tsx b/apps/www/components/docs/install-button.tsx
index 278d10014..60387bdd0 100644
--- a/apps/www/components/docs/install-button.tsx
+++ b/apps/www/components/docs/install-button.tsx
@@ -8,18 +8,24 @@ import { Button } from "~/components/ui/button";
export function InstallButton() {
const pathname = usePathname();
const isVitePluginPage = pathname?.includes("/docs/vite-plugin");
+ const isBunPluginPage = pathname?.includes("/docs/bun-plugin");
+
+ let href = "/docs/arkenv/quickstart#install";
+ let label = "Install ArkEnv";
+
+ if (isVitePluginPage) {
+ href = "/docs/vite-plugin#installation";
+ label = "Install ArkEnv Vite plugin";
+ } else if (isBunPluginPage) {
+ href = "/docs/bun-plugin#installation";
+ label = "Install ArkEnv Bun plugin";
+ }
return (
);
diff --git a/apps/www/content/docs/bun-plugin/index.mdx b/apps/www/content/docs/bun-plugin/index.mdx
new file mode 100644
index 000000000..effdaa569
--- /dev/null
+++ b/apps/www/content/docs/bun-plugin/index.mdx
@@ -0,0 +1,78 @@
+---
+title: Introduction
+icon: Album
+---
+
+## What is this?
+
+The Bun plugin for ArkEnv lets you validate environment variables at build-time and runtime with ArkEnv.
+
+It supports **zero-config** usage for most projects, automatically discovering your schema from `./src/env.ts` or `./env.ts`.
+
+## Usage
+
+### Zero-Config (Recommended)
+
+Add the plugin to your `bunfig.toml`. This works for both `bun run dev` and `bun run build`.
+
+```toml title="bunfig.toml"
+[serve.static]
+plugins = ["@arkenv/bun-plugin"]
+```
+
+Create your schema in `src/env.ts` (or `env.ts`):
+
+```ts title="src/env.ts"
+import { type } from "arkenv";
+
+export default type({
+ BUN_PUBLIC_API_URL: "string",
+ BUN_PUBLIC_DEBUG: "boolean",
+});
+```
+
+### Manual Configuration
+
+If you prefer explicit configuration or need to customize the schema location, you can use the plugin in your build script:
+
+```ts title="build.ts"
+import arkenv from "@arkenv/bun-plugin";
+import { type } from "arkenv";
+
+const env = type({
+ BUN_PUBLIC_API_URL: "string",
+ BUN_PUBLIC_DEBUG: "boolean",
+});
+
+await Bun.build({
+ entrypoints: ["./app.tsx"],
+ outdir: "./dist",
+ plugins: [arkenv(env)],
+});
+```
+
+### Zero-Config in Build Scripts
+
+You can also use the zero-config pattern in build scripts by passing the plugin object directly:
+
+```ts title="build.ts"
+import arkenv from "@arkenv/bun-plugin";
+
+await Bun.build({
+ // ...
+ plugins: [arkenv], // Auto-discovers src/env.ts
+});
+```
+
+## Features
+
+- **Static Analysis**: Automatically replaces `process.env.VARIABLE` with validated values during the build.
+- **Type Safety**: Ensures your code only accesses variables defined in your schema.
+- **Hot Reloading**: Automatically reloads your schema when `env.ts` changes (in `bun --hot` mode).
+- **Secure Defaults**: Only exposes variables prefixed with `BUN_PUBLIC_` to the client bundle (configurable via Bun's standard behavior).
+
+## Installation
+
+```package-install
+@arkenv/bun-plugin arkenv arktype
+```
diff --git a/apps/www/content/docs/bun-plugin/meta.json b/apps/www/content/docs/bun-plugin/meta.json
new file mode 100644
index 000000000..c8e4e0eb6
--- /dev/null
+++ b/apps/www/content/docs/bun-plugin/meta.json
@@ -0,0 +1,7 @@
+{
+ "title": "@arkenv/bun-plugin",
+ "description": "The Bun plugin",
+ "root": true,
+ "icon": "Bun",
+ "pages": ["---Guide---", "index"]
+}
diff --git a/apps/www/content/docs/meta.json b/apps/www/content/docs/meta.json
index 1f4b8cd3a..6f40ecac2 100644
--- a/apps/www/content/docs/meta.json
+++ b/apps/www/content/docs/meta.json
@@ -1,3 +1,3 @@
{
- "pages": ["arkenv", "vite-plugin"]
+ "pages": ["arkenv", "vite-plugin", "bun-plugin"]
}
From 9b81288de9e503d84fe7d62785880af0b777ccc9 Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Thu, 27 Nov 2025 23:45:24 +0500
Subject: [PATCH 30/48] refactor: Remove Vite logger support proposal files and
add arkenv alias to bun-plugin vitest config.
---
.../changes/add-vite-logger-support/design.md | 130 ------------------
.../add-vite-logger-support/proposal.md | 31 -----
.../specs/error-formatting/spec.md | 34 -----
.../specs/vite-plugin/spec.md | 36 -----
.../changes/add-vite-logger-support/tasks.md | 58 --------
packages/bun-plugin/vitest.config.ts | 1 +
packages/vite-plugin/src/logger-adapter.ts | 59 --------
7 files changed, 1 insertion(+), 348 deletions(-)
delete mode 100644 openspec/changes/add-vite-logger-support/design.md
delete mode 100644 openspec/changes/add-vite-logger-support/proposal.md
delete mode 100644 openspec/changes/add-vite-logger-support/specs/error-formatting/spec.md
delete mode 100644 openspec/changes/add-vite-logger-support/specs/vite-plugin/spec.md
delete mode 100644 openspec/changes/add-vite-logger-support/tasks.md
delete mode 100644 packages/vite-plugin/src/logger-adapter.ts
diff --git a/openspec/changes/add-vite-logger-support/design.md b/openspec/changes/add-vite-logger-support/design.md
deleted file mode 100644
index 40d53b692..000000000
--- a/openspec/changes/add-vite-logger-support/design.md
+++ /dev/null
@@ -1,130 +0,0 @@
-# Design: Pluggable Logger Support
-
-## Context
-
-The Vite plugin currently uses ArkEnv's `ArkEnvError` which formats errors using ANSI color codes via `styleText`. Vite provides a built-in logger (using picocolors internally) that should be used for better integration with Vite's build output. The goal is to make the implementation minimal on the Vite plugin side while supporting pluggable loggers in the core library for future extensibility.
-
-## Goals / Non-Goals
-
-### Goals
-- Support Vite's logger API for error formatting in the Vite plugin
-- Maintain backward compatibility (logger is optional, defaults to current ANSI behavior)
-- Keep Vite plugin implementation minimal (pass logger from Vite to core library)
-- Design extensible logger interface for future logger support (e.g., other build tools)
-- Zero new external dependencies
-
-### Non-Goals
-- Supporting all possible logger APIs (focus on Vite's logger first)
-- Changing the error message format (only the styling mechanism changes)
-- Adding logger support to browser environments (logger only applies in Node/build environments)
-
-## Decisions
-
-### Decision: Logger Abstraction Interface
-
-**What**: Define a simple logger interface that accepts a color name and text, returning styled text.
-
-**Why**: This abstraction allows plugging in different loggers (Vite's logger, future loggers) while keeping the core library decoupled from specific logger implementations.
-
-**Alternatives considered**:
-1. **Direct picocolors dependency**: Would add an external dependency and doesn't align with zero-dependency goal
-2. **Pass full logger object**: Too complex, we only need the styling functionality
-3. **No abstraction**: Would require custom code in Vite plugin, violating minimal implementation goal
-
-**Implementation**:
-```typescript
-type LoggerStyle = (color: "red" | "yellow" | "cyan", text: string) => string;
-```
-
-### Decision: Optional Logger Parameter
-
-**What**: Make logger parameter optional throughout the codebase, defaulting to current `styleText` behavior.
-
-**Why**: Maintains backward compatibility and allows gradual adoption. Existing code continues to work without changes.
-
-**Alternatives considered**:
-1. **Required logger**: Would be a breaking change
-2. **Global logger configuration**: Adds complexity and global state
-
-**Implementation**: Logger is optional at all levels:
-- `styleText(color, text, logger?)`
-- `formatErrors(errors, logger?)`
-- `ArkEnvError(errors, message?, logger?)`
-- `createEnv(schema, env, logger?)`
-
-### Decision: Vite Logger Adapter
-
-**What**: Create a thin adapter function that converts Vite's logger API to our logger interface.
-
-**Why**: Vite's logger has methods like `logger.error()`, `logger.warn()`, `logger.info()` that accept strings. We need to extract the styling functionality (picocolors) from these methods. The adapter provides a clean interface conversion.
-
-**Alternatives considered**:
-1. **Direct picocolors usage**: Would require adding picocolors as a dependency
-2. **String manipulation**: Too fragile, doesn't leverage Vite's logger properly
-3. **Custom logger implementation**: Defeats the purpose of using Vite's logger
-
-**Implementation**: Adapter function in vite-plugin package that wraps Vite's logger:
-```typescript
-function createViteLoggerAdapter(logger: Logger): LoggerStyle {
- // Extract picocolors from Vite's logger
- // Return function matching LoggerStyle interface
-}
-```
-
-### Decision: Error Formatting Flow
-
-**What**: When validation fails in Vite plugin, catch the error, extract errors, and use Vite's logger to format and display them before failing the build.
-
-**Why**: Follows Vite's best practices by using Vite's logger for build output. The error is still thrown to fail the build, but formatting uses Vite's logger.
-
-**Alternatives considered**:
-1. **Prevent error throwing**: Would require significant refactoring
-2. **Custom error handling**: Doesn't use Vite's logger, violates minimal implementation goal
-
-**Implementation**:
-- Vite plugin catches `ArkEnvError` or validation errors
-- Extracts error information
-- Uses Vite's logger to format and display errors
-- Calls `this.error()` to fail the build with Rollup-style error
-
-## Risks / Trade-offs
-
-### Risk: Logger API Changes
-**Mitigation**: The logger interface is simple and stable. Vite's logger API is unlikely to change significantly.
-
-### Risk: Performance Impact
-**Mitigation**: Logger is only used during error cases (validation failures), which are rare. No performance impact on successful builds.
-
-### Trade-off: Abstraction Overhead
-**Trade-off**: Adding abstraction layer adds some complexity, but enables future extensibility and keeps Vite plugin minimal.
-
-**Mitigation**: The abstraction is simple (single function type) and well-documented.
-
-## Migration Plan
-
-1. **Phase 1**: Add logger support to core library (backward compatible)
- - Extend `styleText` to accept optional logger
- - Extend `formatErrors` and `ArkEnvError` to accept optional logger
- - Extend `createEnv` to accept optional logger
- - All changes are optional, defaults to current behavior
-
-2. **Phase 2**: Implement Vite logger adapter
- - Create adapter function in vite-plugin package
- - Adapter converts Vite's logger to our logger interface
-
-3. **Phase 3**: Update Vite plugin to use logger
- - Catch validation errors
- - Use Vite's logger (via adapter) to format errors
- - Display formatted errors using Vite's logger methods
- - Fail build with `this.error()`
-
-4. **Testing**:
- - Unit tests for logger adapter
- - Integration tests for Vite plugin with logger
- - Verify backward compatibility (no logger passed)
-
-## Open Questions
-
-- How to extract picocolors from Vite's logger? (May need to inspect Vite's logger implementation)
-- Should we support other logger APIs in the future? (Yes, but out of scope for this change)
-
diff --git a/openspec/changes/add-vite-logger-support/proposal.md b/openspec/changes/add-vite-logger-support/proposal.md
deleted file mode 100644
index 88f91ab32..000000000
--- a/openspec/changes/add-vite-logger-support/proposal.md
+++ /dev/null
@@ -1,31 +0,0 @@
-# Change: Add Vite Logger Support for Pretty Printing
-
-## Why
-
-Currently, the Vite plugin uses ArkEnv's error formatting which relies on ANSI color codes via the `styleText` utility. Issue #206 requests using Vite's built-in logger (which uses picocolors internally) for better integration with Vite's build output and to follow Vite's best practices.
-
-The implementation should be minimal on the Vite plugin side and adhere to Vite's best practices. We should strive to make the plugin work with a standard API logger, and "bring that logger" from the Vite plugin to the core arkenv library, rather than doing a lot of custom code on the Vite plugin side. In essence, our current `styleText` util should be grown to support Vite's logger (based on picocolors internally) and others in the future.
-
-## What Changes
-
-- **ADDED**: Logger abstraction interface to support pluggable loggers (Vite's logger, and others in the future)
-- **MODIFIED**: `styleText` utility to accept an optional logger function for styling text
-- **MODIFIED**: `formatErrors` function to accept an optional logger for error formatting
-- **MODIFIED**: `ArkEnvError` class to accept an optional logger for error message formatting
-- **MODIFIED**: `createEnv` function to accept an optional logger parameter
-- **MODIFIED**: Vite plugin to use Vite's logger for error formatting when validation fails
-- **ADDED**: Logger adapter utilities to convert Vite's logger API to the standard logger interface
-
-## Impact
-
-- **Affected specs**:
- - `error-formatting` (new capability)
- - `vite-plugin` (modified to use logger)
-- **Affected code**:
- - `packages/arkenv/src/utils/style-text.ts` - Extended to support logger
- - `packages/arkenv/src/errors.ts` - Modified to accept logger
- - `packages/arkenv/src/create-env.ts` - Modified to accept and pass logger
- - `packages/vite-plugin/src/index.ts` - Modified to use Vite's logger
-- **Breaking changes**: None (logger parameter is optional, defaults to current behavior)
-- **Dependencies**: No new external dependencies (uses Vite's logger when available)
-
diff --git a/openspec/changes/add-vite-logger-support/specs/error-formatting/spec.md b/openspec/changes/add-vite-logger-support/specs/error-formatting/spec.md
deleted file mode 100644
index d82761373..000000000
--- a/openspec/changes/add-vite-logger-support/specs/error-formatting/spec.md
+++ /dev/null
@@ -1,34 +0,0 @@
-## ADDED Requirements
-
-### Requirement: Pluggable Logger Support for Error Formatting
-
-The error formatting system SHALL support pluggable loggers for styling error messages. When a logger is provided, it SHALL be used for styling text instead of the default ANSI color codes. When no logger is provided, the system SHALL default to the current ANSI color code behavior for backward compatibility.
-
-#### Scenario: Error formatting with logger
-- **WHEN** a logger function is provided to `formatErrors` or `ArkEnvError`
-- **AND** the logger function accepts a color name and text string
-- **THEN** the logger function is used to style error messages
-- **AND** the formatted errors use the logger's styling instead of ANSI codes
-
-#### Scenario: Error formatting without logger (backward compatibility)
-- **WHEN** no logger is provided to `formatErrors` or `ArkEnvError`
-- **THEN** the system uses the default ANSI color code behavior
-- **AND** error messages are formatted with ANSI codes as before
-- **AND** existing code continues to work without changes
-
-#### Scenario: Logger passed through createEnv
-- **WHEN** a logger is provided to `createEnv`
-- **AND** validation fails
-- **THEN** the logger is passed to `ArkEnvError` for error formatting
-- **AND** error messages use the logger's styling
-
-### Requirement: Logger Interface
-
-The logger interface SHALL be a simple function type that accepts a color name and text string, returning styled text. The logger SHALL support the colors "red", "yellow", and "cyan" to match the current `styleText` utility.
-
-#### Scenario: Logger function signature
-- **WHEN** a logger function is provided
-- **THEN** it accepts parameters `(color: "red" | "yellow" | "cyan", text: string)`
-- **AND** it returns a string with styled text
-- **AND** the function can be used interchangeably with the default `styleText` behavior
-
diff --git a/openspec/changes/add-vite-logger-support/specs/vite-plugin/spec.md b/openspec/changes/add-vite-logger-support/specs/vite-plugin/spec.md
deleted file mode 100644
index 3818de5df..000000000
--- a/openspec/changes/add-vite-logger-support/specs/vite-plugin/spec.md
+++ /dev/null
@@ -1,36 +0,0 @@
-## MODIFIED Requirements
-
-### Requirement: Vite Plugin Environment Variable Validation and Exposure
-
-The Vite plugin SHALL validate environment variables at build-time and expose them to client code through Vite's `define` option. The plugin SHALL only expose environment variables that match Vite's configured prefix (defaults to `VITE_`), filtering out server-only variables automatically. When validation fails, the plugin SHALL use Vite's logger to format and display error messages, following Vite's best practices for build output.
-
-#### Scenario: Plugin validates and exposes client-safe variables
-- **WHEN** a user configures the Vite plugin with a schema containing environment variables
-- **AND** the schema includes variables with the Vite prefix (e.g., `VITE_API_URL`, `VITE_DEBUG`)
-- **THEN** the plugin validates all variables in the schema at build-time
-- **AND** the plugin filters the validated results to only include variables matching the configured prefix
-- **AND** filtered variables are exposed to client code via `import.meta.env.*`
-- **AND** server-only variables without the prefix are filtered out and not exposed
-
-#### Scenario: Plugin respects Vite prefix configuration
-- **WHEN** a user configures `envPrefix` in their Vite config
-- **AND** they pass a schema to the Vite plugin
-- **THEN** the plugin uses the configured prefix to filter variables after validation
-- **AND** only variables matching the configured prefix are validated and exposed
-- **AND** the filtering happens automatically without requiring schema changes
-
-#### Scenario: Plugin uses Vite logger for error formatting
-- **WHEN** environment variable validation fails in the Vite plugin
-- **THEN** the plugin uses Vite's logger to format and display error messages
-- **AND** error messages are styled using Vite's logger (which uses picocolors internally)
-- **AND** the plugin displays formatted errors using Vite's logger methods (e.g., `logger.error()`)
-- **AND** the build fails with a Rollup-style error using `this.error()`
-- **AND** the error output integrates seamlessly with Vite's build output
-
-#### Scenario: Plugin passes Vite logger to core library
-- **WHEN** the Vite plugin needs to validate environment variables
-- **THEN** it extracts Vite's logger from the resolved config
-- **AND** it creates a logger adapter that converts Vite's logger API to the standard logger interface
-- **AND** it passes the logger adapter to `createEnv` for error formatting
-- **AND** the implementation is minimal (no custom error formatting code in the plugin)
-
diff --git a/openspec/changes/add-vite-logger-support/tasks.md b/openspec/changes/add-vite-logger-support/tasks.md
deleted file mode 100644
index e603930ee..000000000
--- a/openspec/changes/add-vite-logger-support/tasks.md
+++ /dev/null
@@ -1,58 +0,0 @@
-## 1. Core Library Changes
-
-- [ ] 1.1 Extend `styleText` utility to accept optional logger parameter
- - Add `LoggerStyle` type definition
- - Make logger parameter optional
- - Use logger if provided, otherwise use current ANSI behavior
- - Update tests to cover logger parameter
-
-- [ ] 1.2 Extend `formatErrors` function to accept optional logger
- - Add optional logger parameter
- - Pass logger to `styleText` calls
- - Update tests to verify logger usage
-
-- [ ] 1.3 Extend `ArkEnvError` class to accept optional logger
- - Add optional logger parameter to constructor
- - Pass logger to `formatErrors` and `styleText` calls
- - Update tests to verify logger usage
-
-- [ ] 1.4 Extend `createEnv` function to accept optional logger
- - Add optional logger parameter
- - Pass logger to `ArkEnvError` when validation fails
- - Update tests to verify logger propagation
-
-## 2. Vite Plugin Changes
-
-- [ ] 2.1 Create Vite logger adapter utility
- - Create adapter function that converts Vite's logger to `LoggerStyle` interface
- - Extract picocolors functionality from Vite's logger
- - Add tests for adapter function
-
-- [ ] 2.2 Update Vite plugin to use logger
- - Access Vite's logger from resolved config in `configResolved` hook
- - Create logger adapter from Vite's logger
- - Pass logger adapter to `createEnv` call
- - Handle validation errors and use Vite's logger for formatting
- - Display errors using Vite's logger methods
- - Fail build with `this.error()` after displaying formatted errors
-
-- [ ] 2.3 Add integration tests for Vite plugin with logger
- - Test error formatting with Vite's logger
- - Verify error output format
- - Test build failure behavior
-
-## 3. Validation
-
-- [ ] 3.1 Verify backward compatibility
- - Run existing tests to ensure no regressions
- - Verify default behavior when no logger is provided
-
-- [ ] 3.2 Test Vite plugin error output
- - Create test fixture with invalid environment variables
- - Verify error messages are formatted using Vite's logger
- - Verify error output matches Vite's build output style
-
-- [ ] 3.3 Validate OpenSpec proposal
- - Run `openspec validate add-vite-logger-support --strict`
- - Fix any validation issues
-
diff --git a/packages/bun-plugin/vitest.config.ts b/packages/bun-plugin/vitest.config.ts
index 459d97847..53978c91b 100644
--- a/packages/bun-plugin/vitest.config.ts
+++ b/packages/bun-plugin/vitest.config.ts
@@ -8,6 +8,7 @@ export default defineConfig({
alias: {
// Mock bun module for testing
bun: new URL("./src/__mocks__/bun.ts", import.meta.url).pathname,
+ arkenv: new URL("../arkenv/src/index.ts", import.meta.url).pathname,
},
},
});
diff --git a/packages/vite-plugin/src/logger-adapter.ts b/packages/vite-plugin/src/logger-adapter.ts
deleted file mode 100644
index a258995e9..000000000
--- a/packages/vite-plugin/src/logger-adapter.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import type { LoggerStyle } from "arkenv";
-import type { Logger } from "vite";
-
-/**
- * Create a logger adapter that converts Vite's logger to ArkEnv's LoggerStyle interface
- * Vite's logger uses picocolors internally. We try to access it through the logger instance.
- * @param logger - Vite's logger instance
- * @returns A LoggerStyle function that can be used with ArkEnv's error formatting
- */
-export function createViteLoggerAdapter(logger: Logger): LoggerStyle {
- // Try to access picocolors through Vite's logger internal structure
- // Vite's logger may have colors exposed or we can access them through the logger instance
- const loggerWithColors = logger as {
- colors?: {
- red?: (text: string) => string;
- yellow?: (text: string) => string;
- cyan?: (text: string) => string;
- };
- };
-
- // If colors are directly available, use them
- if (loggerWithColors.colors) {
- const colors = loggerWithColors.colors;
- return (color: "red" | "yellow" | "cyan", text: string): string => {
- const colorFn = colors[color];
- if (colorFn) {
- return colorFn(text);
- }
- return text;
- };
- }
-
- // Try to access picocolors through require (if available in Vite's context)
- // This is a fallback that works if picocolors is available in the module resolution
- try {
- // eslint-disable-next-line @typescript-eslint/no-require-imports
- const picocolors = require("picocolors");
- if (picocolors) {
- return (color: "red" | "yellow" | "cyan", text: string): string => {
- switch (color) {
- case "red":
- return picocolors.red(text);
- case "yellow":
- return picocolors.yellow(text);
- case "cyan":
- return picocolors.cyan(text);
- default:
- return text;
- }
- };
- }
- } catch {
- // picocolors not available, fall through to plain text
- }
-
- // Final fallback: return plain text
- // This maintains functionality even if colors can't be accessed
- return (_color: "red" | "yellow" | "cyan", text: string): string => text;
-}
From d3bdcbc78a568c726ec8d99a3c20cb99272a50ce Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Fri, 28 Nov 2025 16:13:10 +0500
Subject: [PATCH 31/48] chore: add bun to devDependencies
---
package.json | 1 +
1 file changed, 1 insertion(+)
diff --git a/package.json b/package.json
index 10bb79725..00841a9f7 100644
--- a/package.json
+++ b/package.json
@@ -45,6 +45,7 @@
"@sentry/cli",
"@tailwindcss/oxide",
"@vercel/speed-insights",
+ "bun",
"esbuild",
"sharp"
]
From 328e86795e23d2170491e8741c0b7ed37ed7d05b Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Fri, 28 Nov 2025 18:44:22 +0200
Subject: [PATCH 32/48] Update apps/playgrounds/bun-react/bin/build.ts
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
---
apps/playgrounds/bun-react/bin/build.ts | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)
diff --git a/apps/playgrounds/bun-react/bin/build.ts b/apps/playgrounds/bun-react/bin/build.ts
index 05765f20c..c19a1be95 100644
--- a/apps/playgrounds/bun-react/bin/build.ts
+++ b/apps/playgrounds/bun-react/bin/build.ts
@@ -1,14 +1,23 @@
import arkenv from "@arkenv/bun-plugin";
-await Bun.build({
+const result = await Bun.build({
entrypoints: ["./src/index.html"],
outdir: "./dist",
sourcemap: true,
target: "browser",
minify: true,
plugins: [arkenv],
- // define NODE_ENV=production
define: {
"process.env.NODE_ENV": '"production"',
},
});
+
+if (!result.success) {
+ console.error("Build failed:");
+ for (const log of result.logs) {
+ console.error(log);
+ }
+ process.exit(1);
+}
+
+console.log(`✓ Build succeeded: ${result.outputs.length} files generated`);
From b498d523ddb7a8c3c30fda29e5e761ecd88b6cd5 Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Fri, 28 Nov 2025 21:49:58 +0500
Subject: [PATCH 33/48] docs: clarify how to customize the public environment
variable prefix in Bun
---
apps/www/content/docs/bun-plugin/index.mdx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/www/content/docs/bun-plugin/index.mdx b/apps/www/content/docs/bun-plugin/index.mdx
index effdaa569..2002288b2 100644
--- a/apps/www/content/docs/bun-plugin/index.mdx
+++ b/apps/www/content/docs/bun-plugin/index.mdx
@@ -69,7 +69,7 @@ await Bun.build({
- **Static Analysis**: Automatically replaces `process.env.VARIABLE` with validated values during the build.
- **Type Safety**: Ensures your code only accesses variables defined in your schema.
- **Hot Reloading**: Automatically reloads your schema when `env.ts` changes (in `bun --hot` mode).
-- **Secure Defaults**: Only exposes variables prefixed with `BUN_PUBLIC_` to the client bundle (configurable via Bun's standard behavior).
+- **Secure Defaults**: Only exposes variables prefixed with `BUN_PUBLIC_` to the client bundle. You can customize this prefix by setting the `env` option in your `Bun.build()` configuration (e.g., `env: "MY_PUBLIC_*"` to use `MY_PUBLIC_` instead). See [Bun's bundler documentation](https://bun.sh/docs/bundler#env) for more details.
## Installation
From 754a62cb2d5a3a213e6278d774c2da3935eed452 Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Fri, 28 Nov 2025 21:50:23 +0500
Subject: [PATCH 34/48] refactor: update Bun plugin usage to import `arkenv`
directly and define environment with `arktype`
---
openspec/changes/add-bun-plugin/proposal.md | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/openspec/changes/add-bun-plugin/proposal.md b/openspec/changes/add-bun-plugin/proposal.md
index a7ce88570..4e55f411a 100644
--- a/openspec/changes/add-bun-plugin/proposal.md
+++ b/openspec/changes/add-bun-plugin/proposal.md
@@ -58,10 +58,10 @@ A third, more advanced mode using a custom static plugin file may be added later
```ts
// build.ts
-import { defineEnv } from "arkenv";
-import { createArkEnvBunPlugin } from "@arkenv/bun-plugin";
+import arkenv from "@arkenv/bun-plugin";
+import { type } from "arktype";
-const env = defineEnv({
+const env = type({
BUN_PUBLIC_API_URL: "string",
BUN_PUBLIC_DEBUG: "boolean",
});
@@ -69,7 +69,7 @@ const env = defineEnv({
await Bun.build({
entrypoints: ["./app.tsx"],
outdir: "./dist",
- plugins: [createArkEnvBunPlugin(env)],
+ plugins: [arkenv(env)],
});
```
From b13f5a37ade9a1d2e6daeb3a1c4a4abe400b3cd7 Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Fri, 28 Nov 2025 21:53:07 +0500
Subject: [PATCH 35/48] docs: Update Bun plugin package name from
`bun-plugin-arkenv` to `@arkenv/bun-plugin` in the proposal.
---
openspec/changes/add-bun-plugin/proposal.md | 17 +++++++++--------
1 file changed, 9 insertions(+), 8 deletions(-)
diff --git a/openspec/changes/add-bun-plugin/proposal.md b/openspec/changes/add-bun-plugin/proposal.md
index 4e55f411a..b7bd97025 100644
--- a/openspec/changes/add-bun-plugin/proposal.md
+++ b/openspec/changes/add-bun-plugin/proposal.md
@@ -20,7 +20,7 @@ A future, more advanced configuration pattern using a custom static file MAY be
- **WHEN** a user wants to use `Bun.serve()` for a full-stack application
- **AND** they configure `bunfig.toml` with:
- a `[serve.static]` section
- - a `plugins` array that includes the package name `bun-plugin-arkenv`
+ - a `plugins` array that includes the package name `@arkenv/bun-plugin`
- **AND** their project contains an ArkEnv schema file in one of the supported default locations (for example, `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`)
- **AND** that schema file exports a schema using `defineEnv` (via a default export or an `env` named export)
- **THEN** the plugin SHALL locate the schema file via this convention-based search
@@ -29,7 +29,7 @@ A future, more advanced configuration pattern using a custom static file MAY be
#### Scenario: Bun.serve configuration fails when no schema file is found
-- **WHEN** a user configures `bunfig.toml` with `[serve.static].plugins = ["bun-plugin-arkenv"]`
+- **WHEN** a user configures `bunfig.toml` with `[serve.static].plugins = ["@arkenv/bun-plugin"]`
- **AND** there is no schema file in any of the supported default locations
- **THEN** the plugin SHALL fail fast with a clear, descriptive error message
- **AND** the error message SHALL list the paths that were checked
@@ -42,7 +42,7 @@ A future, more advanced configuration pattern using a custom static file MAY be
**What**: The plugin supports two core configuration modes:
1. **Direct Reference (Bun.build)** – pass a configured plugin instance directly in the `plugins` array.
-2. **Package Reference with Convention (Bun.serve)** – declare `bun-plugin-arkenv` in `bunfig.toml` and let the plugin discover the ArkEnv schema file using a set of well-known paths.
+2. **Package Reference with Convention (Bun.serve)** – declare `@arkenv/bun-plugin` in `bunfig.toml` and let the plugin discover the ArkEnv schema file using a set of well-known paths.
A third, more advanced mode using a custom static plugin file may be added later for projects with non-standard layouts, but is not required for the initial release.
@@ -50,7 +50,7 @@ A third, more advanced mode using a custom static plugin file may be added later
- `Bun.build()` accepts plugins directly as JavaScript objects, which is ideal for explicit, programmatic configuration.
- `Bun.serve()` uses `bunfig.toml` where `plugins` is a list of strings (module specifiers) and does not support passing options inline.
-- By using a package name (`"bun-plugin-arkenv"`) plus convention-based schema discovery, we avoid forcing users to create extra “config glue” files for the common case, while still keeping an escape hatch for advanced setups.
+- By using a package name (`"@arkenv/bun-plugin"`) plus convention-based schema discovery, we avoid forcing users to create extra “config glue” files for the common case, while still keeping an escape hatch for advanced setups.
**Implementation**:
@@ -79,7 +79,7 @@ await Bun.build({
# bunfig.toml
[serve.static]
env = "BUN_PUBLIC_*"
-plugins = ["bun-plugin-arkenv"]
+plugins = ["@arkenv/bun-plugin"]
```
With a schema file at a conventional path, for example:
@@ -96,7 +96,7 @@ const env = defineEnv({
export default env;
```
-At startup, `bun-plugin-arkenv`:
+At startup, `@arkenv/bun-plugin`:
- Searches for a schema file in a small set of well-known locations (for example: `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`).
- Imports the first one it finds.
@@ -117,7 +117,7 @@ plugins = ["./arkenv.bun-plugin.ts"]
```ts
// arkenv.bun-plugin.ts
import { Bun } from "bun";
-import { buildArkEnvBunPlugin } from "bun-plugin-arkenv";
+import { buildArkEnvBunPlugin } from "@arkenv/bun-plugin";
Bun.plugin(
await buildArkEnvBunPlugin({
@@ -170,5 +170,6 @@ The plugin will work very similarly to the Vite plugin:
**Usage Patterns**:
- **Bun.build**: Pass a configured plugin instance directly in the `plugins` array (standard Bun plugin API), for example `plugins: [createArkEnvBunPlugin(env)]`.
-- **Bun.serve (default)**: Configure `bunfig.toml` with `[serve.static].plugins = ["bun-plugin-arkenv"]`. The plugin discovers the ArkEnv schema file from a small set of conventional locations (for example `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`) and uses it automatically.
+- **Bun.serve (default)**: Configure `bunfig.toml` with `[serve.static].plugins = ["@arkenv/bun-plugin"]`. The plugin discovers the ArkEnv schema file from a small set of conventional locations (for example `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`) and uses it automatically.
- **Bun.serve (advanced, future)**: Optionally support a custom plugin entry file referenced from `bunfig.toml` (for example `plugins = ["./arkenv.bun-plugin.ts"]`) for projects that need a non-standard schema location or additional configuration.
+
From 5a88d9a9dff92913fa81c840e4c856bf56c415a4 Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Fri, 28 Nov 2025 22:12:07 +0500
Subject: [PATCH 36/48] feat: Update Bun plugin package name to
`@arkenv/bun-plugin`, switch schema definition to `arktype.type`, and
simplify usage examples.
---
openspec/changes/add-bun-plugin/design.md | 59 +++++++++--------------
openspec/changes/add-bun-plugin/tasks.md | 36 +++++++-------
2 files changed, 41 insertions(+), 54 deletions(-)
diff --git a/openspec/changes/add-bun-plugin/design.md b/openspec/changes/add-bun-plugin/design.md
index 64f492027..5a4c14cf6 100644
--- a/openspec/changes/add-bun-plugin/design.md
+++ b/openspec/changes/add-bun-plugin/design.md
@@ -42,7 +42,7 @@ A future, more advanced configuration pattern using a custom static file MAY be
**What**: The plugin supports two core configuration modes:
1. **Direct Reference (Bun.build)** – pass a configured plugin instance directly in the `plugins` array.
-2. **Package Reference with Convention (Bun.serve)** – declare `bun-plugin-arkenv` in `bunfig.toml` and let the plugin discover the ArkEnv schema file using a set of well-known paths.
+2. **Package Reference with Convention (Bun.serve)** – declare `@arkenv/bun-plugin` in `bunfig.toml` and let the plugin discover the ArkEnv schema file using a set of well-known paths.
A third, more advanced mode using a custom static plugin file may be added later for projects with non-standard layouts, but is not required for the initial release.
@@ -50,26 +50,16 @@ A third, more advanced mode using a custom static plugin file may be added later
- `Bun.build()` accepts plugins directly as JavaScript objects, which is ideal for explicit, programmatic configuration.
- `Bun.serve()` uses `bunfig.toml` where `plugins` is a list of strings (module specifiers) and does not support passing options inline.
-- By using a package name (`"bun-plugin-arkenv"`) plus convention-based schema discovery, we avoid forcing users to create extra “config glue” files for the common case, while still keeping an escape hatch for advanced setups.
+- By using a package name (`"@arkenv/bun-plugin"`) plus convention-based schema discovery, we avoid forcing users to create extra “config glue” files for the common case, while still keeping an escape hatch for advanced setups.
**Implementation**:
-
-
-**Unified Hybrid Export**
-
-The plugin uses a "Hybrid Export" pattern. The default export is both:
-1. A **Plugin Factory Function**: `arkenv(schema)` for manual configuration.
-2. A **Plugin Object**: `{ name, setup }` for zero-config static analysis.
-
-This allows a single import to serve both use cases without friction.
-
-**Pattern 1: Bun.build (Manual Configuration)**
+**Pattern 1: Bun.build (Direct Reference)**
```ts
// build.ts
import arkenv from "@arkenv/bun-plugin";
-import { type } from "arkenv";
+import { type } from "arktype";
const env = type({
BUN_PUBLIC_API_URL: "string",
@@ -79,42 +69,39 @@ const env = type({
await Bun.build({
entrypoints: ["./app.tsx"],
outdir: "./dist",
- plugins: [arkenv(env)], // Call as function
+ plugins: [arkenv(env)],
});
```
-**Pattern 2: Bun.serve / bunfig.toml (Zero-Config)**
+**Pattern 2: Bun.serve (Package Reference with Convention)**
```toml
# bunfig.toml
[serve.static]
+env = "BUN_PUBLIC_*"
plugins = ["@arkenv/bun-plugin"]
```
-**Pattern 3: Bun.build (Zero-Config)**
-
-Because the default export is also a valid plugin object, it can be passed directly to `Bun.build` to trigger auto-discovery:
+With a schema file at a conventional path, for example:
```ts
-import arkenv from "@arkenv/bun-plugin";
+// src/env.ts
+import { type } from "arktype";
-await Bun.build({
- // ...
- plugins: [arkenv], // Pass as object
+const env = type({
+ BUN_PUBLIC_API_URL: "string",
+ BUN_PUBLIC_DEBUG: "boolean",
});
-```
-**Schema Discovery**
-
-In zero-config mode, the plugin searches for:
-1. `./src/env.ts`
-2. `./env.ts`
-
-It expects a `default` export containing the ArkEnv schema.
+export default env;
+```
-**Hot Reloading**
+At startup, `@arkenv/bun-plugin`:
-The plugin uses the `onStart` hook to load/reload the schema at the beginning of every build/dev cycle. This ensures that changes to `env.ts` are picked up (provided the file watcher triggers a rebuild).
+- Searches for a schema file in a small set of well-known locations (for example: `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`).
+- Imports the first one it finds.
+- Reads the default export (or an `env` named export) as the ArkEnv schema.
+- Creates a Bun plugin and registers it with `Bun.plugin(...)`.
If no schema file is found, or the module does not export a usable schema, the plugin fails fast with a clear error message that lists the paths checked and shows a minimal example.
@@ -130,7 +117,7 @@ plugins = ["./arkenv.bun-plugin.ts"]
```ts
// arkenv.bun-plugin.ts
import { Bun } from "bun";
-import { buildArkEnvBunPlugin } from "bun-plugin-arkenv";
+import { buildArkEnvBunPlugin } from "@arkenv/bun-plugin";
Bun.plugin(
await buildArkEnvBunPlugin({
@@ -151,7 +138,7 @@ This keeps the default experience zero-config for most users, while still allowi
**Usage Patterns**:
-- **Bun.build**: Pass a configured plugin instance directly in the `plugins` array (standard Bun plugin API), for example `plugins: [createArkEnvBunPlugin(env)]`.
-- **Bun.serve (default)**: Configure `bunfig.toml` with `[serve.static].plugins = ["bun-plugin-arkenv"]`. The plugin discovers the ArkEnv schema file from a small set of conventional locations (for example `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`) and uses it automatically.
+- **Bun.build**: Pass a configured plugin instance directly in the `plugins` array (standard Bun plugin API), for example `plugins: [arkenv(env)]`.
+- **Bun.serve (default)**: Configure `bunfig.toml` with `[serve.static].plugins = ["@arkenv/bun-plugin"]`. The plugin discovers the ArkEnv schema file from a small set of conventional locations (for example `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`) and uses it automatically.
- **Bun.serve (advanced, future)**: Optionally support a custom plugin entry file referenced from `bunfig.toml` (for example `plugins = ["./arkenv.bun-plugin.ts"]`) for projects that need a non-standard schema location or additional configuration.
diff --git a/openspec/changes/add-bun-plugin/tasks.md b/openspec/changes/add-bun-plugin/tasks.md
index 226b2b65d..792479792 100644
--- a/openspec/changes/add-bun-plugin/tasks.md
+++ b/openspec/changes/add-bun-plugin/tasks.md
@@ -20,16 +20,16 @@ A future, more advanced configuration pattern using a custom static file MAY be
- **WHEN** a user wants to use `Bun.serve()` for a full-stack application
- **AND** they configure `bunfig.toml` with:
- a `[serve.static]` section
- - a `plugins` array that includes the package name `bun-plugin-arkenv`
+ - a `plugins` array that includes the package name `@arkenv/bun-plugin`
- **AND** their project contains an ArkEnv schema file in one of the supported default locations (for example, `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`)
-- **AND** that schema file exports a schema using `defineEnv` (via a default export or an `env` named export)
+- **AND** that schema file exports a schema using `type` from arktype (via a default export or an `env` named export)
- **THEN** the plugin SHALL locate the schema file via this convention-based search
- **AND** it SHALL load the schema at startup
- **AND** it SHALL use that schema to validate and transform environment variables during the bundling phase
#### Scenario: Bun.serve configuration fails when no schema file is found
-- **WHEN** a user configures `bunfig.toml` with `[serve.static].plugins = ["bun-plugin-arkenv"]`
+- **WHEN** a user configures `bunfig.toml` with `[serve.static].plugins = ["@arkenv/bun-plugin"]`
- **AND** there is no schema file in any of the supported default locations
- **THEN** the plugin SHALL fail fast with a clear, descriptive error message
- **AND** the error message SHALL list the paths that were checked
@@ -44,7 +44,7 @@ A future, more advanced configuration pattern using a custom static file MAY be
**What**: The plugin supports two core configuration modes:
1. **Direct Reference (Bun.build)** – pass a configured plugin instance directly in the `plugins` array.
-2. **Package Reference with Convention (Bun.serve)** – declare `bun-plugin-arkenv` in `bunfig.toml` and let the plugin discover the ArkEnv schema file using a set of well-known paths.
+2. **Package Reference with Convention (Bun.serve)** – declare `@arkenv/bun-plugin` in `bunfig.toml` and let the plugin discover the ArkEnv schema file using a set of well-known paths.
A third, more advanced mode using a custom static plugin file may be added later for projects with non-standard layouts, but is not required for the initial release.
@@ -52,7 +52,7 @@ A third, more advanced mode using a custom static plugin file may be added later
- `Bun.build()` accepts plugins directly as JavaScript objects, which is ideal for explicit, programmatic configuration.
- `Bun.serve()` uses `bunfig.toml` where `plugins` is a list of strings (module specifiers) and does not support passing options inline.
-- By using a package name (`"bun-plugin-arkenv"`) plus convention-based schema discovery, we avoid forcing users to create extra “config glue” files for the common case, while still keeping an escape hatch for advanced setups.
+- By using a package name (`"@arkenv/bun-plugin"`) plus convention-based schema discovery, we avoid forcing users to create extra "config glue" files for the common case, while still keeping an escape hatch for advanced setups.
**Implementation**:
@@ -60,10 +60,10 @@ A third, more advanced mode using a custom static plugin file may be added later
```ts
// build.ts
-import { defineEnv } from "arkenv";
-import { createArkEnvBunPlugin } from "@arkenv/bun-plugin";
+import arkenv from "@arkenv/bun-plugin";
+import { type } from "arktype";
-const env = defineEnv({
+const env = type({
BUN_PUBLIC_API_URL: "string",
BUN_PUBLIC_DEBUG: "boolean",
});
@@ -71,7 +71,7 @@ const env = defineEnv({
await Bun.build({
entrypoints: ["./app.tsx"],
outdir: "./dist",
- plugins: [createArkEnvBunPlugin(env)],
+ plugins: [arkenv(env)],
});
```
@@ -81,16 +81,16 @@ await Bun.build({
# bunfig.toml
[serve.static]
env = "BUN_PUBLIC_*"
-plugins = ["bun-plugin-arkenv"]
+plugins = ["@arkenv/bun-plugin"]
```
With a schema file at a conventional path, for example:
```ts
// src/env.ts
-import { defineEnv } from "arkenv";
+import { type } from "arktype";
-const env = defineEnv({
+const env = type({
BUN_PUBLIC_API_URL: "string",
BUN_PUBLIC_DEBUG: "boolean",
});
@@ -98,12 +98,12 @@ const env = defineEnv({
export default env;
```
-At startup, `bun-plugin-arkenv`:
+At startup, `@arkenv/bun-plugin`:
- Searches for a schema file in a small set of well-known locations (for example: `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`).
- Imports the first one it finds.
- Reads the default export (or an `env` named export) as the ArkEnv schema.
-- Creates a Bun plugin via `createArkEnvBunPlugin(schema)` and registers it with `Bun.plugin(...)`.
+- Creates a Bun plugin and registers it with `Bun.plugin(...)`.
If no schema file is found, or the module does not export a usable schema, the plugin fails fast with a clear error message that lists the paths checked and shows a minimal example.
@@ -119,7 +119,7 @@ plugins = ["./arkenv.bun-plugin.ts"]
```ts
// arkenv.bun-plugin.ts
import { Bun } from "bun";
-import { buildArkEnvBunPlugin } from "bun-plugin-arkenv";
+import { buildArkEnvBunPlugin } from "@arkenv/bun-plugin";
Bun.plugin(
await buildArkEnvBunPlugin({
@@ -134,12 +134,12 @@ This keeps the default experience zero-config for most users, while still allowi
**Alternatives considered**:
- **Static file reference as the only pattern** (previous design): Forces every project to create a separate `bun-plugin-config.ts` file even in simple setups, increasing boilerplate.
-- **Schema definition via JSON/YAML or bunfig.toml**: Reduces type safety and breaks the “define once in TypeScript” story that ArkEnv aims for.
+- **Schema definition via JSON/YAML or bunfig.toml**: Reduces type safety and breaks the "define once in TypeScript" story that ArkEnv aims for.
- **Only programmatic configuration** (no bunfig path): Would make full-stack `Bun.serve()` setups awkward compared to other Bun plugins that integrate via `bunfig.toml`.
**Usage Patterns**:
-- **Bun.build**: Pass a configured plugin instance directly in the `plugins` array (standard Bun plugin API), for example `plugins: [createArkEnvBunPlugin(env)]`.
-- **Bun.serve (default)**: Configure `bunfig.toml` with `[serve.static].plugins = ["bun-plugin-arkenv"]`. The plugin discovers the ArkEnv schema file from a small set of conventional locations (for example `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`) and uses it automatically.
+- **Bun.build**: Pass a configured plugin instance directly in the `plugins` array (standard Bun plugin API), for example `plugins: [arkenv(env)]`.
+- **Bun.serve (default)**: Configure `bunfig.toml` with `[serve.static].plugins = ["@arkenv/bun-plugin"]`. The plugin discovers the ArkEnv schema file from a small set of conventional locations (for example `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`) and uses it automatically.
- **Bun.serve (advanced, future)**: Optionally support a custom plugin entry file referenced from `bunfig.toml` (for example `plugins = ["./arkenv.bun-plugin.ts"]`) for projects that need a non-standard schema location or additional configuration.
From f1ba8978c27e73d87d06f9087dd76c69ab6016fb Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Fri, 28 Nov 2025 22:14:48 +0500
Subject: [PATCH 37/48] feat: update Bun plugin package name and schema
definition method from `defineEnv` to `type`.
---
openspec/changes/add-bun-plugin/design.md | 6 +++---
openspec/changes/add-bun-plugin/proposal.md | 6 +++---
openspec/changes/add-bun-plugin/specs/bun-plugin/spec.md | 6 +++---
3 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/openspec/changes/add-bun-plugin/design.md b/openspec/changes/add-bun-plugin/design.md
index 5a4c14cf6..a8b69ddb9 100644
--- a/openspec/changes/add-bun-plugin/design.md
+++ b/openspec/changes/add-bun-plugin/design.md
@@ -20,16 +20,16 @@ A future, more advanced configuration pattern using a custom static file MAY be
- **WHEN** a user wants to use `Bun.serve()` for a full-stack application
- **AND** they configure `bunfig.toml` with:
- a `[serve.static]` section
- - a `plugins` array that includes the package name `bun-plugin-arkenv`
+ - a `plugins` array that includes the package name `@arkenv/bun-plugin`
- **AND** their project contains an ArkEnv schema file in one of the supported default locations (for example, `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`)
-- **AND** that schema file exports a schema using `defineEnv` (via a default export or an `env` named export)
+- **AND** that schema file exports a schema using `type` from arktype (via a default export or an `env` named export)
- **THEN** the plugin SHALL locate the schema file via this convention-based search
- **AND** it SHALL load the schema at startup
- **AND** it SHALL use that schema to validate and transform environment variables during the bundling phase
#### Scenario: Bun.serve configuration fails when no schema file is found
-- **WHEN** a user configures `bunfig.toml` with `[serve.static].plugins = ["bun-plugin-arkenv"]`
+- **WHEN** a user configures `bunfig.toml` with `[serve.static].plugins = ["@arkenv/bun-plugin"]`
- **AND** there is no schema file in any of the supported default locations
- **THEN** the plugin SHALL fail fast with a clear, descriptive error message
- **AND** the error message SHALL list the paths that were checked
diff --git a/openspec/changes/add-bun-plugin/proposal.md b/openspec/changes/add-bun-plugin/proposal.md
index b7bd97025..d86430ab6 100644
--- a/openspec/changes/add-bun-plugin/proposal.md
+++ b/openspec/changes/add-bun-plugin/proposal.md
@@ -22,7 +22,7 @@ A future, more advanced configuration pattern using a custom static file MAY be
- a `[serve.static]` section
- a `plugins` array that includes the package name `@arkenv/bun-plugin`
- **AND** their project contains an ArkEnv schema file in one of the supported default locations (for example, `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`)
-- **AND** that schema file exports a schema using `defineEnv` (via a default export or an `env` named export)
+- **AND** that schema file exports a schema using `type` from arktype (via a default export or an `env` named export)
- **THEN** the plugin SHALL locate the schema file via this convention-based search
- **AND** it SHALL load the schema at startup
- **AND** it SHALL use that schema to validate and transform environment variables during the bundling phase
@@ -86,9 +86,9 @@ With a schema file at a conventional path, for example:
```ts
// src/env.ts
-import { defineEnv } from "arkenv";
+import { type } from "arktype";
-const env = defineEnv({
+const env = type({
BUN_PUBLIC_API_URL: "string",
BUN_PUBLIC_DEBUG: "boolean",
});
diff --git a/openspec/changes/add-bun-plugin/specs/bun-plugin/spec.md b/openspec/changes/add-bun-plugin/specs/bun-plugin/spec.md
index 91810f762..009ef520e 100644
--- a/openspec/changes/add-bun-plugin/specs/bun-plugin/spec.md
+++ b/openspec/changes/add-bun-plugin/specs/bun-plugin/spec.md
@@ -23,16 +23,16 @@ A future, more advanced configuration pattern using a custom static file MAY be
- **WHEN** a user wants to use `Bun.serve()` for a full-stack application
- **AND** they configure `bunfig.toml` with:
- a `[serve.static]` section
- - a `plugins` array that includes the package name `bun-plugin-arkenv`
+ - a `plugins` array that includes the package name `@arkenv/bun-plugin`
- **AND** their project contains an ArkEnv schema file in one of the supported default locations (for example, `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`)
-- **AND** that schema file exports a schema using `defineEnv` (via a default export or an `env` named export)
+- **AND** that schema file exports a schema using `type` from arktype (via a default export or an `env` named export)
- **THEN** the plugin SHALL locate the schema file via this convention-based search
- **AND** it SHALL load the schema at startup
- **AND** it SHALL use that schema to validate and transform environment variables during the bundling phase
#### Scenario: Bun.serve configuration fails when no schema file is found
-- **WHEN** a user configures `bunfig.toml` with `[serve.static].plugins = ["bun-plugin-arkenv"]`
+- **WHEN** a user configures `bunfig.toml` with `[serve.static].plugins = ["@arkenv/bun-plugin"]`
- **AND** there is no schema file in any of the supported default locations
- **THEN** the plugin SHALL fail fast with a clear, descriptive error message
- **AND** the error message SHALL list the paths that were checked
From 0b345b7e36490109ac40a63f99e66e8475c96579 Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Fri, 28 Nov 2025 19:19:02 +0200
Subject: [PATCH 38/48] Update packages/bun-plugin/src/index.ts
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
---
packages/bun-plugin/src/index.ts | 3 +++
1 file changed, 3 insertions(+)
diff --git a/packages/bun-plugin/src/index.ts b/packages/bun-plugin/src/index.ts
index 9b7cf13e5..bf4f510ee 100644
--- a/packages/bun-plugin/src/index.ts
+++ b/packages/bun-plugin/src/index.ts
@@ -198,6 +198,9 @@ hybrid.setup = (build) => {
if (mod.default) {
schema = mod.default;
break;
+ } else if (mod.env) {
+ schema = mod.env;
+ break;
}
} catch (e) {
console.error(`Failed to load env schema from ${p}:`, e);
From cd702becd26718779f5a7d378d1c52a3e9d8a00e Mon Sep 17 00:00:00 2001
From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com>
Date: Fri, 28 Nov 2025 17:19:47 +0000
Subject: [PATCH 39/48] [autofix.ci] apply automated fixes
---
packages/bun-plugin/src/index.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/packages/bun-plugin/src/index.ts b/packages/bun-plugin/src/index.ts
index bf4f510ee..5651179d5 100644
--- a/packages/bun-plugin/src/index.ts
+++ b/packages/bun-plugin/src/index.ts
@@ -198,7 +198,8 @@ hybrid.setup = (build) => {
if (mod.default) {
schema = mod.default;
break;
- } else if (mod.env) {
+ }
+ if (mod.env) {
schema = mod.env;
break;
}
From bad196e5da6f374bd3d8440bdf09d04a25f7601c Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Fri, 28 Nov 2025 22:16:40 +0500
Subject: [PATCH 40/48] docs: simplify Bun plugin creation function name in
examples and descriptions
---
openspec/changes/add-bun-plugin/proposal.md | 4 ++--
packages/bun-plugin/src/index.ts | 17 ++++++++++++++---
2 files changed, 16 insertions(+), 5 deletions(-)
diff --git a/openspec/changes/add-bun-plugin/proposal.md b/openspec/changes/add-bun-plugin/proposal.md
index d86430ab6..c3bfba446 100644
--- a/openspec/changes/add-bun-plugin/proposal.md
+++ b/openspec/changes/add-bun-plugin/proposal.md
@@ -101,7 +101,7 @@ At startup, `@arkenv/bun-plugin`:
- Searches for a schema file in a small set of well-known locations (for example: `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`).
- Imports the first one it finds.
- Reads the default export (or an `env` named export) as the ArkEnv schema.
-- Creates a Bun plugin via `createArkEnvBunPlugin(schema)` and registers it with `Bun.plugin(...)`.
+- Creates a Bun plugin and registers it with `Bun.plugin(...)`.
If no schema file is found, or the module does not export a usable schema, the plugin fails fast with a clear error message that lists the paths checked and shows a minimal example.
@@ -169,7 +169,7 @@ The plugin will work very similarly to the Vite plugin:
- Provides TypeScript type augmentation for type-safe access
**Usage Patterns**:
-- **Bun.build**: Pass a configured plugin instance directly in the `plugins` array (standard Bun plugin API), for example `plugins: [createArkEnvBunPlugin(env)]`.
+- **Bun.build**: Pass a configured plugin instance directly in the `plugins` array (standard Bun plugin API), for example `plugins: [arkenv(env)]`.
- **Bun.serve (default)**: Configure `bunfig.toml` with `[serve.static].plugins = ["@arkenv/bun-plugin"]`. The plugin discovers the ArkEnv schema file from a small set of conventional locations (for example `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`) and uses it automatically.
- **Bun.serve (advanced, future)**: Optionally support a custom plugin entry file referenced from `bunfig.toml` (for example `plugins = ["./arkenv.bun-plugin.ts"]`) for projects that need a non-standard schema location or additional configuration.
diff --git a/packages/bun-plugin/src/index.ts b/packages/bun-plugin/src/index.ts
index 5651179d5..b8e9a2943 100644
--- a/packages/bun-plugin/src/index.ts
+++ b/packages/bun-plugin/src/index.ts
@@ -210,10 +210,21 @@ hybrid.setup = (build) => {
}
if (!schema) {
- console.warn(
- "No env schema found in src/env.ts or env.ts. Skipping @arkenv/bun-plugin validation.",
+ const pathsList = possiblePaths.map((p) => ` - ${p}`).join("\n");
+ const example = `
+Example \`src/env.ts\`:
+\`\`\`ts
+import { type } from "arktype";
+
+export default type({
+ BUN_PUBLIC_API_URL: "string",
+ BUN_PUBLIC_DEBUG: "boolean"
+});
+\`\`\`
+`;
+ 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}`,
);
- return;
}
// Update the shared envMap with new values
From a70bde8b7bec65f7d8b9eb30b50c673d0e5e9650 Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Fri, 28 Nov 2025 22:17:43 +0500
Subject: [PATCH 41/48] docs: Update `ProcessEnvAugmented` example to use
default export from `./env` and add a clarifying comment.
---
packages/bun-plugin/README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/bun-plugin/README.md b/packages/bun-plugin/README.md
index 49fc69859..ab4aa5ce7 100644
--- a/packages/bun-plugin/README.md
+++ b/packages/bun-plugin/README.md
@@ -79,11 +79,11 @@ await Bun.build({
///
import type { ProcessEnvAugmented } from '@arkenv/bun-plugin';
-import type { Env } from './env'; // or from bun.config.ts
declare global {
namespace NodeJS {
- interface ProcessEnv extends ProcessEnvAugmented {}
+ // Note: This assumes your env schema is the default export from "./env"
+ interface ProcessEnv extends ProcessEnvAugmented {}
}
}
```
From c4047f4d205e94f556163dcd71e617ed6af8a21b Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Fri, 28 Nov 2025 22:18:14 +0500
Subject: [PATCH 42/48] test: improve test isolation by saving and restoring
process.env in tests.
---
packages/bun-plugin/src/index.test.ts | 21 +++++++++++----------
1 file changed, 11 insertions(+), 10 deletions(-)
diff --git a/packages/bun-plugin/src/index.test.ts b/packages/bun-plugin/src/index.test.ts
index 3a5bcae1c..8b2cc3784 100644
--- a/packages/bun-plugin/src/index.test.ts
+++ b/packages/bun-plugin/src/index.test.ts
@@ -1,7 +1,17 @@
-import { describe, expect, it } from "vitest";
+import { afterEach, beforeEach, describe, expect, it } from "vitest";
import arkenvPlugin from "./index.js";
describe("Bun Plugin", () => {
+ let originalEnv: NodeJS.ProcessEnv;
+
+ beforeEach(() => {
+ originalEnv = { ...process.env };
+ });
+
+ afterEach(() => {
+ process.env = originalEnv;
+ });
+
it("should create a plugin function", () => {
expect(typeof arkenvPlugin).toBe("function");
});
@@ -15,8 +25,6 @@ describe("Bun Plugin", () => {
expect(pluginInstance).toHaveProperty("name", "@arkenv/bun-plugin");
expect(pluginInstance).toHaveProperty("setup");
expect(typeof pluginInstance.setup).toBe("function");
-
- delete process.env.BUN_PUBLIC_TEST;
});
it("should validate environment variables at plugin creation", () => {
@@ -26,8 +34,6 @@ describe("Bun Plugin", () => {
expect(() => {
arkenvPlugin({ BUN_PUBLIC_TEST: "string" });
}).not.toThrow();
-
- delete process.env.BUN_PUBLIC_TEST;
});
it("should throw if environment variable validation fails", () => {
@@ -52,10 +58,5 @@ describe("Bun Plugin", () => {
// The plugin should be created successfully
expect(pluginInstance).toBeDefined();
-
- // Clean up
- delete process.env.BUN_PUBLIC_API_URL;
- delete process.env.PORT;
- delete process.env.DATABASE_URL;
});
});
From 05e81cad691e1afbf7b252afc139defad9899a68 Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Fri, 28 Nov 2025 22:22:07 +0500
Subject: [PATCH 43/48] feat: export `processEnvSchema` function and update
tests to directly verify its output, enabling declaration file emission.
---
packages/bun-plugin/src/index.test.ts | 15 +++++++++++----
packages/bun-plugin/src/index.ts | 2 +-
packages/bun-plugin/tsconfig.json | 4 ++--
3 files changed, 14 insertions(+), 7 deletions(-)
diff --git a/packages/bun-plugin/src/index.test.ts b/packages/bun-plugin/src/index.test.ts
index 8b2cc3784..96a576a63 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 from "./index.js";
+import arkenvPlugin, { processEnvSchema } from "./index.js";
describe("Bun Plugin", () => {
let originalEnv: NodeJS.ProcessEnv;
@@ -50,13 +50,20 @@ describe("Bun Plugin", () => {
process.env.PORT = "3000";
process.env.DATABASE_URL = "postgres://localhost/db";
- const pluginInstance = arkenvPlugin({
+ const envMap = processEnvSchema({
BUN_PUBLIC_API_URL: "string",
PORT: "number.port",
DATABASE_URL: "string",
});
- // The plugin should be created successfully
- expect(pluginInstance).toBeDefined();
+ // Check that prefixed variables are present
+ expect(envMap.has("BUN_PUBLIC_API_URL")).toBe(true);
+ expect(envMap.get("BUN_PUBLIC_API_URL")).toBe(
+ 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);
});
});
diff --git a/packages/bun-plugin/src/index.ts b/packages/bun-plugin/src/index.ts
index b8e9a2943..3153ae79c 100644
--- a/packages/bun-plugin/src/index.ts
+++ b/packages/bun-plugin/src/index.ts
@@ -9,7 +9,7 @@ export type { ProcessEnvAugmented } from "./types";
/**
* Helper to process env schema and return envMap
*/
-function processEnvSchema(options: EnvSchema | type.Any) {
+export function processEnvSchema(options: EnvSchema | type.Any) {
// Validate environment variables
const env = createEnv(options, process.env);
diff --git a/packages/bun-plugin/tsconfig.json b/packages/bun-plugin/tsconfig.json
index caaebbcdd..8a4bcc9d2 100644
--- a/packages/bun-plugin/tsconfig.json
+++ b/packages/bun-plugin/tsconfig.json
@@ -8,10 +8,10 @@
"skipLibCheck": true,
"noUnusedLocals": true,
"noImplicitAny": true,
- "noEmit": true,
+ "emitDeclarationOnly": true,
"outDir": "dist",
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true
}
-}
+}
\ No newline at end of file
From df8c39eebc41670078b2cd75867edb5a52bfde38 Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Fri, 28 Nov 2025 22:23:03 +0500
Subject: [PATCH 44/48] docs: add impact section to Bun plugin proposal
detailing its opt-in, non-breaking nature and new dependency.
---
openspec/changes/add-bun-plugin/proposal.md | 7 +++++++
packages/bun-plugin/tsconfig.json | 2 +-
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/openspec/changes/add-bun-plugin/proposal.md b/openspec/changes/add-bun-plugin/proposal.md
index c3bfba446..b06f4d66a 100644
--- a/openspec/changes/add-bun-plugin/proposal.md
+++ b/openspec/changes/add-bun-plugin/proposal.md
@@ -172,4 +172,11 @@ The plugin will work very similarly to the Vite plugin:
- **Bun.build**: Pass a configured plugin instance directly in the `plugins` array (standard Bun plugin API), for example `plugins: [arkenv(env)]`.
- **Bun.serve (default)**: Configure `bunfig.toml` with `[serve.static].plugins = ["@arkenv/bun-plugin"]`. The plugin discovers the ArkEnv schema file from a small set of conventional locations (for example `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`) and uses it automatically.
- **Bun.serve (advanced, future)**: Optionally support a custom plugin entry file referenced from `bunfig.toml` (for example `plugins = ["./arkenv.bun-plugin.ts"]`) for projects that need a non-standard schema location or additional configuration.
+
+## Impact
+
+- **New opt-in feature**: The Bun plugin is an entirely new package (@arkenv/bun-plugin) and does not affect existing arkenv users or workflows.
+- **No breaking changes**: This is a pure addition and does not modify any public APIs or existing behavior.
+- **Backward compatible**: All existing code continues to work unchanged. Bun users simply gain access to new functionality.
+- **New dependency**: Projects using @arkenv/bun-plugin will add a new dev dependency to their package.json.
diff --git a/packages/bun-plugin/tsconfig.json b/packages/bun-plugin/tsconfig.json
index 8a4bcc9d2..0288a3e60 100644
--- a/packages/bun-plugin/tsconfig.json
+++ b/packages/bun-plugin/tsconfig.json
@@ -14,4 +14,4 @@
"declaration": true,
"declarationMap": true
}
-}
\ No newline at end of file
+}
From 84c6c1f7f90a29ade3b7f70d0666c9f53ec2325d Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Fri, 28 Nov 2025 22:23:09 +0500
Subject: [PATCH 45/48] Replace detailed Bun plugin specification and design
with an implementation checklist.
---
openspec/changes/add-bun-plugin/tasks.md | 155 ++---------------------
1 file changed, 10 insertions(+), 145 deletions(-)
diff --git a/openspec/changes/add-bun-plugin/tasks.md b/openspec/changes/add-bun-plugin/tasks.md
index 792479792..6c03b2844 100644
--- a/openspec/changes/add-bun-plugin/tasks.md
+++ b/openspec/changes/add-bun-plugin/tasks.md
@@ -1,145 +1,10 @@
-
-### Requirement: Bun Plugin Configuration Patterns
-
-The Bun plugin SHALL support two primary configuration patterns depending on the usage context:
-
-1. **Direct Reference (Bun.build)**: The plugin SHALL be configurable by passing a configured plugin instance directly in the `plugins` array when using `Bun.build()`.
-2. **Package Reference (Bun.serve)**: The plugin SHALL be configurable via a package name in `bunfig.toml` when using `Bun.serve()` for full-stack applications, with convention-based schema discovery.
-
-A future, more advanced configuration pattern using a custom static file MAY be supported, but is not required for the initial version.
-
-#### Scenario: Plugin configuration with Bun.build
-
-- **WHEN** a user wants to build an application using `Bun.build()`
-- **AND** they configure the plugin with an environment variable schema
-- **THEN** they can pass a configured plugin instance directly in the `plugins` array
-- **AND** the plugin validates and transforms environment variables during the build process
-
-#### Scenario: Plugin configuration with Bun.serve via package reference
-
-- **WHEN** a user wants to use `Bun.serve()` for a full-stack application
-- **AND** they configure `bunfig.toml` with:
- - a `[serve.static]` section
- - a `plugins` array that includes the package name `@arkenv/bun-plugin`
-- **AND** their project contains an ArkEnv schema file in one of the supported default locations (for example, `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`)
-- **AND** that schema file exports a schema using `type` from arktype (via a default export or an `env` named export)
-- **THEN** the plugin SHALL locate the schema file via this convention-based search
-- **AND** it SHALL load the schema at startup
-- **AND** it SHALL use that schema to validate and transform environment variables during the bundling phase
-
-#### Scenario: Bun.serve configuration fails when no schema file is found
-
-- **WHEN** a user configures `bunfig.toml` with `[serve.static].plugins = ["@arkenv/bun-plugin"]`
-- **AND** there is no schema file in any of the supported default locations
-- **THEN** the plugin SHALL fail fast with a clear, descriptive error message
-- **AND** the error message SHALL list the paths that were checked
-- **AND** the error message SHALL show an example of a minimal `env` schema file the user can create
-
-### Requirement: Bun Plugin Environment Variable Validation and Transformation
-
-
-
-### Decision: Configuration Modes and Schema Discovery
-
-**What**: The plugin supports two core configuration modes:
-
-1. **Direct Reference (Bun.build)** – pass a configured plugin instance directly in the `plugins` array.
-2. **Package Reference with Convention (Bun.serve)** – declare `@arkenv/bun-plugin` in `bunfig.toml` and let the plugin discover the ArkEnv schema file using a set of well-known paths.
-
-A third, more advanced mode using a custom static plugin file may be added later for projects with non-standard layouts, but is not required for the initial release.
-
-**Why**:
-
-- `Bun.build()` accepts plugins directly as JavaScript objects, which is ideal for explicit, programmatic configuration.
-- `Bun.serve()` uses `bunfig.toml` where `plugins` is a list of strings (module specifiers) and does not support passing options inline.
-- By using a package name (`"@arkenv/bun-plugin"`) plus convention-based schema discovery, we avoid forcing users to create extra "config glue" files for the common case, while still keeping an escape hatch for advanced setups.
-
-**Implementation**:
-
-**Pattern 1: Bun.build (Direct Reference)**
-
-```ts
-// build.ts
-import arkenv from "@arkenv/bun-plugin";
-import { type } from "arktype";
-
-const env = type({
- BUN_PUBLIC_API_URL: "string",
- BUN_PUBLIC_DEBUG: "boolean",
-});
-
-await Bun.build({
- entrypoints: ["./app.tsx"],
- outdir: "./dist",
- plugins: [arkenv(env)],
-});
-```
-
-**Pattern 2: Bun.serve (Package Reference with Convention)**
-
-```toml
-# bunfig.toml
-[serve.static]
-env = "BUN_PUBLIC_*"
-plugins = ["@arkenv/bun-plugin"]
-```
-
-With a schema file at a conventional path, for example:
-
-```ts
-// src/env.ts
-import { type } from "arktype";
-
-const env = type({
- BUN_PUBLIC_API_URL: "string",
- BUN_PUBLIC_DEBUG: "boolean",
-});
-
-export default env;
-```
-
-At startup, `@arkenv/bun-plugin`:
-
-- Searches for a schema file in a small set of well-known locations (for example: `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`).
-- Imports the first one it finds.
-- Reads the default export (or an `env` named export) as the ArkEnv schema.
-- Creates a Bun plugin and registers it with `Bun.plugin(...)`.
-
-If no schema file is found, or the module does not export a usable schema, the plugin fails fast with a clear error message that lists the paths checked and shows a minimal example.
-
-**Future / Advanced Mode (Custom Static File)**
-
-In future iterations we MAY support a custom plugin entry file pattern for advanced layouts, for example:
-
-```toml
-[serve.static]
-plugins = ["./arkenv.bun-plugin.ts"]
-```
-
-```ts
-// arkenv.bun-plugin.ts
-import { Bun } from "bun";
-import { buildArkEnvBunPlugin } from "@arkenv/bun-plugin";
-
-Bun.plugin(
- await buildArkEnvBunPlugin({
- schemaPath: "./config/env/app.env.ts",
- // future options (prefix overrides, strictness, etc.)
- }),
-);
-```
-
-This keeps the default experience zero-config for most users, while still allowing power users to override the schema location and other options when needed.
-
-**Alternatives considered**:
-
-- **Static file reference as the only pattern** (previous design): Forces every project to create a separate `bun-plugin-config.ts` file even in simple setups, increasing boilerplate.
-- **Schema definition via JSON/YAML or bunfig.toml**: Reduces type safety and breaks the "define once in TypeScript" story that ArkEnv aims for.
-- **Only programmatic configuration** (no bunfig path): Would make full-stack `Bun.serve()` setups awkward compared to other Bun plugins that integrate via `bunfig.toml`.
-
-
-
-**Usage Patterns**:
-- **Bun.build**: Pass a configured plugin instance directly in the `plugins` array (standard Bun plugin API), for example `plugins: [arkenv(env)]`.
-- **Bun.serve (default)**: Configure `bunfig.toml` with `[serve.static].plugins = ["@arkenv/bun-plugin"]`. The plugin discovers the ArkEnv schema file from a small set of conventional locations (for example `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`) and uses it automatically.
-- **Bun.serve (advanced, future)**: Optionally support a custom plugin entry file referenced from `bunfig.toml` (for example `plugins = ["./arkenv.bun-plugin.ts"]`) for projects that need a non-standard schema location or additional configuration.
+# Implementation Checklist
+
+- [x] Implement Direct Reference (Bun.build)
+- [x] Implement Package Reference (Bun.serve) with convention-based schema discovery
+- [x] Add schema file path resolution for default locations
+- [x] Implement error handling when schema file is not found
+- [x] Add unit tests
+- [x] Add integration tests
+- [x] Update docs
+- [x] Setup playground app
From 517ce7599dc8aca6fc866225618df8f0252ab04d Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Fri, 28 Nov 2025 22:42:31 +0500
Subject: [PATCH 46/48] docs: update markdown heading levels and formatting in
design and proposal documents
---
openspec/changes/add-bun-plugin/design.md | 14 +++++++-------
openspec/changes/add-bun-plugin/proposal.md | 2 +-
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/openspec/changes/add-bun-plugin/design.md b/openspec/changes/add-bun-plugin/design.md
index a8b69ddb9..3ffff2123 100644
--- a/openspec/changes/add-bun-plugin/design.md
+++ b/openspec/changes/add-bun-plugin/design.md
@@ -46,15 +46,15 @@ A future, more advanced configuration pattern using a custom static file MAY be
A third, more advanced mode using a custom static plugin file may be added later for projects with non-standard layouts, but is not required for the initial release.
-**Why**:
+#### Why
- `Bun.build()` accepts plugins directly as JavaScript objects, which is ideal for explicit, programmatic configuration.
- `Bun.serve()` uses `bunfig.toml` where `plugins` is a list of strings (module specifiers) and does not support passing options inline.
- By using a package name (`"@arkenv/bun-plugin"`) plus convention-based schema discovery, we avoid forcing users to create extra “config glue” files for the common case, while still keeping an escape hatch for advanced setups.
-**Implementation**:
+#### Implementation
-**Pattern 1: Bun.build (Direct Reference)**
+### Pattern 1: Bun.build (Direct Reference)
```ts
// build.ts
@@ -73,7 +73,7 @@ await Bun.build({
});
```
-**Pattern 2: Bun.serve (Package Reference with Convention)**
+### Pattern 2: Bun.serve (Package Reference with Convention)
```toml
# bunfig.toml
@@ -105,7 +105,7 @@ At startup, `@arkenv/bun-plugin`:
If no schema file is found, or the module does not export a usable schema, the plugin fails fast with a clear error message that lists the paths checked and shows a minimal example.
-**Future / Advanced Mode (Custom Static File)**
+### Future / Advanced Mode (Custom Static File)
In future iterations we MAY support a custom plugin entry file pattern for advanced layouts, for example:
@@ -129,7 +129,7 @@ Bun.plugin(
This keeps the default experience zero-config for most users, while still allowing power users to override the schema location and other options when needed.
-**Alternatives considered**:
+#### Alternatives considered
- **Static file reference as the only pattern** (previous design): Forces every project to create a separate `bun-plugin-config.ts` file even in simple setups, increasing boilerplate.
- **Schema definition via JSON/YAML or bunfig.toml**: Reduces type safety and breaks the “define once in TypeScript” story that ArkEnv aims for.
@@ -137,7 +137,7 @@ This keeps the default experience zero-config for most users, while still allowi
-**Usage Patterns**:
+### Usage Patterns
- **Bun.build**: Pass a configured plugin instance directly in the `plugins` array (standard Bun plugin API), for example `plugins: [arkenv(env)]`.
- **Bun.serve (default)**: Configure `bunfig.toml` with `[serve.static].plugins = ["@arkenv/bun-plugin"]`. The plugin discovers the ArkEnv schema file from a small set of conventional locations (for example `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`) and uses it automatically.
- **Bun.serve (advanced, future)**: Optionally support a custom plugin entry file referenced from `bunfig.toml` (for example `plugins = ["./arkenv.bun-plugin.ts"]`) for projects that need a non-standard schema location or additional configuration.
diff --git a/openspec/changes/add-bun-plugin/proposal.md b/openspec/changes/add-bun-plugin/proposal.md
index b06f4d66a..03c0334d4 100644
--- a/openspec/changes/add-bun-plugin/proposal.md
+++ b/openspec/changes/add-bun-plugin/proposal.md
@@ -168,7 +168,7 @@ The plugin will work very similarly to the Vite plugin:
- Replaces `process.env.VARIABLE` with validated, transformed values (e.g., string to boolean, default values)
- Provides TypeScript type augmentation for type-safe access
-**Usage Patterns**:
+### Usage Patterns
- **Bun.build**: Pass a configured plugin instance directly in the `plugins` array (standard Bun plugin API), for example `plugins: [arkenv(env)]`.
- **Bun.serve (default)**: Configure `bunfig.toml` with `[serve.static].plugins = ["@arkenv/bun-plugin"]`. The plugin discovers the ArkEnv schema file from a small set of conventional locations (for example `./src/env.arkenv.ts`, `./src/env.ts`, `./env.arkenv.ts`, `./env.ts`) and uses it automatically.
- **Bun.serve (advanced, future)**: Optionally support a custom plugin entry file referenced from `bunfig.toml` (for example `plugins = ["./arkenv.bun-plugin.ts"]`) for projects that need a non-standard schema location or additional configuration.
From 3981063a62565a8d3dd1d436366adff8828771a4 Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Fri, 28 Nov 2025 22:42:40 +0500
Subject: [PATCH 47/48] feat: introduce OpenSpec agent workflows for proposal,
apply, and archive, and update proposal command guardrails.
---
.agent/workflows/openspec-apply.md | 20 ++++++++++++++++++++
.agent/workflows/openspec-archive.md | 24 ++++++++++++++++++++++++
.agent/workflows/openspec-proposal.md | 25 +++++++++++++++++++++++++
.cursor/commands/openspec-proposal.md | 1 +
4 files changed, 70 insertions(+)
create mode 100644 .agent/workflows/openspec-apply.md
create mode 100644 .agent/workflows/openspec-archive.md
create mode 100644 .agent/workflows/openspec-proposal.md
diff --git a/.agent/workflows/openspec-apply.md b/.agent/workflows/openspec-apply.md
new file mode 100644
index 000000000..b0154dd9d
--- /dev/null
+++ b/.agent/workflows/openspec-apply.md
@@ -0,0 +1,20 @@
+---
+description: Implement an approved OpenSpec change and keep tasks in sync.
+---
+
+**Guardrails**
+- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
+- Keep changes tightly scoped to the requested outcome.
+- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
+
+**Steps**
+Track these steps as TODOs and complete them one by one.
+1. Read `changes//proposal.md`, `design.md` (if present), and `tasks.md` to confirm scope and acceptance criteria.
+2. Work through tasks sequentially, keeping edits minimal and focused on the requested change.
+3. Confirm completion before updating statuses—make sure every item in `tasks.md` is finished.
+4. Update the checklist after all work is done so each task is marked `- [x]` and reflects reality.
+5. Reference `openspec list` or `openspec show ` when additional context is required.
+
+**Reference**
+- Use `openspec show --json --deltas-only` if you need additional context from the proposal while implementing.
+
diff --git a/.agent/workflows/openspec-archive.md b/.agent/workflows/openspec-archive.md
new file mode 100644
index 000000000..38dcc8174
--- /dev/null
+++ b/.agent/workflows/openspec-archive.md
@@ -0,0 +1,24 @@
+---
+description: Archive a deployed OpenSpec change and update specs.
+---
+
+**Guardrails**
+- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
+- Keep changes tightly scoped to the requested outcome.
+- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
+
+**Steps**
+1. Determine the change ID to archive:
+ - If this prompt already includes a specific change ID (for example inside a `` block populated by slash-command arguments), use that value after trimming whitespace.
+ - If the conversation references a change loosely (for example by title or summary), run `openspec list` to surface likely IDs, share the relevant candidates, and confirm which one the user intends.
+ - Otherwise, review the conversation, run `openspec list`, and ask the user which change to archive; wait for a confirmed change ID before proceeding.
+ - If you still cannot identify a single change ID, stop and tell the user you cannot archive anything yet.
+2. Validate the change ID by running `openspec list` (or `openspec show `) and stop if the change is missing, already archived, or otherwise not ready to archive.
+3. Run `openspec archive --yes` so the CLI moves the change and applies spec updates without prompts (use `--skip-specs` only for tooling-only work).
+4. Review the command output to confirm the target specs were updated and the change landed in `changes/archive/`.
+5. Validate with `openspec validate --strict` and inspect with `openspec show ` if anything looks off.
+
+**Reference**
+- Use `openspec list` to confirm change IDs before archiving.
+- Inspect refreshed specs with `openspec list --specs` and address any validation issues before handing off.
+
diff --git a/.agent/workflows/openspec-proposal.md b/.agent/workflows/openspec-proposal.md
new file mode 100644
index 000000000..db8a95ad0
--- /dev/null
+++ b/.agent/workflows/openspec-proposal.md
@@ -0,0 +1,25 @@
+---
+description: Scaffold a new OpenSpec change and validate strictly.
+---
+
+**Guardrails**
+- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
+- Keep changes tightly scoped to the requested outcome.
+- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
+- Identify any vague or ambiguous details and ask the necessary follow-up questions before editing files.
+- Do not write any code during the proposal stage. Only create design documents (proposal.md, tasks.md, design.md, and spec deltas). Implementation happens in the apply stage after approval.
+
+**Steps**
+1. Review `openspec/project.md`, run `openspec list` and `openspec list --specs`, and inspect related code or docs (e.g., via `rg`/`ls`) to ground the proposal in current behaviour; note any gaps that require clarification.
+2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, and `design.md` (when needed) under `openspec/changes//`.
+3. Map the change into concrete capabilities or requirements, breaking multi-scope efforts into distinct spec deltas with clear relationships and sequencing.
+4. Capture architectural reasoning in `design.md` when the solution spans multiple systems, introduces new patterns, or demands trade-off discussion before committing to specs.
+5. Draft spec deltas in `changes//specs//spec.md` (one folder per capability) using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement and cross-reference related capabilities when relevant.
+6. Draft `tasks.md` as an ordered list of small, verifiable work items that deliver user-visible progress, include validation (tests, tooling), and highlight dependencies or parallelizable work.
+7. Validate with `openspec validate --strict` and resolve every issue before sharing the proposal.
+
+**Reference**
+- Use `openspec show --json --deltas-only` or `openspec show --type spec` to inspect details when validation fails.
+- Search existing requirements with `rg -n "Requirement:|Scenario:" openspec/specs` before writing new ones.
+- Explore the codebase with `rg `, `ls`, or direct file reads so proposals align with current implementation realities.
+
diff --git a/.cursor/commands/openspec-proposal.md b/.cursor/commands/openspec-proposal.md
index 2d7ed7e93..25f1a3f57 100644
--- a/.cursor/commands/openspec-proposal.md
+++ b/.cursor/commands/openspec-proposal.md
@@ -10,6 +10,7 @@ description: Scaffold a new OpenSpec change and validate strictly.
- Keep changes tightly scoped to the requested outcome.
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
- Identify any vague or ambiguous details and ask the necessary follow-up questions before editing files.
+- Do not write any code during the proposal stage. Only create design documents (proposal.md, tasks.md, design.md, and spec deltas). Implementation happens in the apply stage after approval.
**Steps**
1. Review `openspec/project.md`, run `openspec list` and `openspec list --specs`, and inspect related code or docs (e.g., via `rg`/`ls`) to ground the proposal in current behaviour; note any gaps that require clarification.
From db3ff266dd9ee3e991730230c4dc7922c0f48e15 Mon Sep 17 00:00:00 2001
From: Yam C Borodetsky
Date: Fri, 28 Nov 2025 23:03:55 +0500
Subject: [PATCH 48/48] feat: Add arkenv and arktype dependencies, configure
production environment variables, and refine build script for `NODE_ENV`.
---
apps/playgrounds/bun-react/.env.production | 2 ++
apps/playgrounds/bun-react/bin/build.ts | 3 ---
apps/playgrounds/bun-react/package.json | 4 +++-
pnpm-lock.yaml | 6 ++++++
4 files changed, 11 insertions(+), 4 deletions(-)
create mode 100644 apps/playgrounds/bun-react/.env.production
diff --git a/apps/playgrounds/bun-react/.env.production b/apps/playgrounds/bun-react/.env.production
new file mode 100644
index 000000000..2cce42ed5
--- /dev/null
+++ b/apps/playgrounds/bun-react/.env.production
@@ -0,0 +1,2 @@
+BUN_PUBLIC_API_URL=https://api.example.com
+BUN_PUBLIC_DEBUG=true
diff --git a/apps/playgrounds/bun-react/bin/build.ts b/apps/playgrounds/bun-react/bin/build.ts
index c19a1be95..409918899 100644
--- a/apps/playgrounds/bun-react/bin/build.ts
+++ b/apps/playgrounds/bun-react/bin/build.ts
@@ -7,9 +7,6 @@ const result = await Bun.build({
target: "browser",
minify: true,
plugins: [arkenv],
- define: {
- "process.env.NODE_ENV": '"production"',
- },
});
if (!result.success) {
diff --git a/apps/playgrounds/bun-react/package.json b/apps/playgrounds/bun-react/package.json
index 09bbf3a0a..4d29842f7 100644
--- a/apps/playgrounds/bun-react/package.json
+++ b/apps/playgrounds/bun-react/package.json
@@ -5,13 +5,15 @@
"type": "module",
"scripts": {
"dev": "bun --hot src/index.tsx",
- "build": "bun bin/build.ts",
+ "build": "NODE_ENV=production bun bin/build.ts",
"preview": "bun dist/index.html",
"start": "NODE_ENV=production bun src/index.tsx",
"clean": "rimraf dist node_modules"
},
"dependencies": {
"@arkenv/bun-plugin": "workspace:*",
+ "arkenv": "workspace:*",
+ "arktype": "catalog:",
"react": "catalog:",
"react-dom": "catalog:"
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 85d1dd0d4..eae919aa6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -131,6 +131,12 @@ importers:
'@arkenv/bun-plugin':
specifier: workspace:*
version: link:../../../packages/bun-plugin
+ arkenv:
+ specifier: workspace:*
+ version: link:../../../packages/arkenv
+ arktype:
+ specifier: 'catalog:'
+ version: 2.1.27
react:
specifier: 'catalog:'
version: 19.2.0