envox
Version:
Fast and flexible environment variable parser with detailed error reporting.
733 lines (567 loc) • 18.3 kB
Markdown
<p align="center">
<h1 align="center">🌿<br/><code>envox</code></h1>
<p align="center">Fast and flexible environment variable parser with detailed error reporting.</p>
</p>
<br/>
<p align="center">
<a href="https://opensource.org/licenses/MIT" rel="nofollow"><img src="https://img.shields.io/github/license/johnie/envox" alt="License"></a>
<a href="https://www.npmjs.com/package/envox" rel="nofollow"><img src="https://img.shields.io/npm/v/envox.svg" alt="npm"></a>
<a href="https://github.com/johnie/envox/actions"><img src="https://github.com/johnie/envox/actions/workflows/ci.yml/badge.svg" alt="Build Status"></a>
<a href="https://github.com/johnie/envox" rel="nofollow"><img src="https://img.shields.io/github/stars/johnie/envox" alt="stars"></a>
</p>
<br/>
<br/>
## Installation
Install envox with your preferred package manager:
```bash
npm install envox
# or
pnpm add envox
# or
bun add envox
```
For schema validation, install a compatible validation library:
```bash
npm install zod valibot effect
# or your preferred validation library that supports Standard Schema
```
## Quick Start
```typescript
import { parseEnv } from "envox";
const envContent = `
# Database configuration
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp
# API settings
API_KEY="secret-key-123"
DEBUG=true
`;
const result = parseEnv(envContent);
if (result.ok) {
console.log(result.data); // { DB_HOST: 'localhost', DB_PORT: '5432', ... }
console.log(result.vars); // Array of variables with line numbers
} else {
console.error(result.errors); // Parse errors with line numbers
}
```
## Features
- **Flexible parsing** - Handle various .env file formats and process.env objects
- **Detailed error reporting** - Get line numbers and context for errors
- **Variable expansion** - Support for `${VAR}` and `$VAR` syntax
- **Quote handling** - Properly parse single and double quoted values
- **Export support** - Handle `export VAR=value` syntax
- **Schema validation** - Validate and transform parsed values with any [Standard Schema](https://standardschema.dev/) compatible library
- **TypeScript support** - Full type definitions included
- **Zero dependencies** - Lightweight and fast (validation libraries are optional)
## API Reference
### `parseEnv` Function
The main function for parsing environment variables.
```typescript
import { parseEnv } from "envox";
const result = parseEnv(source, options);
```
#### Function Options
```typescript
interface EnvoxOptions<T = Record<string, string>> {
allowEmpty?: boolean; // Default: true
allowComments?: boolean; // Default: true
allowExport?: boolean; // Default: true
trimValues?: boolean; // Default: true
expandVariables?: boolean; // Default: false
schema?: StandardSchemaV1<Record<string, string>, T>; // Optional schema
}
```
#### `parseEnv(source, options)` Parameters
Parse environment variables from a string or object.
```typescript
// Parse from string
const result = parseEnv(`
DATABASE_URL=postgres://localhost/mydb
PORT=3000
`);
// Parse from process.env or any object
const result = parseEnv(process.env);
const result = parseEnv({ API_KEY: "secret", DEBUG: "true" });
```
**Returns:**
```typescript
type ParseResult<T> =
| { ok: true; data: T; vars: EnvVariable[] }
| { ok: false; errors: EnvoxParseError[] };
interface EnvVariable {
key: string;
value: string;
line: number;
}
interface EnvoxParseError {
line: number;
message: string;
content: string;
}
```
### Helper Functions
#### `isEnvFile(content)`
Check if content looks like an environment file.
```typescript
import { isEnvFile } from "envox";
const content = `
NODE_ENV=development
PORT=3000
`;
console.log(isEnvFile(content)); // true
```
#### `fromObject(obj, { includeExport })`
Convert a plain object to environment variable format.
```typescript
import { fromObject } from "envox";
const obj = { API_KEY: "secret", DEBUG: "true" };
const envString = fromObject(obj, { includeExport: true }); // Include 'export' prefix
console.log(envString);
// export API_KEY=secret
// export DEBUG=true
```
## Schema Validation
Envox supports any validation library that implements the [Standard Schema](https://standardschema.dev/) specification. This includes popular libraries like Zod, Valibot, and Effect Schema.
### With Zod
```typescript
import { z } from "zod";
import { parseEnv } from "envox";
// Define your schema
const ConfigSchema = z.object({
NODE_ENV: z.enum(["development", "production", "test"]),
PORT: z.coerce.number().int().positive(),
DATABASE_URL: z.string().url(),
DEBUG: z.coerce.boolean().default(false),
});
const envContent = `
NODE_ENV=development
PORT=3000
DATABASE_URL=https://localhost/db
DEBUG=true
`;
// Parse with validation
const result = parseEnv(envContent, { schema: ConfigSchema });
if (result.ok) {
console.log(result.data); // Fully typed and validated config
console.log(result.data.PORT); // number (3000)
console.log(result.data.DEBUG); // boolean (true)
} else {
console.error("Validation errors:", result.errors);
}
```
### With Valibot
```typescript
import * as v from "valibot";
import { parseEnv } from "envox";
const ConfigSchema = v.object({
API_KEY: v.string(),
MAX_CONNECTIONS: v.pipe(v.string(), v.transform(Number), v.number()),
ENABLE_SSL: v.pipe(
v.string(),
v.transform((val) => val === "true"),
v.boolean()
),
});
const result = parseEnv(envContent, { schema: ConfigSchema });
```
### Custom Schema
You can also create custom schemas that implement the Standard Schema interface:
```typescript
import type { StandardSchemaV1 } from "@standard-schema/spec";
interface MyConfig {
name: string;
version: number;
}
const customSchema: StandardSchemaV1<Record<string, string>, MyConfig> = {
"~standard": {
version: 1,
vendor: "my-validator",
validate: (value) => {
const obj = value as Record<string, string>;
if (!obj.name || !obj.version) {
return {
issues: [
{ message: "name is required", path: ["name"] },
{ message: "version is required", path: ["version"] },
],
};
}
const version = parseInt(obj.version, 10);
if (isNaN(version)) {
return {
issues: [{ message: "version must be a number", path: ["version"] }],
};
}
return {
value: {
name: obj.name,
version: version,
},
};
},
},
};
const result = parseEnv(envContent, { schema: customSchema });
```
## Options
### Option Details
- **`allowEmpty`** - When `true`, lines without `=` are ignored. When `false`, they cause parse errors.
- **`allowComments`** - When `true`, lines starting with `#` are treated as comments.
- **`allowExport`** - When `true`, supports `export VAR=value` syntax.
- **`trimValues`** - When `true`, removes leading/trailing whitespace from values.
- **`expandVariables`** - When `true`, expands `${VAR}` and `$VAR` references.
- **`schema`** - Optional Standard Schema for validation and transformation.
## Working with process.env
Envox can parse and validate variables directly from `process.env`:
```typescript
import { parseEnv } from "envox";
import { z } from "zod";
// Parse process.env with schema validation
const schema = z.object({
PORT: z.coerce.number().default(3000),
NODE_ENV: z.enum(["development", "production"]).default("development"),
API_KEY: z.string().min(1),
});
const result = parseEnv(process.env, { schema });
if (result.ok) {
console.log(result.data); // Typed and validated config
}
// Variable expansion also works with process.env
process.env.API_HOST = "api.example.com";
process.env.API_URL = "https://${API_HOST}/v1";
const expanded = parseEnv(process.env, { expandVariables: true });
if (expanded.ok) {
console.log(expanded.data.API_URL); // "https://api.example.com/v1"
}
```
## Using with dotenv
Envox works great alongside dotenv for enhanced validation and type safety:
```typescript
import "dotenv/config"; // Load .env file into process.env
import { parseEnv } from "envox";
import { z } from "zod";
// Define your configuration schema
const ConfigSchema = z.object({
NODE_ENV: z
.enum(["development", "production", "test"])
.default("development"),
PORT: z.coerce.number().int().min(1).max(65535).default(3000),
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(32),
DEBUG: z.coerce.boolean().default(false),
});
// Validate and transform process.env after dotenv loads it
const result = parseEnv(process.env, { schema: ConfigSchema });
if (result.ok) {
const config = result.data;
// Now you have fully typed and validated configuration
console.log(config.PORT); // TypeScript knows this is a number
console.log(config.DEBUG); // TypeScript knows this is a boolean
export default config;
} else {
console.error("Configuration errors:", result.errors);
process.exit(1);
}
```
### Advanced dotenv integration
```typescript
import dotenv from "dotenv";
import { parseEnv } from "envox";
import { z } from "zod";
// Load different .env files based on environment
const envFile = process.env.NODE_ENV === "test" ? ".env.test" : ".env";
dotenv.config({ path: envFile });
const ConfigSchema = z.object({
DATABASE_URL: z.string().url(),
REDIS_URL: z.string().url().optional(),
JWT_SECRET: z.string().min(32),
PORT: z.coerce.number().default(3000),
// Enable variable expansion for complex configurations
API_ENDPOINT: z.string().url(),
});
// Parse with detailed error reporting
const result = parseEnv(process.env, {
schema: ConfigSchema,
expandVariables: true,
});
if (!result.ok) {
console.error("Configuration errors:");
result.errors.forEach((error) => {
console.error(`- ${error.message} (line ${error.line})`);
});
process.exit(1);
}
const config = result.data;
```
## Variable Expansion
When `expandVariables` is enabled, you can reference previously defined variables:
```typescript
const content = `
BASE_URL=https://api.example.com
API_ENDPOINT=${BASE_URL}/v1
FULL_URL=$API_ENDPOINT/users
`;
const result = parseEnv(content, { expandVariables: true });
if (result.ok) {
console.log(result.data.FULL_URL); // "https://api.example.com/v1/users"
}
```
## Quote Handling
Envox properly handles both single and double quotes:
```typescript
const content = `
SINGLE='value with spaces'
DOUBLE="value with $pecial chars"
ESCAPED="value with \\"quotes\\""
`;
const result = parseEnv(content);
if (result.ok) {
console.log(result.data.SINGLE); // "value with spaces"
console.log(result.data.DOUBLE); // "value with $pecial chars"
console.log(result.data.ESCAPED); // "value with "quotes""
}
```
## Error Handling
Envox provides detailed error information for both parsing and validation errors:
```typescript
const result = parseEnv(`
VALID_VAR=okay
123_INVALID=bad
ANOTHER=fine
`);
if (!result.ok) {
result.errors.forEach((error) => {
console.log(`Line ${error.line}: ${error.message}`);
console.log(`Content: ${error.content}`);
});
}
```
### Validation Error Handling
When using schemas, validation errors are included in the errors array:
```typescript
import { z } from "zod";
import { parseEnv } from "envox";
const schema = z.object({
PORT: z.coerce.number().min(1000),
API_KEY: z.string().min(10),
});
const result = parseEnv(
`
PORT=80
API_KEY=short
`,
{ schema }
);
if (!result.ok) {
result.errors.forEach((error) => {
if (error.line === 0) {
console.log(`Validation error: ${error.message}`);
} else {
console.log(`Parse error at line ${error.line}: ${error.message}`);
}
});
}
```
## Examples
### Basic Usage
```typescript
import { parseEnv } from "envox";
const result = parseEnv(`
NODE_ENV=development
PORT=3000
DATABASE_URL=postgres://localhost/mydb
`);
if (result.ok) {
// Use in your application
const server = createServer();
server.listen(parseInt(result.data.PORT));
}
```
### With Variable Expansion
```typescript
import { parseEnv } from "envox";
const result = parseEnv(
`
APP_NAME=myapp
VERSION=1.0.0
IMAGE_TAG=${APP_NAME}:${VERSION}
CONTAINER_NAME=${APP_NAME}-container
`,
{ expandVariables: true }
);
if (result.ok) {
console.log(result.data.IMAGE_TAG); // "myapp:1.0.0"
console.log(result.vars.find((v) => v.key === "IMAGE_TAG")?.value); // "myapp:1.0.0"
}
```
### Comprehensive Example with Zod
```typescript
import { z } from "zod";
import { parseEnv } from "envox";
// Define a comprehensive schema
const AppConfigSchema = z.object({
// Server settings
NODE_ENV: z
.enum(["development", "production", "test"])
.default("development"),
PORT: z.coerce.number().int().min(1).max(65535).default(3000),
HOST: z.string().default("localhost"),
// Database
DATABASE_URL: z.string().url(),
DB_POOL_SIZE: z.coerce.number().int().min(1).max(100).default(10),
// API settings
API_KEY: z.string().min(32),
API_TIMEOUT: z.coerce.number().int().min(1000).default(5000),
// Feature flags
ENABLE_LOGGING: z.coerce.boolean().default(true),
ENABLE_METRICS: z.coerce.boolean().default(false),
// Optional settings
REDIS_URL: z.string().url().optional(),
SENTRY_DSN: z.string().url().optional(),
});
type AppConfig = z.infer<typeof AppConfigSchema>;
// Load and validate environment
const envContent = `
NODE_ENV=production
PORT=8080
DATABASE_URL=postgres://user:pass@localhost:5432/myapp
API_KEY=super-secret-api-key-that-is-long-enough
ENABLE_LOGGING=true
ENABLE_METRICS=true
`;
const result = parseEnv(envContent, { schema: AppConfigSchema });
if (result.ok) {
const config: AppConfig = result.data;
console.log("Configuration loaded successfully:");
console.log(`Server will run on ${config.HOST}:${config.PORT}`);
console.log(`Environment: ${config.NODE_ENV}`);
console.log(`Logging enabled: ${config.ENABLE_LOGGING}`);
// All values are properly typed and validated
startServer(config);
} else {
console.error("Failed to load configuration:");
result.errors.forEach((error) => console.error(error.message));
process.exit(1);
}
function startServer(config: AppConfig) {
// Your application logic here
// All config values are guaranteed to be valid
}
```
### Advanced Schema with Transformations
```typescript
import { z } from "zod";
import { parseEnv } from "envox";
const AdvancedSchema = z.object({
// Transform comma-separated values to array
ALLOWED_ORIGINS: z
.string()
.transform((val) => val.split(",").map((s) => s.trim())),
// Parse JSON configuration
FEATURE_FLAGS: z.string().transform((val) => {
try {
return JSON.parse(val);
} catch {
throw new Error("FEATURE_FLAGS must be valid JSON");
}
}),
// Custom validation for log level
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
// Transform string to Date
DEPLOYMENT_DATE: z
.string()
.datetime()
.transform((val) => new Date(val)),
// Conditional validation
CACHE_TTL: z.coerce.number().min(60).max(86400),
});
const result = parseEnv(
`
ALLOWED_ORIGINS=https://example.com, https://app.example.com
FEATURE_FLAGS={"newUI": true, "betaFeatures": false}
LOG_LEVEL=info
DEPLOYMENT_DATE=2024-01-15T10:30:00Z
CACHE_TTL=3600
`,
{ schema: AdvancedSchema }
);
if (result.ok) {
console.log(result.data.ALLOWED_ORIGINS); // ['https://example.com', 'https://app.example.com']
console.log(result.data.FEATURE_FLAGS); // { newUI: true, betaFeatures: false }
console.log(result.data.DEPLOYMENT_DATE); // Date object
}
```
### Error Handling with Detailed Reporting
```typescript
import { z } from "zod";
import { parseEnv } from "envox";
const StrictSchema = z.object({
API_URL: z.string().url("Must be a valid URL"),
MAX_RETRIES: z.coerce.number().int().min(1).max(10),
TIMEOUT_MS: z.coerce.number().int().min(100),
});
const problematicEnv = `
# This will have multiple issues
API_URL=not-a-url
MAX_RETRIES=20
TIMEOUT_MS=50
123_INVALID=value
`;
const result = parseEnv(problematicEnv, { schema: StrictSchema });
if (!result.ok) {
console.log("Environment validation failed:");
result.errors.forEach((error, index) => {
if (error.line > 0) {
console.log(
`${index + 1}. Parse error (line ${error.line}): ${error.message}`
);
} else {
console.log(`${index + 1}. Validation error: ${error.message}`);
}
});
// Output might be:
// 1. Parse error (line 6): Invalid environment variable key: 123_INVALID
// 2. Validation error: Must be a valid URL
// 3. Validation error: Number must be less than or equal to 10
// 4. Validation error: Number must be greater than or equal to 100
}
```
## TypeScript Integration
Envox provides excellent TypeScript support with full type inference when using schemas:
```typescript
import { z } from "zod";
import { parseEnv } from "envox";
const ConfigSchema = z.object({
API_KEY: z.string(),
PORT: z.coerce.number(),
DEBUG: z.coerce.boolean(),
});
const result = parseEnv(envContent, { schema: ConfigSchema });
if (result.ok) {
// Type is automatically inferred as:
// { API_KEY: string; PORT: number; DEBUG: boolean; }
const config = result.data;
// TypeScript knows the exact types
config.PORT.toFixed(2); // ✅ number method
config.DEBUG ? "yes" : "no"; // ✅ boolean
config.API_KEY.toLowerCase(); // ✅ string method
}
```
## Performance
Envox is designed to be fast and lightweight:
- Zero dependencies for core functionality
- Efficient parsing with minimal allocations
- Optional schema validation only when needed
- Supports both sync workflows
```typescript
// For performance-critical applications, you can skip validation
const result = parseEnv(content); // No schema = faster parsing
// Or use validation only in development
const schema = process.env.NODE_ENV === "development" ? MySchema : undefined;
const result = parseEnv(content, { schema });
```
## Contributing
We welcome contributions! Please see our [Contributing Guide](https://github.com/johnie/envox/blob/main/CONTRIBUTING.md) for details.
## License
MIT License. See the [LICENSE](https://github.com/johnie/envox/blob/main/LICENSE) file for details.