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
29 changes: 29 additions & 0 deletions .changeset/silly-animals-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
"arkenv": patch
---

#### Automatic boolean string conversion

The `boolean` type now accepts `"true"`/`"false"` strings from environment variables and converts them to actual boolean values. This also works with boolean defaults.

Example:

```ts
import arkenv from 'arkenv';

const env = arkenv({
DEBUG: "boolean",
ENABLE_FEATURE: "boolean = true"
});

console.log(env.DEBUG);
console.log(env.ENABLE_FEATURE);
```

Result:

```sh
❯ DEBUG=true npx tsx index.ts
true
true
```
1 change: 1 addition & 0 deletions apps/playgrounds/node/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
HOST=localhost
PORT=3000
NODE_ENV=development
DEBUG=true
11 changes: 9 additions & 2 deletions apps/playgrounds/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const env = arkenv({
PORT: "number.port",
NODE_ENV: "'development' | 'production' | 'test' = 'development'",
ALLOWED_ORIGINS: type("string[]").default(() => []),
DEBUG: "boolean = true",
});

// Automatically validate and parse process.env
Expand All @@ -13,7 +14,13 @@ const host = env.HOST;
const port = env.PORT;
const nodeEnv = env.NODE_ENV;
const allowedOrigins = env.ALLOWED_ORIGINS;

console.log({ host, port, nodeEnv, allowedOrigins });
const debug = env.DEBUG;
console.log({
host,
port,
nodeEnv,
allowedOrigins,
debug,
});

export default env;
3 changes: 3 additions & 0 deletions apps/www/content/docs/how-to/load-environment-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@ export const env = arkenv({
DATABASE_HOST: "string.host",
DATABASE_PORT: "number.port",

// Boolean values (accepts "true"/"false" strings, converts to boolean)
DEBUG: "boolean",

// Optional variables with defaults
LOG_LEVEL: "'debug' | 'info' | 'warn' | 'error' = 'info'",

Expand Down
2 changes: 2 additions & 0 deletions apps/www/content/docs/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"index",
"quickstart",
"examples",
"---API---",
"morphs",
"---[New]Integrations---",
"[Blocks][VS Code & Cursor](/docs/integrations/vscode)",
"[Blocks][JetBrains IDEs](/docs/integrations/jetbrains)",
Expand Down
63 changes: 63 additions & 0 deletions apps/www/content/docs/morphs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
title: Morphs
icon: WandSparkles
---

Since environment variables are defined as strings, ArkEnv automatically transforms certain ArkType keywords when imported through ArkEnv instead of directly from ArkType. These "morphs" provide environment-variable-specific behavior that's optimized for configuration management.

### `boolean`

The `boolean` type automatically morphs (or coalesces) string values from environment variables to boolean.

```ts twoslash
// When using ArkEnv
import arkenv from 'arkenv';

const env = arkenv({ DEBUG: "boolean" });
const isDebug = env.DEBUG;

// Hover to see the type
console.log(isDebug);
```
Result:
```sh
❯ DEBUG=true npx tsx index.ts
true
```

This works by checking if the value is one of the following:
- `"true"`
- `"false"`

You can set boolean defaults:

```ts
const env = arkenv({
DEBUG: "boolean = false",
ENABLE_FEATURES: "boolean = true"
});
```

If you wish to customize this behavior (and allow other string representations of boolean), you can define your own morph function:

```ts
import arkenv, { type } from 'arkenv';

const env = arkenv({
DEBUG: type("'true' | 'false' | '0' | '1'").pipe((str) => str === "true" || str === "1")
});
const isDebug = env.DEBUG;

console.log(isDebug);
```
Result:
```sh
❯ DEBUG=false npx tsx index.ts
false
❯ DEBUG=0 npx tsx index.ts
false
❯ DEBUG=true npx tsx index.ts
true
❯ DEBUG=1 npx tsx index.ts
true
```
4 changes: 4 additions & 0 deletions apps/www/content/docs/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ import { FolderCode, Blocks } from "lucide-react";
DATABASE_HOST: "string.host",
DATABASE_PORT: "number.port",

// Boolean values (accepts "true"/"false" strings, converts to boolean)
DEBUG: "boolean",

// Custom string literals
NODE_ENV: "'development' | 'production' | 'test'",

Expand Down Expand Up @@ -71,6 +74,7 @@ import { FolderCode, Blocks } from "lucide-react";
```dotenv title=".env"
DATABASE_HOST=localhost
DATABASE_PORT=5432
DEBUG=true
NODE_ENV=development
API_KEY=your-secret-key
```
Expand Down
1 change: 1 addition & 0 deletions apps/www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@vercel/speed-insights": "^1.2.0",
"arkdark": "^5.4.2",
"arkenv": "workspace:*",
"arktype": "^2.1.22",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"fumadocs-core": "15.8.3",
Expand Down
3 changes: 2 additions & 1 deletion packages/arkenv/src/scope.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { scope, type } from "arktype";
import { host, port } from "./types";
import { boolean, host, port } from "./types";

// For an explanation of the `$` variable naming convention, see: https://discord.com/channels/957797212103016458/1414659167008063588/1414670282756587581

Expand All @@ -15,4 +15,5 @@ export const $ = scope({
...type.keywords.number,
port,
}),
boolean,
});
31 changes: 27 additions & 4 deletions packages/arkenv/src/type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,16 @@ describe("type", () => {

it("should create a type from a boolean schema", () => {
const envType = type({ DEBUG: "boolean" });
const result = envType.assert({ DEBUG: true });
const result = envType.assert({ DEBUG: "true" });
expect(result.DEBUG).toBe(true);
});

it("should convert 'false' string to false boolean", () => {
const envType = type({ DEBUG: "boolean" });
const result = envType.assert({ DEBUG: "false" });
expect(result.DEBUG).toBe(false);
});

it("should create a type with optional fields", () => {
const envType = type({
REQUIRED: "string",
Expand Down Expand Up @@ -90,7 +96,7 @@ describe("type", () => {
API_URL: "https://api.example.com",
HOST: "localhost",
PORT: "3000",
DEBUG: true,
DEBUG: "true",
// API_KEY is optional, so we can omit it
});

Expand Down Expand Up @@ -120,7 +126,7 @@ describe("type", () => {

expect(() => envType.assert({ PORT: "not-a-number" })).toThrow();
expect(() => envType.assert({ DEBUG: "not-a-boolean" })).toThrow();
expect(() => envType.assert({ PORT: "123", DEBUG: true })).toThrow();
expect(() => envType.assert({ PORT: "123", DEBUG: "invalid" })).toThrow();
});

it("should work with nested object types", () => {
Expand Down Expand Up @@ -175,7 +181,7 @@ describe("type", () => {
const result = envType.assert({
STRING_VALUE: "hello",
NUMBER_VALUE: 42,
BOOLEAN_VALUE: true,
BOOLEAN_VALUE: "true",
});

expect(result.STRING_VALUE).toBe("hello");
Expand All @@ -192,4 +198,21 @@ describe("type", () => {
const result = envType.assert({});
expect(result.array).toEqual([]);
});

it("should work with boolean defaults", () => {
const envType = type({
DEBUG: "boolean = false",
ENABLE_FEATURES: "boolean = true",
});

// Test with no environment variables (should use defaults)
const result1 = envType.assert({});
expect(result1.DEBUG).toBe(false);
expect(result1.ENABLE_FEATURES).toBe(true);

// Test with environment variables (should override defaults)
const result2 = envType.assert({ DEBUG: "true", ENABLE_FEATURES: "false" });
expect(result2.DEBUG).toBe(true);
expect(result2.ENABLE_FEATURES).toBe(false);
});
});
10 changes: 10 additions & 0 deletions packages/arkenv/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,13 @@ export const port = type("string", "=>", (data, ctx) => {
* An IP address or `"localhost"`
*/
export const host = type("string.ip | 'localhost'");

/**
* A boolean that accepts string values and converts them to boolean
* Accepts "true" or "false" strings and converts them to actual boolean values
*/
export const boolean = type(
"'true' | 'false' | true | false",
"=>",
(str) => str === "true" || str === true,
);
5 changes: 4 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
// TypeScript
// See: https://turborepo.com/docs/guides/tools/typescript#linting-your-codebase
"typecheck": {
"dependsOn": ["transit"],
"dependsOn": ["transit", "^build"],
"inputs": ["*.ts", "*.tsx"]
},
// Biome
Expand Down
Loading