diff --git a/.changeset/array-defaults-support.md b/.changeset/array-defaults-support.md new file mode 100644 index 000000000..2f01bae01 --- /dev/null +++ b/.changeset/array-defaults-support.md @@ -0,0 +1,22 @@ +--- +"arkenv": minor +--- + +#### Support array defaults using type().default() syntax + +Fix to an issue where `type("array[]").default(() => [...])` syntax was not accepted by `createEnv` due to overly restrictive type constraints. The function now accepts any string-keyed record while still maintaining type safety through ArkType's validation system. + +### New Features +- Array defaults to empty using `type("string[]").default(() => [])` syntax +- Support for complex array types with defaults +- Mixed schemas combining string-based and type-based defaults + +### Example + +```typescript +const env = arkenv({ + ALLOWED_ORIGINS: type("string[]").default(() => ["localhost"]), + FEATURE_FLAGS: type("string[]").default(() => []), + PORT: "number.port", +}); +``` diff --git a/apps/playgrounds/node/.env.example b/apps/playgrounds/node/.env.example new file mode 100644 index 000000000..4a191eaba --- /dev/null +++ b/apps/playgrounds/node/.env.example @@ -0,0 +1,3 @@ +HOST=localhost +PORT=3000 +NODE_ENV=development diff --git a/apps/playgrounds/node/index.ts b/apps/playgrounds/node/index.ts index 1199ff0c4..a09dfa791 100644 --- a/apps/playgrounds/node/index.ts +++ b/apps/playgrounds/node/index.ts @@ -1,9 +1,10 @@ -import arkenv from "arkenv"; +import arkenv, { type } from "arkenv"; const env = arkenv({ HOST: "string.host", PORT: "number.port", NODE_ENV: "'development' | 'production' | 'test' = 'development'", + ALLOWED_ORIGINS: type("string[]").default(() => []), }); // Automatically validate and parse process.env @@ -11,7 +12,8 @@ const env = arkenv({ const host = env.HOST; const port = env.PORT; const nodeEnv = env.NODE_ENV; +const allowedOrigins = env.ALLOWED_ORIGINS; -console.log({ host, port, nodeEnv }); +console.log({ host, port, nodeEnv, allowedOrigins }); export default env; diff --git a/apps/www/content/docs/quickstart.mdx b/apps/www/content/docs/quickstart.mdx index 79df20e91..059d02aee 100644 --- a/apps/www/content/docs/quickstart.mdx +++ b/apps/www/content/docs/quickstart.mdx @@ -35,7 +35,7 @@ description: Let's get you started with a few simple steps. Add a schema to make your environment variables **validated** and **typesafe**: ```ts title="config/env.ts" twoslash - import arkenv from 'arkenv'; + import arkenv, { type } from 'arkenv'; export const env = arkenv({ // Built-in validators @@ -48,6 +48,10 @@ description: Let's get you started with a few simple steps. // Optional variables with defaults LOG_LEVEL: "'debug' | 'info' | 'warn' | 'error' = 'info'", + // Array defaults using type function + ALLOWED_ORIGINS: type("string[]").default(() => ["localhost"]), + FEATURE_FLAGS: type("string[]").default(() => []), + // Optional environment variable "API_KEY?": 'string' }); diff --git a/packages/arkenv/src/array-defaults-integration.test.ts b/packages/arkenv/src/array-defaults-integration.test.ts new file mode 100644 index 000000000..ea823bf09 --- /dev/null +++ b/packages/arkenv/src/array-defaults-integration.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; +import arkenv, { type } from "./index"; + +describe("arkenv array defaults", () => { + it("should work with the exact syntax from the GitHub issue", () => { + // This is the exact code from the GitHub issue that should now work + const Thing = arkenv({ + array: type("number.integer[]").default(() => []), + }); + + expect(Thing.array).toEqual([]); + }); + + it("should work with complex array defaults", () => { + const env = arkenv( + { + ALLOWED_HOSTS: type("string[]").default(() => [ + "localhost", + "127.0.0.1", + ]), + FEATURE_FLAGS: type("string[]").default(() => []), + PORTS: type("number[]").default(() => [3000, 8080]), + }, + {}, + ); + + expect(env.ALLOWED_HOSTS).toEqual(["localhost", "127.0.0.1"]); + expect(env.FEATURE_FLAGS).toEqual([]); + expect(env.PORTS).toEqual([3000, 8080]); + }); + + it("should support arrays with defaults and environment overrides", () => { + const env = arkenv( + { + NUMBERS: type("number[]").default(() => [1, 2, 3]), + }, + { + NUMBERS: [4, 5, 6], // Non-string environment for testing + }, + ); + + expect(env.NUMBERS).toEqual([4, 5, 6]); + }); +}); diff --git a/packages/arkenv/src/create-env.test.ts b/packages/arkenv/src/create-env.test.ts index 25a656a45..5b0a917b7 100644 --- a/packages/arkenv/src/create-env.test.ts +++ b/packages/arkenv/src/create-env.test.ts @@ -1,6 +1,7 @@ import { styleText } from "node:util"; import { describe, expect, it } from "vitest"; import { createEnv } from "./create-env"; +import { type } from "./type"; import { indent } from "./utils"; /** @@ -83,4 +84,29 @@ describe("env", () => { expect(TEST_STRING).toBe("hello"); }); + + it("should support array types with default values", () => { + const env = createEnv( + { + NUMBERS: type("number[]").default(() => [1, 2, 3]), + STRINGS: type("string[]").default(() => ["a", "b"]), + }, + {}, + ); + + expect(env.NUMBERS).toEqual([1, 2, 3]); + expect(env.STRINGS).toEqual(["a", "b"]); + }); + + it("should support array types with defaults when no environment value provided", () => { + // Test default value usage when environment variable is not set + const env = createEnv( + { + NUMBERS: type("number[]").default(() => [1, 2, 3]), + }, + {}, + ); + + expect(env.NUMBERS).toEqual([1, 2, 3]); + }); }); diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts index 431962967..d2d0487a9 100644 --- a/packages/arkenv/src/create-env.ts +++ b/packages/arkenv/src/create-env.ts @@ -6,6 +6,11 @@ type RuntimeEnvironment = Record; export type EnvSchema = type.validate; +/** + * 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`. + */ + /** * Create an environment variables object from a schema and an environment * @param def - The environment variable schema @@ -13,7 +18,7 @@ export type EnvSchema = type.validate; * @returns The validated environment variable schema * @throws An {@link ArkEnvError | error} if the environment variables are invalid. */ -export function createEnv>( +export function createEnv>( def: EnvSchema, env: RuntimeEnvironment = process.env, ): distill.Out> { diff --git a/packages/arkenv/src/type.test.ts b/packages/arkenv/src/type.test.ts index 9bece9280..9aff9e222 100644 --- a/packages/arkenv/src/type.test.ts +++ b/packages/arkenv/src/type.test.ts @@ -182,4 +182,14 @@ describe("type", () => { expect(result.NUMBER_VALUE).toBe(42); expect(result.BOOLEAN_VALUE).toBe(true); }); + + it("should work with array defaults using type function", () => { + // This demonstrates the specific issue mentioned in the GitHub issue + const envType = type({ + array: type("number.integer[]").default(() => []), + }); + + const result = envType.assert({}); + expect(result.array).toEqual([]); + }); });