diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index beb2907..cd21692 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [20] + node-version: [18, 20] steps: - name: Checkout diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0f4ca21..4ed0785 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,6 +8,9 @@ on: jobs: verify: runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18, 20] permissions: contents: read @@ -18,7 +21,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: 20 + node-version: ${{ matrix.node-version }} cache: npm - name: Verify tag matches package version diff --git a/POST_ROADMAP.md b/POST_ROADMAP.md new file mode 100644 index 0000000..87ba122 --- /dev/null +++ b/POST_ROADMAP.md @@ -0,0 +1,58 @@ +# Post-Roadmap Notes + +This file is intentionally retired from active planning. + +Use [ROADMAP.md](/Users/joshmclain/code/ts-env-validator/ROADMAP.md) as the only roadmap and release-planning source of truth. + +### Testing Utilities + +Future helpers: + +- mock env generator +- test schema validation +- snapshot env configs + +## Ecosystem Plan + +Split into modular packages: + +- `ts-env-validator` (core) +- `@ts-env-validator/cli` +- `@ts-env-validator/next` +- `@ts-env-validator/vite` +- `@ts-env-validator/github-action` +- `@ts-env-validator/docs` + +## High Impact Features (Priority) + +If limited time, prioritize: + +- `.env.example` generation +- CLI (`check`, `generate-example`) +- Next.js adapter +- secret-aware validation +- documentation generation + +## Success Metrics + +- npm downloads growth +- GitHub stars +- usage in real projects +- adoption in CI pipelines +- developer feedback + +## Long-Term Vision + +Position the library as: + +> The standard way to define and validate configuration in TypeScript applications + +## Guiding Principle + +Do not just add validators. + +Build: + +- a system +- a workflow +- a developer habit diff --git a/README.md b/README.md index 248ba25..a2b8670 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Without validation, bad config leaks into runtime and fails late. `ts-env-valida npm install ts-env-validator ``` -Node.js `>=20` is supported. +Node.js `>=18` is supported. ## Quick start @@ -52,7 +52,7 @@ export const env = createEnv({ ENABLE_CACHE: boolean().default(false), MAX_RETRIES: integer().default(3), LATENCY_THRESHOLD: float().default(0.75), - FEATURE_FLAGS: json().optional(), + FEATURE_FLAGS: json<{ enabled: boolean; limit: number }>().optional(), LOG_LEVEL: enumOf(["debug", "info", "warn", "error"]).optional(), }); ``` @@ -79,7 +79,7 @@ env.ALLOWED_HOSTS; // string[] env.FEATURE_FLAGS; -// unknown +// { enabled: boolean; limit: number } | undefined env.LOG_LEVEL; // "debug" | "info" | "warn" | "error" | undefined @@ -194,15 +194,16 @@ DATABASE_URL: url() For example, `"https://example.com"` becomes `"https://example.com/"`. -#### `json()` +#### `json()` -Parses any valid JSON and returns `unknown`. +Parses any valid JSON and returns the caller-provided type. ```ts -FEATURE_FLAGS: json() +FEATURE_FLAGS: json<{ enabled: boolean; limit: number }>() ``` This validator accepts JSON objects, arrays, primitives, and `null`. +Typing is compile-time only; runtime validation is still plain `JSON.parse`. #### `array(separator?)` @@ -218,6 +219,52 @@ SCOPES: array(";") - Empty items are rejected - Separators are treated literally +### Custom validators + +#### `createValidator({ expected, parse })` + +Creates a custom validator that works with `createEnv`, type inference, and all existing modifiers. + +```ts +import { createValidator } from "ts-env-validator"; + +const port = createValidator({ + expected: "port", + parse: (input) => { + const value = Number.parseInt(input, 10); + + if (!Number.isInteger(value) || value < 1 || value > 65535) { + return { + success: false, + message: `expected port, received ${JSON.stringify(input)}`, + }; + } + + return { + success: true, + value, + }; + }, +}); +``` + +Use it in schemas just like a built-in validator: + +```ts +const env = createEnv({ + PORT: port.default(3000), +}); +``` + +Parser contract: + +- `parse` receives the raw string value +- return `{ success: true, value }` on success +- return `{ success: false, message }` on failure +- keep parsers synchronous +- if `parse` throws an `Error`, its `message` is surfaced as the validation failure +- if `parse` throws a non-`Error` value, `ts-env-validator` falls back to `expected ${expected}, received ...` + ### Modifiers #### `.optional()` @@ -308,7 +355,7 @@ export const env = createEnv({ JWT_SECRET: string(), MAX_RETRIES: integer().default(3), LATENCY_THRESHOLD: float().default(0.75), - SERVICE_METADATA: json().optional(), + SERVICE_METADATA: json<{ region: string; team: string }>().optional(), }); ``` @@ -320,7 +367,7 @@ import { array, createEnv, json, string } from "ts-env-validator"; export const env = createEnv({ NEXT_PUBLIC_ENABLED_LOCALES: array(), NEXT_PUBLIC_API_URL: string(), - NEXT_PUBLIC_THEME_CONFIG: json().optional(), + NEXT_PUBLIC_THEME_CONFIG: json<{ accent: string; compact: boolean }>().optional(), }); ``` @@ -334,7 +381,7 @@ npm test npm run build ``` -Maintainers: pushing a version tag like `v0.2.0` runs the publish workflow, releasing `ts-env-validator` to npmjs and `@/ts-env-validator` to GitHub Packages. The published npm tarball includes both `README.md` and `LICENSE`. +Maintainers: pushing a version tag like `v0.3.0` runs the publish workflow, releasing `ts-env-validator` to npmjs and `@/ts-env-validator` to GitHub Packages. The published npm tarball includes both `README.md` and `LICENSE`. ## License diff --git a/ROADMAP.md b/ROADMAP.md index 18204b6..bd3a9f2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,121 +1,96 @@ # ts-env-validator — Roadmap -## 🎯 Vision +`ROADMAP.md` is the single source of truth for planned releases. -A lightweight, TypeScript-first environment variable validator that: -- Validates at runtime -- Infers types at compile time -- Provides excellent developer experience -- Works across Node, Next.js, and serverless environments +## Vision ---- +Build a lightweight, TypeScript-first environment validator that: -## 🚀 Milestones +- validates at runtime +- infers types at compile time +- stays small and framework-agnostic +- gives helpful startup failures instead of late runtime surprises -### v0.1.0 — MVP (Initial Release) +## Shipped -#### Core Features +### v0.1.0 — Core MVP - `createEnv(schema, options?)` -- Validators: - - `string()` - - `number()` - - `boolean()` - - `enumOf([...])` - - `url()` - -#### Modifiers - -- `.optional()` -- `.default(value)` -- `.describe(text)` - -#### Behavior - -- Reads from `process.env` -- Supports custom env object -- Coerces types (string → number/boolean/etc.) -- Collects ALL errors before throwing -- Strong TypeScript inference - -#### DX Features - -- Clean error formatting -- Helpful error messages -- Zero config usage - ---- +- validators: `string()`, `number()`, `boolean()`, `enumOf([...])`, `url()` +- modifiers: `.optional()`, `.default(value)`, `.describe(text)` +- aggregated error reporting +- custom env source support ### v0.2.0 — Extended Types -Completed: - - `integer()` - `float()` -- `json()` +- `json()` - `array(separator?)` ---- +## Next -### v0.3.0 — Extensibility (Next) +### v0.3.0 — Extensibility Foundation -- Custom validator API -- `.transform(fn)` -- `.refine(fn)` +Goal: let users create first-class custom validators without expanding the validation model yet. ---- +- public `createValidator({ expected, parse })` API +- public validator and parser result types +- custom validators work with `createEnv` +- custom validators support `.optional()`, `.default()`, and `.describe()` +- thrown parser errors are normalized into validation failures -### v0.4.0 — Ecosystem Enhancements +Explicitly deferred: -- dotenv helper -- `.env.example` validator CLI -- Next.js helpers (server/client env separation) +- `.transform(fn)` +- `.refine(fn)` +- constraint-style modifiers like `.min()` and `.max()` ---- +## Provisional -### v1.0.0 — Stable Release +### v0.4.0 — Validation Constraints -- Finalized API -- Performance optimizations -- Full documentation -- Community feedback incorporated +Potential focus: ---- +- string and number constraints +- `.transform(fn)` +- `.refine(fn, message?)` +- more composable validator pipelines + +### v0.5.0 — Tooling -## 🧠 Design Principles +Potential focus: -- Minimal API surface -- Strong typing by default -- Fail fast at runtime -- Clear, readable errors -- Framework agnostic -- No unnecessary dependencies +- dotenv helper +- `.env.example` generation or validation +- schema-driven docs or CLI support ---- +### v0.6.0 — Framework DX -## ❌ Explicit Non-Goals (v1) +Potential focus: -- Nested schemas -- Async validation -- Framework-specific wrappers -- Over-engineered abstractions +- Next.js helpers for server and client separation +- prefix enforcement for client-safe env keys +- framework-specific docs, not core package coupling ---- +### v1.0.0 — Stable Release -## 📦 Release Strategy +- stable public API +- polished docs +- performance review +- community feedback incorporated -| Version | Focus | -|--------|------| -| 0.1.0 | Core functionality | -| 0.2.0 | More types | -| 0.3.0 | Extensibility | -| 1.0.0 | Stability | +## Design Principles ---- +- minimal API surface +- strong typing by default +- readable errors +- sync, startup-time validation +- composable internals without overengineering -## 📈 Success Metrics +## Non-Goals -- Easy adoption in new projects -- Clean TypeScript inference -- Positive developer feedback -- GitHub stars / npm downloads +- nested schemas +- async validation +- framework-specific wrappers in the core package +- broad transform pipelines before the base validator contract is stable diff --git a/TASKS.md b/TASKS.md index 0f49ca8..f928a02 100644 --- a/TASKS.md +++ b/TASKS.md @@ -161,7 +161,7 @@ - [x] Add `integer()` - [x] Add `float()` -- [x] Add `json()` +- [x] Add `json()` - [x] Add `array(separator?)` - [x] Reuse existing validator architecture and modifiers - [x] Add unit tests for all new validators diff --git a/examples/next.ts b/examples/next.ts index 15a8dd3..4cd79c0 100644 --- a/examples/next.ts +++ b/examples/next.ts @@ -3,5 +3,5 @@ import { array, createEnv, json, string } from "../src/index"; export const env = createEnv({ NEXT_PUBLIC_ENABLED_LOCALES: array(), NEXT_PUBLIC_API_URL: string(), - NEXT_PUBLIC_THEME_CONFIG: json().optional(), + NEXT_PUBLIC_THEME_CONFIG: json<{ accent: string; compact: boolean }>().optional(), }); diff --git a/examples/node.ts b/examples/node.ts index 4e6ae26..90ba60a 100644 --- a/examples/node.ts +++ b/examples/node.ts @@ -16,5 +16,5 @@ export const env = createEnv({ LATENCY_THRESHOLD: float().default(0.75), MAX_RETRIES: integer().default(3), PORT: number().default(3000), - SERVICE_METADATA: json().optional(), + SERVICE_METADATA: json<{ region: string; team: string }>().optional(), }); diff --git a/package-lock.json b/package-lock.json index cd17652..d9ab197 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ts-env-validator", - "version": "0.2.0", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ts-env-validator", - "version": "0.2.0", + "version": "0.3.0", "license": "MIT", "devDependencies": { "@eslint/js": "^9.39.1", @@ -21,7 +21,7 @@ "vitest": "^3.2.4" }, "engines": { - "node": ">=20" + "node": ">=18" } }, "node_modules/@esbuild/aix-ppc64": { diff --git a/package.json b/package.json index 4ff371a..f3f7fd2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-env-validator", - "version": "0.2.0", + "version": "0.3.0", "description": "Type-safe environment variable validation with runtime checks and TypeScript inference.", "keywords": [ "env", @@ -38,7 +38,7 @@ "LICENSE" ], "engines": { - "node": ">=20" + "node": ">=18" }, "publishConfig": { "access": "public" diff --git a/src/index.ts b/src/index.ts index 2a57f6b..d059335 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ export { array } from "./validators/array"; export { createEnv } from "./create-env"; export { EnvValidationError } from "./errors"; +export { createValidator } from "./validator"; export { boolean } from "./validators/boolean"; export { enumOf } from "./validators/enum"; export { float } from "./validators/float"; @@ -9,3 +10,10 @@ export { json } from "./validators/json"; export { number } from "./validators/number"; export { string } from "./validators/string"; export { url } from "./validators/url"; +export type { + Parser, + ValidationFailure, + ValidationResult, + ValidationSuccess, +} from "./types"; +export type { EnvSchema, InferEnv, InferValidator, Validator } from "./validator"; diff --git a/src/validator.ts b/src/validator.ts index c5c1677..0fb1657 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -1,10 +1,28 @@ import type { Parser, ValidationResult, ValidatorConfig } from "./types"; +import { formatReceivedValue } from "./utils/format-value"; -export class EnvValidator< +export interface Validator< T, TOptional extends boolean = false, THasDefault extends boolean = false, > { + parse(input: string): ValidationResult; + optional(): Validator; + default(value: T): Validator; + describe(text: string): Validator; + readonly expected: string; + readonly description: string | undefined; + readonly hasDefault: boolean; + readonly isOptional: boolean; + readonly defaultValue: T | undefined; +} + +class EnvValidator< + T, + TOptional extends boolean = false, + THasDefault extends boolean = false, +> implements Validator +{ private readonly config: ValidatorConfig; public constructor(config: ValidatorConfig) { @@ -15,13 +33,13 @@ export class EnvValidator< return this.config.parse(input); } - public optional(): EnvValidator { + public optional(): Validator { return this.clone({ isOptional: true, }); } - public default(value: T): EnvValidator { + public default(value: T): Validator { return this.clone({ defaultValue: value, hasDefault: true, @@ -29,7 +47,7 @@ export class EnvValidator< }); } - public describe(text: string): EnvValidator { + public describe(text: string): Validator { return this.clone({ description: text, }); @@ -60,7 +78,7 @@ export class EnvValidator< TNextHasDefault extends boolean = THasDefault, >( overrides: Partial>, - ): EnvValidator { + ): Validator { return new EnvValidator({ ...this.config, ...overrides, @@ -68,9 +86,9 @@ export class EnvValidator< } } -export type AnyValidator = EnvValidator; +export type AnyValidator = Validator; -export type InferValidator = TValidator extends EnvValidator< +export type InferValidator = TValidator extends Validator< infer TValue, infer TOptional, infer THasDefault @@ -91,11 +109,29 @@ export type InferEnv = { export function createValidator(config: { expected: string; parse: Parser; -}): EnvValidator { +}): Validator { + const parse: Parser = (input) => { + try { + return config.parse(input); + } catch (error) { + if (error instanceof Error) { + return { + message: error.message, + success: false, + }; + } + + return { + message: `expected ${config.expected}, received ${formatReceivedValue(input)}`, + success: false, + }; + } + }; + return new EnvValidator({ expected: config.expected, hasDefault: false, isOptional: false, - parse: config.parse, + parse, }); } diff --git a/src/validators/json.ts b/src/validators/json.ts index 5ab3768..842ecea 100644 --- a/src/validators/json.ts +++ b/src/validators/json.ts @@ -1,8 +1,8 @@ import { createValidator } from "../validator"; import { formatReceivedValue } from "../utils/format-value"; -export function json() { - return createValidator({ +export function json() { + return createValidator({ expected: "JSON", parse: (input) => { if (input === "") { @@ -15,7 +15,7 @@ export function json() { try { return { success: true, - value: JSON.parse(input) as unknown, + value: JSON.parse(input) as T, }; } catch { return { diff --git a/test/create-env.test.ts b/test/create-env.test.ts index 6a88187..bbb115b 100644 --- a/test/create-env.test.ts +++ b/test/create-env.test.ts @@ -5,6 +5,7 @@ import { array, boolean, createEnv, + createValidator, enumOf, float, integer, @@ -21,7 +22,7 @@ describe("createEnv", () => { ALLOWED_HOSTS: array(), DATABASE_URL: url(), ENABLE_CACHE: boolean().default(false), - FEATURE_FLAGS: json().optional(), + FEATURE_FLAGS: json<{ enabled: boolean; limit: number }>().optional(), LATENCY_THRESHOLD: float().default(0.75), MAX_RETRIES: integer().default(3), JWT_SECRET: string(), @@ -57,7 +58,9 @@ describe("createEnv", () => { expectTypeOf(env.LATENCY_THRESHOLD).toEqualTypeOf(); expectTypeOf(env.ENABLE_CACHE).toEqualTypeOf(); expectTypeOf(env.ALLOWED_HOSTS).toEqualTypeOf(); - expectTypeOf(env.FEATURE_FLAGS).toEqualTypeOf(); + expectTypeOf(env.FEATURE_FLAGS).toEqualTypeOf< + { enabled: boolean; limit: number } | undefined + >(); expectTypeOf(env.LOG_LEVEL).toEqualTypeOf< "debug" | "info" | "warn" | "error" | undefined >(); @@ -97,10 +100,52 @@ describe("createEnv", () => { expect(env.API_KEY).toBe("test-key"); }); + it("supports custom validators alongside built-in validators", () => { + const port = createValidator({ + expected: "port", + parse: (input) => { + const value = Number.parseInt(input, 10); + + if (!Number.isInteger(value) || value < 1 || value > 65535) { + return { + message: `expected port, received ${JSON.stringify(input)}`, + success: false, + }; + } + + return { + success: true, + value, + }; + }, + }); + + const env = createEnv( + { + API_URL: url(), + LOG_LEVEL: enumOf(["debug", "info"]).optional(), + PORT: port.default(3000), + }, + { + env: { + API_URL: "https://example.com", + }, + }, + ); + + expect(env).toEqual({ + API_URL: "https://example.com/", + LOG_LEVEL: undefined, + PORT: 3000, + }); + expectTypeOf(env.PORT).toEqualTypeOf(); + expectTypeOf(env.LOG_LEVEL).toEqualTypeOf<"debug" | "info" | undefined>(); + }); + it("parses new v0.2.0 validators in mixed schemas", () => { const env = createEnv( { - FEATURE_FLAGS: json(), + FEATURE_FLAGS: json<{ beta: boolean; limit: number }>(), LATENCY_THRESHOLD: float(), MAX_RETRIES: integer(), SCOPES: array(";"), @@ -237,6 +282,73 @@ describe("createEnv", () => { } }); + it("aggregates custom validator failures with missing variables", () => { + const port = createValidator({ + expected: "port", + parse: () => { + throw new Error("port must be between 1 and 65535"); + }, + }); + + try { + createEnv( + { + API_KEY: string().describe("Service API key"), + PORT: port.describe("Application port"), + }, + { + env: { + PORT: "70000", + }, + }, + ); + throw new Error("Expected createEnv to throw"); + } catch (error) { + expect(error).toBeInstanceOf(EnvValidationError); + expect((error as EnvValidationError).message).toBe( + [ + "Environment validation failed", + "", + "Missing required variables:", + "- API_KEY (Service API key)", + "", + "Invalid variables:", + "- PORT (Application port): port must be between 1 and 65535", + ].join("\n"), + ); + } + }); + + it("infers required, optional, and defaulted custom validator results", () => { + const dateString = createValidator({ + expected: "date", + parse: (input) => ({ + success: true, + value: new Date(input), + }), + }); + + const env = createEnv( + { + END_DATE: dateString.optional(), + RELEASE_DATE: dateString, + START_DATE: dateString.default(new Date("2026-01-01T00:00:00.000Z")), + }, + { + env: { + RELEASE_DATE: "2026-04-01T12:00:00.000Z", + }, + }, + ); + + expect(env.END_DATE).toBeUndefined(); + expect(env.RELEASE_DATE).toBeInstanceOf(Date); + expect(env.START_DATE).toBeInstanceOf(Date); + expectTypeOf(env.END_DATE).toEqualTypeOf(); + expectTypeOf(env.RELEASE_DATE).toEqualTypeOf(); + expectTypeOf(env.START_DATE).toEqualTypeOf(); + }); + it("allows optional values to resolve to undefined", () => { const env = createEnv( { diff --git a/test/validators.test.ts b/test/validators.test.ts index 8b95b51..0ae4878 100644 --- a/test/validators.test.ts +++ b/test/validators.test.ts @@ -3,6 +3,7 @@ import { describe, expect, expectTypeOf, it } from "vitest"; import { array, boolean, + createValidator, enumOf, float, integer, @@ -119,8 +120,8 @@ describe("validators", () => { }); }); - it("parses JSON values and returns unknown", () => { - expect(json().parse('{"enabled":true,"count":2}')).toEqual({ + it("parses JSON values and supports caller-provided typing", () => { + expect(json<{ enabled: boolean; count: number }>().parse('{"enabled":true,"count":2}')).toEqual({ success: true, value: { count: 2, @@ -180,6 +181,90 @@ describe("validators", () => { expect(withDefault.defaultValue).toBe(3000); }); + it("creates custom validators with typed outputs", () => { + const port = createValidator({ + expected: "port", + parse: (input) => { + const value = Number.parseInt(input, 10); + + if (!Number.isInteger(value) || value < 1 || value > 65535) { + return { + message: `expected port, received ${JSON.stringify(input)}`, + success: false, + }; + } + + return { + success: true, + value, + }; + }, + }); + + expect(port.parse("3000")).toEqual({ + success: true, + value: 3000, + }); + expect(port.parse("70000")).toEqual({ + message: 'expected port, received "70000"', + success: false, + }); + expectTypeOf(port.parse).toBeCallableWith("3000"); + expectTypeOf(port.default(3000).defaultValue).toEqualTypeOf< + number | undefined + >(); + }); + + it("supports modifier chaining on custom validators", () => { + const base = createValidator({ + expected: "port", + parse: (input) => ({ + success: true, + value: Number.parseInt(input, 10), + }), + }); + const described = base.describe("Application port"); + const optional = described.optional(); + const withDefault = optional.default(3000); + + expect(base.description).toBeUndefined(); + expect(base.isOptional).toBe(false); + expect(base.hasDefault).toBe(false); + + expect(described.description).toBe("Application port"); + expect(optional.isOptional).toBe(true); + expect(withDefault.hasDefault).toBe(true); + expect(withDefault.defaultValue).toBe(3000); + }); + + it("converts thrown parser errors into validation failures", () => { + const validator = createValidator({ + expected: "port", + parse: () => { + throw new Error("port must be between 1 and 65535"); + }, + }); + + expect(validator.parse("70000")).toEqual({ + message: "port must be between 1 and 65535", + success: false, + }); + }); + + it("uses a fallback error when a parser throws a non-Error value", () => { + const validator = createValidator({ + expected: "port", + parse: () => { + throw "boom"; + }, + }); + + expect(validator.parse("70000")).toEqual({ + message: 'expected port, received "70000"', + success: false, + }); + }); + it("exposes helpful error metadata", () => { const error = new EnvValidationError({ invalid: [ @@ -212,9 +297,24 @@ describe("validators", () => { expectTypeOf(array().default(["a"]).defaultValue).toEqualTypeOf< string[] | undefined >(); - expectTypeOf(json().default({ enabled: true } as unknown).defaultValue).toEqualTypeOf< - unknown + expectTypeOf( + json<{ enabled: boolean }>().default({ enabled: true }).defaultValue, + ).toEqualTypeOf< + { enabled: boolean } | undefined >(); + expectTypeOf(createValidator({ + expected: "date", + parse: (input) => ({ + success: true, + value: new Date(input), + }), + })).toMatchTypeOf(createValidator({ + expected: "date", + parse: (input) => ({ + success: true, + value: new Date(input), + }), + })); expectTypeOf(enumOf(["development", "production"])).toMatchTypeOf( enumOf(["development", "production"]), ); diff --git a/tsup.config.ts b/tsup.config.ts index 9722028..e5118fb 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -7,5 +7,5 @@ export default defineConfig({ format: ["esm", "cjs"], outDir: "dist", sourcemap: true, - target: "node20", + target: "node18", });