Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/free-oranges-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@arkenv/vite-plugin": patch
---

#### Add `ImportMetaEnvAugmented` type helper for typesafe `import.meta.env`.

Add a new `ImportMetaEnvAugmented` type that augments `import.meta.env` with your environment variable schema. This provides full type safety and autocomplete for all your `VITE_*` environment variables in client code.

Implementation inspired by [Julien-R44](https://github.com/Julien-R44)'s [vite-plugin-validate-env](https://github.com/Julien-R44/vite-plugin-validate-env#typing-importmetaenv).
3 changes: 2 additions & 1 deletion apps/playgrounds/vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@vitejs/plugin-react": "5.1.1",
"globals": "16.5.0",
"typescript": "5.9.3",
"vite": "npm:rolldown-vite@latest"
"vite": "npm:rolldown-vite@latest",
"vite-tsconfig-paths": "5.1.4"
}
}
20 changes: 20 additions & 0 deletions apps/playgrounds/vite/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,21 @@
/// <reference types="vite/client" />

// Import the Env schema type from vite.config.ts
// Note: In a real project, you might want to export Env from a separate env.ts file
// and import it like: import type { Env } from "./env";
type ImportMetaEnvAugmented =
import("@arkenv/vite-plugin").ImportMetaEnvAugmented<
typeof import("../vite.config").Env
>;

interface ViteTypeOptions {
// By adding this line, you can make the type of ImportMetaEnv strict
// to disallow unknown keys.
// See: https://vite.dev/guide/env-and-mode#intellisense-for-typescript
// ⚠️ This option requires Vite 6.3.x or higher
strictImportMetaEnv: unknown;
}

// Now import.meta.env is totally typesafe and based on your `Env` schema definition
// Only VITE_* prefixed variables will be included (PORT is excluded)
interface ImportMetaEnv extends ImportMetaEnvAugmented {}
14 changes: 12 additions & 2 deletions apps/playgrounds/vite/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
{
"files": [],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
]
}
10 changes: 4 additions & 6 deletions apps/playgrounds/vite/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import arkenvVitePlugin from "@arkenv/vite-plugin";
import reactPlugin from "@vitejs/plugin-react";
import arkenv, { type } from "arkenv";
import { defineConfig, loadEnv } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";

// Define the schema once, outside of defineConfig using type()
// This schema is used for both:
// 1. Validating unprefixed config variables (PORT) via loadEnv
// 2. Validating VITE_* variables via the plugin
// The plugin automatically filters to only expose variables matching the Vite prefix (defaults to VITE_)
const Env = type({
export const Env = type({
PORT: "number.port",
VITE_MY_VAR: "string",
VITE_MY_NUMBER: type("string").pipe((str) => Number.parseInt(str, 10)),
Expand All @@ -17,14 +17,12 @@ const Env = type({

// https://vite.dev/config/
export default defineConfig(({ mode }) => {
// Validate unprefixed config variables (e.g., PORT) using loadEnv
// These are server-only and not exposed to client code
// The schema defined with type() can be passed directly to arkenv()
const env = arkenv(Env, loadEnv(mode, process.cwd(), ""));

console.log(env.VITE_MY_NUMBER + " " + typeof env.VITE_MY_NUMBER);
console.log(`${env.VITE_MY_NUMBER} ${typeof env.VITE_MY_NUMBER}`);
return {
plugins: [
tsconfigPaths(),
reactPlugin(),
// The plugin validates VITE_* variables and automatically filters to only expose
// variables matching the Vite prefix (defaults to VITE_). Server-only variables
Expand Down
10 changes: 7 additions & 3 deletions apps/www/content/docs/vite-plugin/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ title: What is the Vite plugin?

This is the Vite plugin for ArkEnv. It validates environment variables at build-time with ArkEnv.

> [!IMPORTANT]
> The plugin automatically filters environment variables to only expose those matching Vite's configured prefix (defaults to `VITE_`). Server-only variables without the prefix are automatically excluded from the client bundle, preventing accidental exposure of sensitive configuration.

```ts title="vite.config.ts"
import arkenv from "@arkenv/vite-plugin";
import { defineConfig } from "vite";
Expand All @@ -21,3 +18,10 @@ export default defineConfig({
],
});
```

> [!NOTE]
> The plugin automatically filters environment variables to only expose those matching Vite's configured prefix (defaults to `VITE_`). Server-only variables without the prefix are automatically excluded from the client bundle. If you _do_ need to use non-prefixed variables, see [Using ArkEnv in Vite config](/docs/vite-plugin/arkenv-in-viteconfig).

## Type-safe import.meta.env

We strongly recommend making `import.meta.env` fully typesafe by augmenting it with your schema. See [Typing import.meta.env](/docs/vite-plugin/typing-import-meta-env) for quick, one-time setup.
2 changes: 1 addition & 1 deletion apps/www/content/docs/vite-plugin/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
"description": "The Vite plugin",
"root": true,
"icon": "Vite",
"pages": ["index", "arkenv-in-viteconfig"]
"pages": ["index", "arkenv-in-viteconfig", "typing-import-meta-env"]
}
102 changes: 102 additions & 0 deletions apps/www/content/docs/vite-plugin/typing-import-meta-env.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
title: Typing import.meta.env
---

For most use cases, you'll want to access your environment variables in your client code. [In a Vite project, this is done via `import.meta.env`](https://vite.dev/guide/env-and-mode).

With ArkEnv, this can be typesafe with a one-time project setup: add a small file in your `src` directory and ensure your `Env` schema is defined in `vite.config.ts` (or exported from a separate file).

> [!NOTE]
> After you complete this setup, each time you add/remove environment variables from your schema, your `import.meta.env` will always be typesafe - no codegen needed.

## Setup

Add this to a `vite-env.d.ts` file in your `src` directory:

Comment thread
coderabbitai[bot] marked this conversation as resolved.
```ts title="src/vite-env.d.ts"
/// <reference types="vite/client" />

type ImportMetaEnvAugmented =
import("@arkenv/vite-plugin").ImportMetaEnvAugmented<
typeof import("../vite.config").Env
>;

interface ViteTypeOptions {
// By adding this line, you can make the type of ImportMetaEnv strict
// to disallow unknown keys.
// See: https://vite.dev/guide/env-and-mode#intellisense-for-typescript
// ⚠️ This option requires Vite 6.3.x or higher
strictImportMetaEnv: unknown;
}

// Augment import.meta.env with your schema
// Only `VITE_*` prefixed variables will be included
interface ImportMetaEnv extends ImportMetaEnvAugmented {}
```

## Usage

Once set up, `import.meta.env` is fully typesafe:

```ts title="src/App.tsx"
// TypeScript knows about your VITE_* variables
const apiUrl = import.meta.env.VITE_API_URL; // ✅ Type-safe
const port = import.meta.env.PORT; // ❌ Error: PORT is not in ImportMetaEnv

// Autocomplete works too!
import.meta.env.VITE_ // Shows all your VITE_* variables
```

## How it works

The `ImportMetaEnvAugmented` type:

1. Extracts the inferred type from your schema (the result of `type()` from arkenv)
2. Filters to only include variables matching the Vite prefix (defaults to `"VITE_"`)
3. Makes them available on `import.meta.env` with full type safety

Server-only variables (like `PORT`) are automatically excluded from the client bundle and won't appear in `import.meta.env`.

## Example with separate env file

As your project grows, you might want to define your schema in a dedicated file for better organization:

```ts title="src/env.ts"
import { type } from "arkenv";

export const Env = type({
PORT: "number.port",
VITE_API_URL: "string",
VITE_API_KEY: "string",
VITE_MY_NUMBER: type("string").pipe((str) => Number.parseInt(str, 10)),
VITE_MY_BOOLEAN: type("string").pipe((str) => str === "true"),
});

export type Env = typeof Env.infer;
```

```ts title="vite.config.ts"
import arkenvVitePlugin from "@arkenv/vite-plugin";
import { defineConfig } from "vite";
import { Env } from "./src/env";

export default defineConfig({
plugins: [arkenvVitePlugin(Env)],
});
```

```ts title="src/vite-env.d.ts"
/// <reference types="vite/client" />

import type { ImportMetaEnvAugmented } from "@arkenv/vite-plugin";
import type { Env } from "./env";

interface ViteTypeOptions {
strictImportMetaEnv: unknown;
}

interface ImportMetaEnv extends ImportMetaEnvAugmented<typeof Env> {}
```

## Credit
This implementation is inspired by [vite-plugin-validate-env by Julien-R44](https://github.com/Julien-R44/vite-plugin-validate-env#typing-importmetaenv).
2 changes: 1 addition & 1 deletion arkenv.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"settings": {
"files.associations": {
"turbo.json": "jsonc",
"tsconfig.json": "jsonc",
"tsconfig*.json": "jsonc",
".github/renovate.json": "jsonc",
"renovate.json": "jsonc"
}
Expand Down
10 changes: 10 additions & 0 deletions biome.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,16 @@
}
}
}
},
{
"includes": ["**/*.d.ts"],
"linter": {
"rules": {
"correctness": {
"noUnusedVariables": "off"
}
}
}
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Users SHALL be able to use ArkEnv directly in `vite.config.ts` files to validate
- **AND** they define a schema for unprefixed environment variables needed by the config (e.g., `PORT`, `DATABASE_URL`)
- **THEN** the environment variables are validated at build-time
- **AND** invalid or missing variables cause the build to fail with clear error messages
- **AND** the validated variables are type-safe and can be used in the Vite config
- **AND** the validated variables are typesafe and can be used in the Vite config

#### Scenario: Type-safe Vite config with environment variables
- **WHEN** a user uses ArkEnv to validate environment variables in vite.config.ts
Expand Down Expand Up @@ -48,17 +48,17 @@ The project SHALL support using ArkEnv with Vite's `loadEnv` function. Since `cr
- **WHEN** a user defines a schema using `type()` outside of `defineConfig`
- **AND** they call `loadEnv()` to load environment variables
- **AND** they pass both the type definition and `loadEnv()` result to `createEnv()` or `arkenv()`
- **THEN** the environment variables are validated and returned as type-safe
- **THEN** the environment variables are validated and returned as typesafe
- **AND** the same type definition can be passed to the Vite plugin for validating `VITE_*` variables
- **AND** no separate wrapper utility is needed

### Requirement: Type Safety Constraint

The environment object returned from `loadEnv` or any wrapper SHALL be type-safe. Unsafe patterns that bypass validation or type checking are FORBIDDEN.
The environment object returned from `loadEnv` or any wrapper SHALL be typesafe. Unsafe patterns that bypass validation or type checking are FORBIDDEN.

#### Constraint: Environment object must be wrapped or typed
- **FORBIDDEN**: Directly using `loadEnv()` result without validation or proper typing
- **FORBIDDEN**: Using `as const` assertion on environment objects (not type-safe, bypasses validation)
- **FORBIDDEN**: Using `as const` assertion on environment objects (not typesafe, bypasses validation)
- **FORBIDDEN**: Defining schemas as raw objects instead of using `type()` function
- **REQUIRED**: The schema MUST be defined using `type()` function from ArkType
- **REQUIRED**: The environment object MUST be wrapped in a function like `arkenv()` or `createEnv()` with the type definition
Expand All @@ -67,7 +67,7 @@ The environment object returned from `loadEnv` or any wrapper SHALL be type-safe
- **WHEN** a user attempts to use `loadEnv()` directly without validation
- **OR** a user attempts to use `as const` assertion on an environment object
- **THEN** the pattern is documented as forbidden
- **AND** examples demonstrate only type-safe patterns
- **AND** examples demonstrate only typesafe patterns
- **AND** documentation clearly explains why unsafe patterns (including `as const`) are not allowed and do not provide type safety

### Requirement: Documentation for Vite Config Usage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
- [x] Modified Vite plugin to accept both raw schema objects and type definitions
- [x] Updated vite.config.ts example to use `type()` to define schema once, outside `defineConfig`
- [x] Verified type inference works correctly - `env` is properly typed (not `unknown`)
- [x] 1.3 If implementing a `loadEnv` wrapper, ensure it provides type-safe return values
- [x] 1.3 If implementing a `loadEnv` wrapper, ensure it provides typesafe return values
- Not needed - `createEnv` directly accepts type definitions, no wrapper required
- [x] 1.4 Ensure the solution allows schema to be shared between `loadEnv` call and plugin definition
- Schema defined with `type()` outside `defineConfig` can be reused in both places
Expand Down
10 changes: 5 additions & 5 deletions openspec/specs/vite-config-usage/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Users SHALL be able to use ArkEnv directly in `vite.config.ts` files to validate
- **AND** they define a schema for unprefixed environment variables needed by the config (e.g., `PORT`, `DATABASE_URL`)
- **THEN** the environment variables are validated at build-time
- **AND** invalid or missing variables cause the build to fail with clear error messages
- **AND** the validated variables are type-safe and can be used in the Vite config
- **AND** the validated variables are typesafe and can be used in the Vite config

#### Scenario: Type-safe Vite config with environment variables
- **WHEN** a user uses ArkEnv to validate environment variables in vite.config.ts
Expand Down Expand Up @@ -51,17 +51,17 @@ The project SHALL support using ArkEnv with Vite's `loadEnv` function. Since `cr
- **WHEN** a user defines a schema using `type()` outside of `defineConfig`
- **AND** they call `loadEnv()` to load environment variables
- **AND** they pass both the type definition and `loadEnv()` result to `createEnv()` or `arkenv()`
- **THEN** the environment variables are validated and returned as type-safe
- **THEN** the environment variables are validated and returned as typesafe
- **AND** the same type definition can be passed to the Vite plugin for validating `VITE_*` variables
- **AND** no separate wrapper utility is needed

### Requirement: Type Safety Constraint

The environment object returned from `loadEnv` or any wrapper SHALL be type-safe. Unsafe patterns that bypass validation or type checking are FORBIDDEN.
The environment object returned from `loadEnv` or any wrapper SHALL be typesafe. Unsafe patterns that bypass validation or type checking are FORBIDDEN.

#### Constraint: Environment object must be wrapped or typed
- **FORBIDDEN**: Directly using `loadEnv()` result without validation or proper typing
- **FORBIDDEN**: Using `as const` assertion on environment objects (not type-safe, bypasses validation)
- **FORBIDDEN**: Using `as const` assertion on environment objects (not typesafe, bypasses validation)
- **FORBIDDEN**: Defining schemas as raw objects instead of using `type()` function
- **REQUIRED**: The schema MUST be defined using `type()` function from ArkType
- **REQUIRED**: The environment object MUST be wrapped in a function like `arkenv()` or `createEnv()` with the type definition
Expand All @@ -70,7 +70,7 @@ The environment object returned from `loadEnv` or any wrapper SHALL be type-safe
- **WHEN** a user attempts to use `loadEnv()` directly without validation
- **OR** a user attempts to use `as const` assertion on an environment object
- **THEN** the pattern is documented as forbidden
- **AND** examples demonstrate only type-safe patterns
- **AND** examples demonstrate only typesafe patterns
- **AND** documentation clearly explains why unsafe patterns (including `as const`) are not allowed and do not provide type safety

### Requirement: Documentation for Vite Config Usage
Expand Down
35 changes: 35 additions & 0 deletions packages/vite-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,46 @@ import { createEnv } from "arkenv";
import type { type } from "arktype";
import { loadEnv, type Plugin } from "vite";

export type { ImportMetaEnvAugmented } from "./types";

/**
* TODO: If possible, find a better type than "const T extends Record<string, unknown>",
* and be as close as possible to the type accepted by ArkType's `type`.
*/

/**
* Vite plugin to validate environment variables using ArkEnv and expose them to client code.
*
* The plugin validates environment variables using ArkEnv's schema validation and
* automatically filters them based on Vite's `envPrefix` configuration (defaults to `"VITE_"`).
* Only environment variables matching the prefix are exposed to client code via `import.meta.env.*`.
*
* @param options - The environment variable schema definition. Can be an `EnvSchema` object
* for typesafe validation or an ArkType `type.Any` for dynamic schemas.
* @returns A Vite plugin that validates environment variables and exposes them to the client.
*
* @example
* ```ts
* // vite.config.ts
* import { defineConfig } from 'vite';
* import arkenv from '@arkenv/vite-plugin';
*
* export default defineConfig({
* plugins: [
* arkenv({
* VITE_API_URL: 'string',
* VITE_API_KEY: 'string',
* }),
* ],
* });
* ```
*
* @example
* ```ts
* // In your client code
* console.log(import.meta.env.VITE_API_URL); // Type-safe access
* ```
*/
export default function arkenv<const T extends Record<string, unknown>>(
options: EnvSchema<T>,
): Plugin;
Expand Down
Loading
Loading