Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
8 changes: 8 additions & 0 deletions .changeset/smooth-lamps-go.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@arkenv/vite-plugin": patch
"@arkenv/bun-plugin": patch
---

#### Improve internal types

Fixed "Type instantiation is excessively deep and possibly infinite" errors when using the plugins with complex ArkType schemas. This change improves type stability during the build process by optimizing how generics are passed to the validation logic.
7 changes: 7 additions & 0 deletions .changeset/twelve-tigers-sell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@repo/types": patch
---

#### Add Schema types

Add Schema types to internal types package, including `EnvSchemaWithType` and `SchemaShape`.
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ jobs:
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Build packages
run: pnpm run build --filter=./packages/*
- run: pnpm run test

test-typesafety:
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@ pnpm-debug.log*

# coverage
coverage/

# size-limit
esbuild-why*.html
8 changes: 8 additions & 0 deletions arkenv.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@
"path": "packages/internal/types",
"name": " @repo/types"
},
{
"path": "packages/internal/scope",
"name": " @repo/scope"
},
{
"path": "packages/internal/keywords",
"name": " @repo/keywords"
},
{
"path": "tooling",
"name": "tooling"
Expand Down
2 changes: 0 additions & 2 deletions packages/arkenv/.gitignore

This file was deleted.

2 changes: 2 additions & 0 deletions packages/arkenv/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
"bugs": "https://github.com/yamcodes/arkenv/labels/arkenv",
"author": "Yam Borodetsky <yam@yam.codes>",
"devDependencies": {
"@repo/keywords": "workspace:*",
"@repo/scope": "workspace:*",
"@repo/types": "workspace:*",
"@size-limit/esbuild-why": "catalog:",
"@size-limit/preset-small-lib": "catalog:",
Expand Down
30 changes: 17 additions & 13 deletions packages/arkenv/src/create-env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,12 @@ describe("env", () => {
process.env.TEST_STRING = "hello";
process.env.TEST_PORT = "3000";

const envSchema = type({
const Env = type({
TEST_STRING: "string",
TEST_PORT: "number.port",
});

const env = createEnv(envSchema);
const env = createEnv(Env);

expect(env.TEST_STRING).toBe("hello");
expect(env.TEST_PORT).toBe(3000);
Expand All @@ -129,16 +129,16 @@ describe("env", () => {
process.env.TEST_STRING = "hello";
process.env.TEST_PORT = "3000";

const envSchema = type({
const Env = type({
TEST_STRING: "string",
TEST_PORT: "number.port",
});

const env = createEnv(envSchema);
const env = createEnv(Env);

// TypeScript should infer these correctly
const str: string = env.TEST_STRING;
const port: number = env.TEST_PORT;
const str = env.TEST_STRING;
const port = env.TEST_PORT;

expect(str).toBe("hello");
expect(port).toBe(3000);
Expand All @@ -147,13 +147,17 @@ describe("env", () => {
it("should allow reusing the same type definition multiple times", () => {
process.env.TEST_STRING = "hello";

const envSchema = type({
const Env = type({
TEST_STRING: "string",
});

// Use the same schema multiple times
const env1 = createEnv(envSchema, { TEST_STRING: "first" });
const env2 = createEnv(envSchema, { TEST_STRING: "second" });
const env1 = createEnv(Env, {
TEST_STRING: "first",
});
const env2 = createEnv(Env, {
TEST_STRING: "second",
});

expect(env1.TEST_STRING).toBe("first");
expect(env2.TEST_STRING).toBe("second");
Expand All @@ -162,15 +166,15 @@ describe("env", () => {
it("should throw when type definition validation fails", () => {
process.env.INVALID_PORT = "not-a-port";

const envSchema = type({
const Env = type({
INVALID_PORT: "number.port",
});

expect(() => createEnv(envSchema)).toThrow(/INVALID_PORT/);
expect(() => createEnv(Env)).toThrow(/INVALID_PORT/);
});

it("should work with custom environment and type definitions", () => {
const envSchema = type({
const Env = type({
HOST: "string.host",
PORT: "number.port",
});
Expand All @@ -180,7 +184,7 @@ describe("env", () => {
PORT: "8080",
};

const env = createEnv(envSchema, customEnv);
const env = createEnv(Env, customEnv);

expect(env.HOST).toBe("localhost");
expect(env.PORT).toBe(8080);
Expand Down
35 changes: 18 additions & 17 deletions packages/arkenv/src/create-env.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import type { InferType } from "@repo/types";
import { type distill, type } from "arktype";
import { $ } from "@repo/scope";
import type { EnvSchemaWithType, InferType, SchemaShape } from "@repo/types";
import type { type as at, distill } from "arktype";
import { ArkEnvError } from "./errors";
import { $ } from "./scope";
import { type } from "./type";

export type EnvSchema<def> = at.validate<def, (typeof $)["t"]>;
type RuntimeEnvironment = Record<string, string | undefined>;

export type EnvSchema<def> = type.validate<def, (typeof $)["t"]>;

/**
* TODO: If possible, find a better type than "const T extends Record<string, unknown>",
* TODO: `SchemaShape` is basically `Record<string, unknown>`.
* 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`.
*/

Expand All @@ -19,22 +20,22 @@ export type EnvSchema<def> = type.validate<def, (typeof $)["t"]>;
* @returns The validated environment variable schema
* @throws An {@link ArkEnvError | error} if the environment variables are invalid.
*/
export function createEnv<const T extends Record<string, unknown>>(
def: EnvSchema<T>,
env?: RuntimeEnvironment,
): distill.Out<type.infer<T, (typeof $)["t"]>>;
export function createEnv<T extends type.Any>(
export function createEnv<T extends EnvSchemaWithType>(
def: T,
env?: RuntimeEnvironment,
): InferType<T>;
export function createEnv<const T extends Record<string, unknown>>(
def: EnvSchema<T> | type.Any,
export function createEnv<const T extends SchemaShape>(
def: EnvSchema<T>,
env?: RuntimeEnvironment,
): distill.Out<at.infer<T, (typeof $)["t"]>>;
export function createEnv<const T extends SchemaShape>(
def: EnvSchema<T> | EnvSchemaWithType,
env?: RuntimeEnvironment,
): distill.Out<type.infer<T, (typeof $)["t"]>> | InferType<typeof def>;
export function createEnv<const T extends Record<string, unknown>>(
def: EnvSchema<T> | type.Any,
): distill.Out<at.infer<T, (typeof $)["t"]>> | InferType<typeof def>;
export function createEnv<const T extends SchemaShape>(
def: EnvSchema<T> | EnvSchemaWithType,
env: RuntimeEnvironment = process.env,
): distill.Out<type.infer<T, (typeof $)["t"]>> | InferType<typeof def> {
): distill.Out<at.infer<T, (typeof $)["t"]>> | InferType<typeof def> {
// If def is a type definition (has assert method), use it directly
// Otherwise, use raw() to convert the schema definition
const schema =
Expand Down
2 changes: 1 addition & 1 deletion packages/arkenv/src/type.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { $ } from "./scope";
import { $ } from "@repo/scope";

export const type = $.type;
2 changes: 1 addition & 1 deletion packages/bun-plugin/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe("Bun Plugin", () => {
BUN_PUBLIC_API_URL: "string",
PORT: "number.port",
DATABASE_URL: "string",
});
} as const);

// Check that prefixed variables are present
expect(envMap.has("BUN_PUBLIC_API_URL")).toBe(true);
Expand Down
29 changes: 17 additions & 12 deletions packages/bun-plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
import { join } from "node:path";
import type { EnvSchemaWithType, SchemaShape } from "@repo/types";
import type { EnvSchema } from "arkenv";
import { createEnv } from "arkenv";
import type { type } from "arktype";
import type { BunPlugin, Loader, PluginBuilder } from "bun";

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

/**
* Helper to process env schema and return envMap
*/
export function processEnvSchema(options: EnvSchema<any> | type.Any) {
export function processEnvSchema<T extends SchemaShape>(
options: EnvSchema<T> | EnvSchemaWithType,
): Map<string, string> {
// Validate environment variables
const env = createEnv(options, process.env);

// TODO: We're using type assertions and explicitly pass in the type arguments here to avoid
// "Type instantiation is excessively deep and possibly infinite" errors.
// Ideally, we should find a way to avoid these assertions while maintaining type safety.

const env = createEnv<T>(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(<Record<string, unknown>>env).filter(([key]) =>
key.startsWith(prefix),
),
Object.entries(<SchemaShape>env).filter(([key]) => key.startsWith(prefix)),
);

// Create a map of variable names to their JSON-stringified values
Expand Down Expand Up @@ -81,7 +86,7 @@ function registerLoader(build: PluginBuilder, envMap: Map<string, string>) {
loader,
contents: transformed,
};
} catch (error) {
} catch {
// If file can't be read, return undefined to let Bun handle it
return undefined;
}
Expand Down Expand Up @@ -119,14 +124,14 @@ function registerLoader(build: PluginBuilder, envMap: Map<string, string>) {
* })
* ```
*/
export function arkenv<const T extends Record<string, unknown>>(
export function arkenv(options: EnvSchemaWithType): BunPlugin;
export function arkenv<const T extends SchemaShape>(
options: EnvSchema<T>,
): BunPlugin;
export function arkenv(options: type.Any): BunPlugin;
export function arkenv<const T extends Record<string, unknown>>(
options: EnvSchema<T> | type.Any,
export function arkenv<const T extends SchemaShape>(
options: EnvSchema<T> | EnvSchemaWithType,
): BunPlugin {
const envMap = processEnvSchema(options);
const envMap = processEnvSchema<T>(options);

return {
name: "@arkenv/bun-plugin",
Expand Down
3 changes: 3 additions & 0 deletions packages/internal/keywords/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# `@repo/keywords`

Internal keywords for ArkEnv.
44 changes: 44 additions & 0 deletions packages/internal/keywords/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "@repo/keywords",
"private": true,
"type": "module",
"version": "0.0.0",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"description": "ArkEnv keywords (internal usage)",
"exports": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"scripts": {
"build": "tsdown",
"size": "size-limit",
"typecheck": "tsc --noEmit",
"clean": "rimraf dist node_modules",
"test": "vitest",
"fix": "pnpm -w run fix"
},
"files": [
"dist"
],
"keywords": [
"keywords"
],
"license": "MIT",
"homepage": "https://arkenv.js.org",
"repository": {
"type": "git",
"url": "git+https://github.com/yamcodes/arkenv.git"
},
"author": "Yam Borodetsky <yam@yam.codes>",
"devDependencies": {
"arktype": "catalog:",
"tsdown": "catalog:",
"typescript": "catalog:"
},
"peerDependencies": {
"arktype": "catalog:"
}
Comment thread
yamcodes marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { host, port } from "./types";
import { host, port } from "./index";

describe("port", () => {
it("should validate a valid port number", () => {
Expand Down
File renamed without changes.
19 changes: 19 additions & 0 deletions packages/internal/keywords/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es2020",
"module": "preserve",
"strict": true,
"esModuleInterop": true,
"moduleResolution": "bundler",
"skipLibCheck": true,
"exactOptionalPropertyTypes": true,
"noImplicitAny": true,
"noEmit": true,
"outDir": "dist",
"resolveJsonModule": true,
"rootDir": "src",
"declaration": true,
"declarationMap": true
},
"include": ["src"]
}
10 changes: 10 additions & 0 deletions packages/internal/keywords/tsdown.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from "tsdown";

export default defineConfig({
format: ["esm", "cjs"],
minify: true,
fixedExtension: false,
dts: {
resolve: ["@repo/types"],
},
});
10 changes: 10 additions & 0 deletions packages/internal/keywords/turbo.json
Original file line number Diff line number Diff line change
@@ -0,0 +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
}
}
}
7 changes: 7 additions & 0 deletions packages/internal/keywords/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineProject } from "vitest/config";

export default defineProject({
test: {
name: "@repo/keywords",
},
});
3 changes: 3 additions & 0 deletions packages/internal/scope/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# `@repo/scope`

Internal scope for ArkEnv.
Loading
Loading