diff --git a/.changeset/selectconfig-sync-config.md b/.changeset/selectconfig-sync-config.md new file mode 100644 index 0000000..be4dd5d --- /dev/null +++ b/.changeset/selectconfig-sync-config.md @@ -0,0 +1,20 @@ +--- +"@vlandoss/env": minor +--- + +Add `selectConfig(configs)` — a synchronous, runtime-agnostic counterpart to `loadConfig` for config files that can't use top-level `await`. + +Some tooling loads its config file via `require()` or by bundling it to CJS, where `await loadConfig(...)` is rejected (`ERR_REQUIRE_ASYNC_MODULE` / "top-level await is not supported with the cjs output format"). `selectConfig` pairs with static `import`s and picks the entry matching `envName()` — no dynamic `import()`, no `await`: + +```ts +import { defineEnv, selectConfig } from "@vlandoss/env"; +import development from "./config/development.ts"; +import production from "./config/production.ts"; + +export const env = defineEnv({ + schema: Env, + config: selectConfig({ development, production }), +}); +``` + +It throws when the current env has no entry. See the new `config-cjs` example and the `loadConfig` guide for details. diff --git a/docsite/content/docs/api-reference/core.mdx b/docsite/content/docs/api-reference/core.mdx index 7100a91..6c09472 100644 --- a/docsite/content/docs/api-reference/core.mdx +++ b/docsite/content/docs/api-reference/core.mdx @@ -12,6 +12,7 @@ The `@vlandoss/env` entrypoint is the only one you always import — it contains | ------------ | -------- | ------------------------------------------------------------------------------------ | | `schema` | Function | Declare the contract — branches and Standard Schema leaves. | | `defineEnv` | Function | Merge config + environment variables, validate, and return a typed `env` object. | +| `selectConfig` | Function | Synchronously pick the current env's config from a map of statically-imported configs. The no-`await` counterpart to `loadConfig` for config files that tooling loads via `require()` or bundles to CJS. | | `envName` | Function | Detect the current environment name across runtimes — `development`, `production`, … | | `readEnv` | Function | Read raw values from the active source (`process.env` on the server, `window.__env` in the browser). Returns `{}` on runtimes without `process` (Workers) — pass `runtimeEnv` to `defineEnv` instead. | | `Config` | Type | The input shape of per-environment config files for a given schema. | diff --git a/docsite/content/docs/guides/fs-loadconfig.mdx b/docsite/content/docs/guides/fs-loadconfig.mdx index 1f9eb90..7a27745 100644 --- a/docsite/content/docs/guides/fs-loadconfig.mdx +++ b/docsite/content/docs/guides/fs-loadconfig.mdx @@ -87,8 +87,30 @@ The pattern **must** contain `{env}`, and it throws if the resolved file doesn't `loadConfig` always reads `envName()`. To load a non-current env, set `ENV=…` in the process env before calling — there's no `env` override on the function itself. +## Config files that can't use top-level `await` (`selectConfig`) + +`loadConfig` is async — it uses dynamic `import()` under the hood, which is irreducibly asynchronous. That's fine in app code, but it breaks in **config files that tooling loads synchronously** — files pulled in via `require()` or bundled to CJS, where a top-level `await` is rejected (`ERR_REQUIRE_ASYNC_MODULE`, or a build-time _"top-level await is not supported with the cjs output format"_). Some database/ORM tooling configs fall in this bucket. + +There's no runtime-agnostic way to make `loadConfig` synchronous — a sync module load means `require()`, which only exists on CJS-capable runtimes and can't load `.ts` without a transpile hook. So for these files, use **`selectConfig`** instead: the synchronous, runtime-agnostic counterpart. Pair it with **static `import`s** so the runtime/bundler resolves and transpiles each config at parse time — no dynamic import, no `await`: + +```ts title="src/env/index.ts (imported by the tool's config file)" +import { defineEnv, selectConfig } from "@vlandoss/env"; +import development from "./config/development.ts"; +import production from "./config/production.ts"; +import { Env } from "./env/schema.ts"; + +export const env = defineEnv({ + schema: Env, + config: selectConfig({ development, production }), +}); +``` + +`selectConfig` picks the entry matching `envName()` and **throws** if there's none (the map is explicit, so a miss is almost always a typo). Like `loadConfig`, it has no `env` override — set `ENV=…` first to select a non-current env. + +See the [`config-cjs` example](https://github.com/variableland/env/tree/main/examples/config-cjs) for a runnable reproduction (it asserts the `loadConfig` + top-level await variant throws under `require()`, and the `selectConfig` variant loads). + ## Tradeoffs - Requires a runtime with a filesystem — works on Node, Bun, and Deno; not on Workers/Edge. Use the [Vite plugin](/docs/api-reference/vite) for those instead. - Loading `.ts` / `.mts` config files depends on the host runtime's TypeScript support: native in Bun and Deno, behind `--experimental-strip-types` in Node ≥22.6 (stable in 23.6). `.js` / `.mjs` / `.json` work everywhere. -- The startup is async because `loadConfig` is async. If you can't use top-level `await`, hoist the boot into an `async function main()` and call it from your entrypoint. +- The startup is async because `loadConfig` is async. If you can't use top-level `await`, hoist the boot into an `async function main()` and call it from your entrypoint — or, for config files loaded synchronously by tooling, use [`selectConfig`](#config-files-that-cant-use-top-level-await-selectconfig) with static imports. diff --git a/examples/README.md b/examples/README.md index b8176dd..b17b90f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,6 +1,6 @@ # `@vlandoss/env` examples -Real-world usage examples for [`@vlandoss/env`](../package). **Each example is runtime-isolated**: it declares its own runtime in a local `mise.toml`, brings its own package manager and lockfile, and consumes `@vlandoss/env` from a packed tarball — exactly as an external consumer would. End-to-end tests use Playwright and exercise real `env` failure modes (missing required vars, wrong types, per-mode config isolation, SSR↔client hydration drift). +Real-world usage examples for [`@vlandoss/env`](../package). **Each example is runtime-isolated**: it declares its own runtime in a local `mise.toml`, brings its own package manager and lockfile, and consumes `@vlandoss/env` from a packed tarball — exactly as an external consumer would. End-to-end tests use Playwright (and `node:test` for the `config-cjs` loader case) and exercise real `env` failure modes (missing required vars, wrong types, per-mode config isolation, SSR↔client hydration drift, config files that can't use top-level await). | Example | Runtime | Package manager | `env` entries exercised | | -------------------------------------------- | ---------------------------------------- | --------------- | -------------------------------------------------------------------------------- | @@ -13,8 +13,9 @@ Real-world usage examples for [`@vlandoss/env`](../package). **Each example is r | [`spa-vite-dynamic`](./spa-vite-dynamic) | Node.js (Vite) | pnpm | `@vlandoss/env` (dynamic `import()`) + `@vlandoss/env/vite` (for `__ENV_NAME__`) | | [`ssr-react-router`](./ssr-react-router) | Node.js (React Router 7) | pnpm | `@vlandoss/env` + `@vlandoss/env/fs` + `@vlandoss/env/react` (``) | | [`ssr-tanstack-start`](./ssr-tanstack-start) | Node.js (TanStack Start) | pnpm | `@vlandoss/env` + `@vlandoss/env/vite` + `@vlandoss/env/react` | +| [`config-cjs`](./config-cjs) | Node.js 22.22.3 | pnpm | `@vlandoss/env` (`selectConfig`) + `@vlandoss/env/fs` (`loadConfig`) + `@vlandoss/env/zod` | -The `backend-*`, `worker-*`, and `edge-nextjs` examples drive Playwright in HTTP-only mode (the `request` fixture); the 4 SPA / SSR examples drive a real chromium browser. +The `backend-*`, `worker-*`, and `edge-nextjs` examples drive Playwright in HTTP-only mode (the `request` fixture); the 4 SPA / SSR examples drive a real chromium browser. `config-cjs` is the odd one out — it runs `node:test` to assert config-file loading semantics under `require()` (no browser, no server). ## How `@vlandoss/env` is consumed @@ -33,7 +34,7 @@ The tarball is generated by `mise run env:pack` (which calls `pnpm pack` inside ```sh mise install # node + pnpm; bun/deno installed per-example as needed mise run setup # bootstraps everything: tools, root deps, env tarball, all examples, Playwright browsers -mise run test:e2e # runs all 9 e2e suites +mise run test:e2e # runs all 10 e2e suites ``` ## Run a single example diff --git a/examples/config-cjs/.gitignore b/examples/config-cjs/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/examples/config-cjs/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/examples/config-cjs/README.md b/examples/config-cjs/README.md new file mode 100644 index 0000000..6e40f29 --- /dev/null +++ b/examples/config-cjs/README.md @@ -0,0 +1,54 @@ +# config-cjs + +Loading `@vlandoss/env` from a **config file that can't use top-level `await`** — +the kind of config file that tooling pulls in via `require()` or bundles to CJS +(e.g. some ORM / database migration tooling). + +## The problem + +```ts title="db.config.broken.mts" +const config = await loadConfig(Env); // ⛔ top-level await +``` + +`loadConfig` is async (it uses dynamic `import()` under the hood, which is +irreducibly asynchronous). The top-level `await` makes the module an **async ES +module**. When a tool loads the config synchronously, that's rejected: + +- `require()` → `ERR_REQUIRE_ASYNC_MODULE` (Node ≥22.12, where `require(esm)` is on) +- esbuild/CJS bundle → _"Top-level await is currently not supported with the cjs output format"_ + +## The fix: `selectConfig` + +`selectConfig` is the synchronous, runtime-agnostic counterpart to `loadConfig`. +Pair it with **static `import`s** so the runtime/bundler resolves and transpiles +each config file at parse time — no dynamic import, no `await`: + +```ts title="src/env/index.ts" +import { defineEnv, selectConfig } from "@vlandoss/env"; +import development from "../../config/development.ts"; +import production from "../../config/production.ts"; +import { Env } from "./schema.ts"; + +export const env = defineEnv({ + schema: Env, + config: selectConfig({ development, production }), +}); +``` + +`db.config.mts` imports that `env` and exports a plain config object — no +top-level await anywhere — so `require()` loads it cleanly. + +## Run it + +```sh +mise run //examples/config-cjs:test:e2e +``` + +[`test/loader.test.ts`](./test/loader.test.ts) `require()`s both configs and +asserts: the `selectConfig` one loads, the `loadConfig` + top-level await one +throws `ERR_REQUIRE_ASYNC_MODULE`. + +> Pinned to Node 22.22.3 — `require(esm)` is default from 22.12 and native TS +> stripping from 22.18, so this is the lowest LTS line where the reproduction +> (and the fix) behaves as described. Node 20 would throw `ERR_REQUIRE_ESM` +> instead, which is a different failure. diff --git a/examples/config-cjs/biome.json b/examples/config-cjs/biome.json new file mode 100644 index 0000000..5b190a0 --- /dev/null +++ b/examples/config-cjs/biome.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.15/schema.json", + "extends": ["@rrlab/biome-config"] +} diff --git a/examples/config-cjs/config/development.ts b/examples/config-cjs/config/development.ts new file mode 100644 index 0000000..1087937 --- /dev/null +++ b/examples/config-cjs/config/development.ts @@ -0,0 +1,6 @@ +import type { EnvConfig } from "../src/env/schema.ts"; + +export default { + server: { PORT: 3001, HOST: "127.0.0.1" }, + db: { URL: "postgres://localhost/dev", LOGGING: true }, +} satisfies EnvConfig; diff --git a/examples/config-cjs/config/production.ts b/examples/config-cjs/config/production.ts new file mode 100644 index 0000000..36f7d90 --- /dev/null +++ b/examples/config-cjs/config/production.ts @@ -0,0 +1,6 @@ +import type { EnvConfig } from "../src/env/schema.ts"; + +export default { + server: { PORT: 3001, HOST: "0.0.0.0" }, + db: { LOGGING: false }, +} satisfies EnvConfig; diff --git a/examples/config-cjs/db.config.broken.mts b/examples/config-cjs/db.config.broken.mts new file mode 100644 index 0000000..0c56642 --- /dev/null +++ b/examples/config-cjs/db.config.broken.mts @@ -0,0 +1,19 @@ +import { defineEnv } from "@vlandoss/env"; +import { loadConfig } from "@vlandoss/env/fs"; +import { Env } from "./src/env/schema.ts"; + +// ❌ The pattern that BREAKS in a config file. `loadConfig` is async, so this +// top-level `await` turns the module into an async ES module. Tooling that +// loads the config via `require()` throws `ERR_REQUIRE_ASYNC_MODULE`, and +// bundlers targeting CJS reject the top-level await at build time. +// +// Kept here only to demonstrate the failure — see `db.config.mts` for the +// `selectConfig` fix and `test/loader.test.ts` for the assertion. +const config = await loadConfig(Env); + +const env = defineEnv({ schema: Env, config }); + +export default { + dialect: "postgresql", + dbCredentials: { url: env.db.URL }, +} as const; diff --git a/examples/config-cjs/db.config.mts b/examples/config-cjs/db.config.mts new file mode 100644 index 0000000..086c000 --- /dev/null +++ b/examples/config-cjs/db.config.mts @@ -0,0 +1,14 @@ +import { env } from "./src/env/index.ts"; + +// Stand-in for an ORM / database tooling config: a file that tooling loads via +// `require()` or bundles to CJS. Because `env` is built with `selectConfig` +// (synchronous), importing it here adds no top-level `await`, so this module +// loads cleanly in a CJS / no-TLA context. +// +// Shape the default export however your tool expects — this mirrors a typical +// database migration config. +export default { + dialect: "postgresql", + dbCredentials: { url: env.db.URL }, + verbose: env.db.LOGGING, +} as const; diff --git a/examples/config-cjs/mise.toml b/examples/config-cjs/mise.toml new file mode 100644 index 0000000..09d0f4c --- /dev/null +++ b/examples/config-cjs/mise.toml @@ -0,0 +1,37 @@ +# Pinned to 22.22.3 on purpose: this example reproduces a `require()`-time +# failure that only surfaces with `require(esm)` (default since 22.12) AND native +# TypeScript stripping (default since 22.18). An older 22.x would either resolve +# the .mts as plain JS (syntax error) or report a different error code. +[tools] +node = "22.22.3" +pnpm = "10.30.3" + +[env] +_.path = ["{{config_root}}/node_modules/.bin"] + +[tasks.setup] +description = "Pack env + install deps (pnpm)" +depends = ["//:env:pack"] +sources = [ + "package.json", + "pnpm-lock.yaml", + "{{config_root}}/../../package/.local/vlandoss-env.tgz", +] +outputs = ["node_modules/.modules.yaml"] +run = "pnpm install --ignore-workspace --no-frozen-lockfile" + +[tasks.reinstall] +description = "Force-reinstall the env tarball" +depends = ["//:env:pack"] +run = "pnpm install --ignore-workspace --no-frozen-lockfile --force" + +[tasks."test:e2e"] +description = "Assert the config loads under require() (selectConfig) and that the loadConfig+TLA variant is rejected" +depends = ["setup"] +env = { ENV = "development" } +run = "node --test test/loader.test.ts" + +[tasks.check] +description = "JS & TS check" +depends = ["setup"] +run = "rr check" diff --git a/examples/config-cjs/package.json b/examples/config-cjs/package.json new file mode 100644 index 0000000..e0f2561 --- /dev/null +++ b/examples/config-cjs/package.json @@ -0,0 +1,20 @@ +{ + "name": "example-config-cjs", + "version": "0.0.0", + "private": true, + "type": "module", + "dependencies": { + "@vlandoss/env": "file:../../package/.local/vlandoss-env.tgz", + "zod": "4.3.6" + }, + "devDependencies": { + "@biomejs/biome": "^2.0.0", + "@rrlab/biome-config": "^0.0.2", + "@rrlab/biome-plugin": "^0.1.1", + "@rrlab/cli": "0.0.3", + "@rrlab/ts-config": "^0.0.2", + "@rrlab/ts-plugin": "^0.1.1", + "@types/node": "24.12.4", + "typescript": "^6.0.0" + } +} diff --git a/examples/config-cjs/pnpm-lock.yaml b/examples/config-cjs/pnpm-lock.yaml new file mode 100644 index 0000000..92ce6c3 --- /dev/null +++ b/examples/config-cjs/pnpm-lock.yaml @@ -0,0 +1,990 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@vlandoss/env': + specifier: file:../../package/.local/vlandoss-env.tgz + version: file:../../package/.local/vlandoss-env.tgz(zod@4.3.6) + zod: + specifier: 4.3.6 + version: 4.3.6 + devDependencies: + '@biomejs/biome': + specifier: ^2.0.0 + version: 2.4.15 + '@rrlab/biome-config': + specifier: ^0.0.2 + version: 0.0.2(@biomejs/biome@2.4.15) + '@rrlab/biome-plugin': + specifier: ^0.1.1 + version: 0.1.1(@biomejs/biome@2.4.15)(@pnpm/logger@1001.0.1)(@rrlab/cli@0.0.3(@pnpm/logger@1001.0.1)) + '@rrlab/cli': + specifier: 0.0.3 + version: 0.0.3(@pnpm/logger@1001.0.1) + '@rrlab/ts-config': + specifier: ^0.0.2 + version: 0.0.2(@types/node@24.12.4)(typescript@6.0.3) + '@rrlab/ts-plugin': + specifier: ^0.1.1 + version: 0.1.1(@pnpm/logger@1001.0.1)(@rrlab/cli@0.0.3(@pnpm/logger@1001.0.1))(typescript@6.0.3) + '@types/node': + specifier: 24.12.4 + version: 24.12.4 + typescript: + specifier: ^6.0.0 + version: 6.0.3 + +packages: + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@bgotink/kdl@0.4.0': + resolution: {integrity: sha512-F0uJCjo5FQvFdcGF5QbYVNfcGiRWlocuzyIdQxottZF2+gu6L2xjMGEu9PIpse2hifAca/19vIospgaETCKxIg==} + + '@biomejs/biome@2.4.15': + resolution: {integrity: sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.4.15': + resolution: {integrity: sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.4.15': + resolution: {integrity: sha512-/5KHXYMfSJs1fNXiX30xFtI8JcCFV6zaVVLxOa0M2sfqBKHkpQhRTv94yxQWxeTY2lzo2OuTlNvPC+hDQt2wcQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.4.15': + resolution: {integrity: sha512-ZPcxznxm0pogHBLZhYntyR3sR+MrZjqJIKEr7ZqVen0Rl+P/4upVmfYXjftizi9RoqZntg33fv/1fbdhbYXpEQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.4.15': + resolution: {integrity: sha512-owaAMZD/T4LrD0ELNCk0Km3qrRHuM0X6EAyVE1FSqGY0rbLoiDLrO4Us2tllm6cAeB2Ioa9C2C08NZPdr8+0Ug==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.4.15': + resolution: {integrity: sha512-CNq/9W38SYSH023lfcQ4KKU8K0YX8T//FZUhcgtMMRABDojx5XsMV7jlweAvGSl389wJQB29Qo6Zb/a+jdvt+w==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.4.15': + resolution: {integrity: sha512-0jj7THz12GbUOLmMibktK6DZjqz2zV64KFxyBtcFTKPiiOIY0a7vns1elpO1dERvxpsZ5ik0oFfz0oGwFde1+g==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.4.15': + resolution: {integrity: sha512-ouhkYdlhp/1GghEJPdWwD/Vi3gQ1nFxuSpMolWsbq3Lsq3QUR4jl6UdhhscdCugKU5vOEuMiJhvKj66O0OCq+w==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.4.15': + resolution: {integrity: sha512-zBrGq5mx5wwpnow4+2BxUvleDM+GNd4sLbPaMapsSLQLD0NGRCquqPBTgN+7XkUteHvj7M+BstuI8tmnV7+HgQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@clack/core@0.5.0': + resolution: {integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==} + + '@clack/prompts@0.11.0': + resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==} + + '@gwhitney/detect-indent@7.0.1': + resolution: {integrity: sha512-7bQW+gkKa2kKZPeJf6+c6gFK9ARxQfn+FKy9ScTBppyKRWH2KzsmweXUoklqeEiHiNVWaeP5csIdsNq6w7QhzA==} + engines: {node: '>=12.20'} + + '@pnpm/constants@1001.3.1': + resolution: {integrity: sha512-2hf0s4pVrVEH8RvdJJ7YRKjQdiG8m0iAT26TTqXnCbK30kKwJW69VLmP5tED5zstmDRXcOeH5eRcrpkdwczQ9g==} + engines: {node: '>=18.12'} + + '@pnpm/core-loggers@1001.0.9': + resolution: {integrity: sha512-pW58m3ssrwVjwhlmTXDW1dh1sv2y6R2Gl5YvQInjM2d01/5mre/sYAY4MK3XfgEShZJQxv6wVXDUvyHHJ0oizg==} + engines: {node: '>=18.12'} + peerDependencies: + '@pnpm/logger': '>=1001.0.0 <1002.0.0' + + '@pnpm/error@1000.1.0': + resolution: {integrity: sha512-Dqc2IJJPjUatwc9Letw+vG29rnaMrDGi5g6WCx1HiZYm0obXbTmLygeRafMbgf+sLKXrWE1shOeiayQuczBdoA==} + engines: {node: '>=18.12'} + + '@pnpm/fs.find-packages@1000.0.24': + resolution: {integrity: sha512-6r2lpvoljgTvQ+CiJYz3jCunzO1PM6g1Cqc3xon49he8sgg8BatMsNxcGnuZWK//du80+ylS/uBXKxwuHMuHUw==} + engines: {node: '>=18.12'} + + '@pnpm/graceful-fs@1000.1.0': + resolution: {integrity: sha512-EsMX4slK0qJN2AR0/AYohY5m0HQNYGMNe+jhN74O994zp22/WbX+PbkIKyw3UQn39yQm2+z6SgwklDxbeapsmQ==} + engines: {node: '>=18.12'} + + '@pnpm/logger@1001.0.1': + resolution: {integrity: sha512-gdwlAMXC4Wc0s7Dmg/4wNybMEd/4lSd9LsXQxeg/piWY0PPXjgz1IXJWnVScx6dZRaaodWP3c1ornrw8mZdFZw==} + engines: {node: '>=18.12'} + + '@pnpm/manifest-utils@1002.0.5': + resolution: {integrity: sha512-2DSwQ6pP73IuJS5mCCtPd5fibJwuAdufXKuSL/Oq1n6AggCqy8616Xea1X3RH3z5dL4mn7Z4EZ+vnX8jX3Wrfw==} + engines: {node: '>=18.12'} + peerDependencies: + '@pnpm/logger': ^1001.0.1 + + '@pnpm/read-project-manifest@1001.2.6': + resolution: {integrity: sha512-BcNO50lAkE4m9JaJ0WmG3m/DH/qLSvMgZywtmb/dfyyLVu5nDZfDqmOd8U+f1NhLcLMbBK6AnS3hyUqZYvw9Vg==} + engines: {node: '>=18.12'} + peerDependencies: + '@pnpm/logger': ^1001.0.1 + + '@pnpm/semver.peer-range@1000.0.0': + resolution: {integrity: sha512-r6VzkrdH7ZKjPmAogTNvxuV/UyS/xwHNme+ZuEFiG0UthZgqudDftYtKmG20fcfrjG1lgJbbWICA8KvZy7mmbw==} + engines: {node: '>=18.12'} + + '@pnpm/text.comments-parser@1000.0.0': + resolution: {integrity: sha512-ivv/esrETOq9uMiKOC0ddVZ1BktEGsfsMQ9RWmrDpwPiqFSqWsIspnquxTBmm5GflC5N06fbqjGOpulZVYo3vQ==} + engines: {node: '>=18.12'} + + '@pnpm/types@1001.3.0': + resolution: {integrity: sha512-NLTXheat/u7OEGg5M5vF6Z85zx8uKUZE0+whtX/sbFV2XL48RdnOWGPTKYuVVkv8M+launaLUTgGEXNs/ess2w==} + engines: {node: '>=18.12'} + + '@pnpm/util.lex-comparator@3.0.2': + resolution: {integrity: sha512-blFO4Ws97tWv/SNE6N39ZdGmZBrocXnBOfVp0ln4kELmns4pGPZizqyRtR8EjfOLMLstbmNCTReBoDvLz1isVg==} + engines: {node: '>=18.12'} + + '@pnpm/write-project-manifest@1000.0.16': + resolution: {integrity: sha512-zG68fk03ryot7TWUl9S/ShQ91uHWzIL9sVr2aQCuNHJo8G9kjsG6S0p58Zj/voahdDQeakZYYBSJ0mjNZeiJnw==} + engines: {node: '>=18.12'} + + '@rrlab/biome-config@0.0.2': + resolution: {integrity: sha512-b54jSWnYejnTSemC/arKz8glA/5W/iLhyEmd/aznYZEXT18pHtRtYz76bMJHZfElmcU3woGC2adHtXBI3/Sing==} + peerDependencies: + '@biomejs/biome': '>=2.0.0' + + '@rrlab/biome-plugin@0.1.1': + resolution: {integrity: sha512-wtnJ4BQrjTk5EDpf9zCkWXUEGVHIJeFb1butcJ0YxAOUpUE9HMxe6xBnWlpkzg0mI4sHnAg5e1owjdxRzi1J+Q==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@biomejs/biome': '>=2.0.0' + '@rrlab/cli': 0.0.3 + + '@rrlab/cli@0.0.3': + resolution: {integrity: sha512-0+dwwG9C/FgOyKWNlyuEaiekQuovyVWqIKdvEuTVqqNSbaVZIcyI0lfkb8GIhh3b3Lc5IS6OY6S/FAlYEx6TVA==} + engines: {node: '>=20.0.0'} + hasBin: true + + '@rrlab/ts-config@0.0.2': + resolution: {integrity: sha512-Tt+XP7TE21Ev17kvpmhjrnmHWx/LI7iJ3cz7sTaYgTWkiFTicr9h9NCwcHaVDswCXo52jKyaQZOHfBkACgXB7Q==} + peerDependencies: + '@types/node': '>=20' + typescript: '>=5.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rrlab/ts-plugin@0.1.1': + resolution: {integrity: sha512-HKYVyeKogn1nps3JzrH+0T5K1CxBkz38HnlXgnCAqjeCBQY09K/eF1l/JMoNb7VoylZ63iSGoVu3Qy5hdr/Pag==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@rrlab/cli': 0.0.3 + typescript: '>=5.0.0' + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@total-typescript/tsconfig@1.0.4': + resolution: {integrity: sha512-fO4ctMPGz1kOFOQ4RCPBRBfMy3gDn+pegUfrGyUFRMv/Rd0ZM3/SHH3hFCYG4u6bPLG8OlmOGcBLDexvyr3A5w==} + + '@types/node@24.12.4': + resolution: {integrity: sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==} + + '@usage-spec/commander@1.1.0': + resolution: {integrity: sha512-hVv+ccKtcPaiaywLrm7Q/Nb4nGdRD319FBhfmTWQq3yUlS1SK/pmwyn0+BFQGAYN4uOMxvYDrqPH+qXpmINrkg==} + + '@usage-spec/core@1.1.0': + resolution: {integrity: sha512-OjcN6IWdvuxN6bZYknTPIx9n/UTZODtGNaQEQe3yN7Y5DluH8/UJ8GeG+C0hNBWc/OePc0n9MmBw3rTbtoRVKg==} + + '@vlandoss/clibuddy@0.6.1': + resolution: {integrity: sha512-beguPZIA9qvDUxb1/oTBx2JBf71MHNQ4YJAict7VHS1t5Uxl1tXBxi+/I+Dhx3QaEmcD5sNrQsW/7d6YYDsAzQ==} + engines: {node: '>=20.0.0'} + + '@vlandoss/env@file:../../package/.local/vlandoss-env.tgz': + resolution: {integrity: sha512-fy/i1owvfERHP9Jk2WcuPYtSHdeZFUyaz7pWOARrrTlTPlu0opZkDKX1wdwIbqXy7e0Oc2VAougm5MpBQgz5JQ==, tarball: file:../../package/.local/vlandoss-env.tgz} + version: 0.2.1 + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=19' + react-dom: '>=19' + vite: '>=5' + zod: ^4 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + vite: + optional: true + zod: + optional: true + + '@vlandoss/loggy@0.2.1': + resolution: {integrity: sha512-M/Lx4FTF54+EQalmGGKpsk52LzdHVS6s38JWzObMDJqZ8Odx3Q3US2kpTGeOZIWx/alCRucne+ud+zQIuzl9DA==} + engines: {node: '>=20.0.0'} + + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-timsort@1.0.3: + resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + bole@5.0.29: + resolution: {integrity: sha512-eYR9i2ubLv5/4TFGyZsQ1cVH4jF9+qLJA72Aow+E7ZZQfqHqQNUZeX3w+pVWF76PQyjl5eDKf2xylyOOX76ozA==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + + comment-json@4.2.5: + resolution: {integrity: sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==} + engines: {node: '>= 6'} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-own-prop@2.0.0: + resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==} + engines: {node: '>=8'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + individual@3.0.0: + resolution: {integrity: sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g==} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lru-cache@11.5.0: + resolution: {integrity: sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==} + engines: {node: 20 || >=22} + + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + memoize@10.2.0: + resolution: {integrity: sha512-DeC6b7QBrZsRs3Y02A6A7lQyzFbsQbqgjI6UW0GigGWV+u1s25TycMr0XHZE4cJce7rY/vyw2ctMQqfDkIhUEA==} + engines: {node: '>=18'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nypm@0.6.0: + resolution: {integrity: sha512-mn8wBFV9G9+UFHIrq+pZ2r2zL4aPau/by3kJb3cM7+5tQHMt6HGQB8FDIeKFYp8o0D2pnH6nVsO88N4AmUxIWg==} + engines: {node: ^14.16.0 || >=16.10.0} + hasBin: true + + p-filter@2.1.0: + resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} + engines: {node: '>=8'} + + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + + pkg-types@2.3.1: + resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + + read-yaml-file@2.1.0: + resolution: {integrity: sha512-UkRNRIwnhG+y7hpqnycCL/xbTk7+ia9VuVTC0S+zVbwd65DI9eUpRMfsWIGrCWxTU/mi+JW8cHQCrv+zfCbEPQ==} + engines: {node: '>=10.13'} + + repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + + rimraf@6.1.3: + resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==} + engines: {node: 20 || >=22} + hasBin: true + + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + engines: {node: '>=10'} + hasBin: true + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-comments-strings@1.2.0: + resolution: {integrity: sha512-zwF4bmnyEjZwRhaak9jUWNxc0DoeKBJ7lwSN/LEc8dQXZcUFG6auaaTQJokQWXopLdM3iTx01nQT8E4aL29DAQ==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyexec@1.1.2: + resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} + engines: {node: '>=18'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + write-yaml-file@5.0.0: + resolution: {integrity: sha512-FdNA4RyH1L43TlvGG8qOMIfcEczwA5ij+zLXUy3Z83CjxhLvcV7/Q/8pk22wnCgYw7PJhtK+7lhO+qqyT4NdvQ==} + engines: {node: '>=16.14'} + + yaml@2.8.4: + resolution: {integrity: sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==} + engines: {node: '>= 14.6'} + hasBin: true + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + +snapshots: + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bgotink/kdl@0.4.0': {} + + '@biomejs/biome@2.4.15': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.4.15 + '@biomejs/cli-darwin-x64': 2.4.15 + '@biomejs/cli-linux-arm64': 2.4.15 + '@biomejs/cli-linux-arm64-musl': 2.4.15 + '@biomejs/cli-linux-x64': 2.4.15 + '@biomejs/cli-linux-x64-musl': 2.4.15 + '@biomejs/cli-win32-arm64': 2.4.15 + '@biomejs/cli-win32-x64': 2.4.15 + + '@biomejs/cli-darwin-arm64@2.4.15': + optional: true + + '@biomejs/cli-darwin-x64@2.4.15': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.4.15': + optional: true + + '@biomejs/cli-linux-arm64@2.4.15': + optional: true + + '@biomejs/cli-linux-x64-musl@2.4.15': + optional: true + + '@biomejs/cli-linux-x64@2.4.15': + optional: true + + '@biomejs/cli-win32-arm64@2.4.15': + optional: true + + '@biomejs/cli-win32-x64@2.4.15': + optional: true + + '@clack/core@0.5.0': + dependencies: + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@clack/prompts@0.11.0': + dependencies: + '@clack/core': 0.5.0 + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@gwhitney/detect-indent@7.0.1': {} + + '@pnpm/constants@1001.3.1': {} + + '@pnpm/core-loggers@1001.0.9(@pnpm/logger@1001.0.1)': + dependencies: + '@pnpm/logger': 1001.0.1 + '@pnpm/types': 1001.3.0 + + '@pnpm/error@1000.1.0': + dependencies: + '@pnpm/constants': 1001.3.1 + + '@pnpm/fs.find-packages@1000.0.24(@pnpm/logger@1001.0.1)': + dependencies: + '@pnpm/read-project-manifest': 1001.2.6(@pnpm/logger@1001.0.1) + '@pnpm/types': 1001.3.0 + '@pnpm/util.lex-comparator': 3.0.2 + p-filter: 2.1.0 + tinyglobby: 0.2.16 + transitivePeerDependencies: + - '@pnpm/logger' + + '@pnpm/graceful-fs@1000.1.0': + dependencies: + graceful-fs: 4.2.11 + + '@pnpm/logger@1001.0.1': + dependencies: + bole: 5.0.29 + split2: 4.2.0 + + '@pnpm/manifest-utils@1002.0.5(@pnpm/logger@1001.0.1)': + dependencies: + '@pnpm/core-loggers': 1001.0.9(@pnpm/logger@1001.0.1) + '@pnpm/error': 1000.1.0 + '@pnpm/logger': 1001.0.1 + '@pnpm/semver.peer-range': 1000.0.0 + '@pnpm/types': 1001.3.0 + semver: 7.8.1 + + '@pnpm/read-project-manifest@1001.2.6(@pnpm/logger@1001.0.1)': + dependencies: + '@gwhitney/detect-indent': 7.0.1 + '@pnpm/error': 1000.1.0 + '@pnpm/graceful-fs': 1000.1.0 + '@pnpm/logger': 1001.0.1 + '@pnpm/manifest-utils': 1002.0.5(@pnpm/logger@1001.0.1) + '@pnpm/text.comments-parser': 1000.0.0 + '@pnpm/types': 1001.3.0 + '@pnpm/write-project-manifest': 1000.0.16 + fast-deep-equal: 3.1.3 + is-windows: 1.0.2 + json5: 2.2.3 + parse-json: 5.2.0 + read-yaml-file: 2.1.0 + strip-bom: 4.0.0 + + '@pnpm/semver.peer-range@1000.0.0': + dependencies: + semver: 7.8.1 + + '@pnpm/text.comments-parser@1000.0.0': + dependencies: + strip-comments-strings: 1.2.0 + + '@pnpm/types@1001.3.0': {} + + '@pnpm/util.lex-comparator@3.0.2': {} + + '@pnpm/write-project-manifest@1000.0.16': + dependencies: + '@pnpm/text.comments-parser': 1000.0.0 + '@pnpm/types': 1001.3.0 + json5: 2.2.3 + write-file-atomic: 5.0.1 + write-yaml-file: 5.0.0 + + '@rrlab/biome-config@0.0.2(@biomejs/biome@2.4.15)': + dependencies: + '@biomejs/biome': 2.4.15 + + '@rrlab/biome-plugin@0.1.1(@biomejs/biome@2.4.15)(@pnpm/logger@1001.0.1)(@rrlab/cli@0.0.3(@pnpm/logger@1001.0.1))': + dependencies: + '@biomejs/biome': 2.4.15 + '@rrlab/cli': 0.0.3(@pnpm/logger@1001.0.1) + '@vlandoss/clibuddy': 0.6.1(@pnpm/logger@1001.0.1) + comment-json: 4.2.5 + transitivePeerDependencies: + - '@pnpm/logger' + + '@rrlab/cli@0.0.3(@pnpm/logger@1001.0.1)': + dependencies: + '@clack/prompts': 0.11.0 + '@usage-spec/commander': 1.1.0 + '@vlandoss/clibuddy': 0.6.1(@pnpm/logger@1001.0.1) + '@vlandoss/loggy': 0.2.1 + commander: 14.0.3 + comment-json: 4.2.5 + glob: 13.0.6 + lilconfig: 3.1.3 + magicast: 0.3.5 + memoize: 10.2.0 + nypm: 0.6.0 + rimraf: 6.1.3 + transitivePeerDependencies: + - '@pnpm/logger' + - supports-color + + '@rrlab/ts-config@0.0.2(@types/node@24.12.4)(typescript@6.0.3)': + dependencies: + '@total-typescript/tsconfig': 1.0.4 + typescript: 6.0.3 + optionalDependencies: + '@types/node': 24.12.4 + + '@rrlab/ts-plugin@0.1.1(@pnpm/logger@1001.0.1)(@rrlab/cli@0.0.3(@pnpm/logger@1001.0.1))(typescript@6.0.3)': + dependencies: + '@rrlab/cli': 0.0.3(@pnpm/logger@1001.0.1) + '@vlandoss/clibuddy': 0.6.1(@pnpm/logger@1001.0.1) + comment-json: 4.2.5 + typescript: 6.0.3 + transitivePeerDependencies: + - '@pnpm/logger' + + '@standard-schema/spec@1.1.0': {} + + '@total-typescript/tsconfig@1.0.4': {} + + '@types/node@24.12.4': + dependencies: + undici-types: 7.16.0 + + '@usage-spec/commander@1.1.0': + dependencies: + '@usage-spec/core': 1.1.0 + commander: 14.0.3 + + '@usage-spec/core@1.1.0': + dependencies: + '@bgotink/kdl': 0.4.0 + + '@vlandoss/clibuddy@0.6.1(@pnpm/logger@1001.0.1)': + dependencies: + '@pnpm/fs.find-packages': 1000.0.24(@pnpm/logger@1001.0.1) + '@pnpm/types': 1001.3.0 + ansis: 4.2.0 + memoize: 10.2.0 + pkg-types: 2.3.0 + std-env: 3.9.0 + tinyexec: 1.1.2 + yaml: 2.8.4 + transitivePeerDependencies: + - '@pnpm/logger' + + '@vlandoss/env@file:../../package/.local/vlandoss-env.tgz(zod@4.3.6)': + dependencies: + '@standard-schema/spec': 1.1.0 + defu: 6.1.7 + optionalDependencies: + zod: 4.3.6 + + '@vlandoss/loggy@0.2.1': + dependencies: + consola: 3.4.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + ansis@4.2.0: {} + + argparse@2.0.1: {} + + array-timsort@1.0.3: {} + + balanced-match@4.0.4: {} + + bole@5.0.29: + dependencies: + fast-safe-stringify: 2.1.1 + individual: 3.0.0 + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + citty@0.1.6: + dependencies: + consola: 3.4.2 + + commander@14.0.3: {} + + comment-json@4.2.5: + dependencies: + array-timsort: 1.0.3 + core-util-is: 1.0.3 + esprima: 4.0.1 + has-own-prop: 2.0.0 + repeat-string: 1.6.1 + + confbox@0.2.4: {} + + consola@3.4.2: {} + + core-util-is@1.0.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + defu@6.1.7: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + esprima@4.0.1: {} + + exsolve@1.0.8: {} + + fast-deep-equal@3.1.3: {} + + fast-safe-stringify@2.1.1: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + + graceful-fs@4.2.11: {} + + has-own-prop@2.0.0: {} + + imurmurhash@0.1.4: {} + + individual@3.0.0: {} + + is-arrayish@0.2.1: {} + + is-windows@1.0.2: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + json-parse-even-better-errors@2.3.1: {} + + json5@2.2.3: {} + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + lru-cache@11.5.0: {} + + magicast@0.3.5: + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + source-map-js: 1.2.1 + + memoize@10.2.0: + dependencies: + mimic-function: 5.0.1 + + mimic-function@5.0.1: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + + minipass@7.1.3: {} + + ms@2.1.3: {} + + nypm@0.6.0: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + pathe: 2.0.3 + pkg-types: 2.3.1 + tinyexec: 0.3.2 + + p-filter@2.1.0: + dependencies: + p-map: 2.1.0 + + p-map@2.1.0: {} + + package-json-from-dist@1.0.1: {} + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.7 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.0 + minipass: 7.1.3 + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.4 + exsolve: 1.0.8 + pathe: 2.0.3 + + pkg-types@2.3.1: + dependencies: + confbox: 0.2.4 + exsolve: 1.0.8 + pathe: 2.0.3 + + read-yaml-file@2.1.0: + dependencies: + js-yaml: 4.1.1 + strip-bom: 4.0.0 + + repeat-string@1.6.1: {} + + rimraf@6.1.3: + dependencies: + glob: 13.0.6 + package-json-from-dist: 1.0.1 + + semver@7.8.1: {} + + signal-exit@4.1.0: {} + + sisteransi@1.0.5: {} + + source-map-js@1.2.1: {} + + split2@4.2.0: {} + + std-env@3.9.0: {} + + strip-bom@4.0.0: {} + + strip-comments-strings@1.2.0: {} + + tinyexec@0.3.2: {} + + tinyexec@1.1.2: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + typescript@6.0.3: {} + + undici-types@7.16.0: {} + + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + + write-yaml-file@5.0.0: + dependencies: + js-yaml: 4.1.1 + write-file-atomic: 5.0.1 + + yaml@2.8.4: {} + + zod@4.3.6: {} diff --git a/examples/config-cjs/run-run.config.mts b/examples/config-cjs/run-run.config.mts new file mode 100644 index 0000000..c5cd9f7 --- /dev/null +++ b/examples/config-cjs/run-run.config.mts @@ -0,0 +1,7 @@ +import biome from "@rrlab/biome-plugin"; +import { defineConfig } from "@rrlab/cli/config"; +import ts from "@rrlab/ts-plugin"; + +export default defineConfig({ + plugins: [biome(), ts()], +}); diff --git a/examples/config-cjs/src/env/index.ts b/examples/config-cjs/src/env/index.ts new file mode 100644 index 0000000..eb636d3 --- /dev/null +++ b/examples/config-cjs/src/env/index.ts @@ -0,0 +1,20 @@ +import { defineEnv, selectConfig } from "@vlandoss/env"; +import development from "../../config/development.ts"; +import production from "../../config/production.ts"; +import { Env } from "./schema.ts"; + +// Synchronous config selection — no `loadConfig`, no top-level `await`. This is +// what lets `db.config.mts` be loaded by tooling that pulls the config in via +// `require()` or bundles it to CJS, where a top-level await would throw +// `ERR_REQUIRE_ASYNC_MODULE` / fail the bundle. +// +// The configs are static `import`s, so the runtime/bundler resolves and +// transpiles each one at parse time; `selectConfig` just picks the one matching +// the current `envName()`. +export const env = defineEnv({ + schema: Env, + config: selectConfig({ development, production }), + vars: { + db: { URL: "DATABASE_URL" }, + }, +}); diff --git a/examples/config-cjs/src/env/schema.ts b/examples/config-cjs/src/env/schema.ts new file mode 100644 index 0000000..ff7538b --- /dev/null +++ b/examples/config-cjs/src/env/schema.ts @@ -0,0 +1,13 @@ +import { type Config, schema } from "@vlandoss/env"; +import * as e from "@vlandoss/env/zod"; +import * as z from "zod"; + +export const Env = schema({ + server: { PORT: e.port, HOST: e.host }, + db: { + URL: z.url(), + LOGGING: e.bool.default(false), + }, +}); + +export type EnvConfig = Config; diff --git a/examples/config-cjs/test/loader.test.ts b/examples/config-cjs/test/loader.test.ts new file mode 100644 index 0000000..dc802a1 --- /dev/null +++ b/examples/config-cjs/test/loader.test.ts @@ -0,0 +1,23 @@ +import assert from "node:assert/strict"; +import { createRequire } from "node:module"; +import { describe, it } from "node:test"; + +// A real CJS `require`, the way some tooling pulls in its config file. On Node +// ≥22.18 `require()` loads ES modules and strips TypeScript, but it still +// refuses an ES module graph that uses top-level await. +const require = createRequire(import.meta.url); + +describe("config file loading in a CJS / no-top-level-await context", () => { + it("selectConfig (sync) loads fine via require()", () => { + const mod = require("../db.config.mts") as { default: { dialect: string; dbCredentials: { url: string } } }; + assert.equal(mod.default.dialect, "postgresql"); + assert.equal(mod.default.dbCredentials.url, "postgres://localhost/dev"); + }); + + it("loadConfig + top-level await throws ERR_REQUIRE_ASYNC_MODULE via require()", () => { + assert.throws( + () => require("../db.config.broken.mts"), + (err: NodeJS.ErrnoException) => err.code === "ERR_REQUIRE_ASYNC_MODULE", + ); + }); +}); diff --git a/examples/config-cjs/tsconfig.json b/examples/config-cjs/tsconfig.json new file mode 100644 index 0000000..880ca9a --- /dev/null +++ b/examples/config-cjs/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@rrlab/ts-config/no-dom/app", + "include": ["src", "test", "config", "db.config.mts", "db.config.broken.mts"] +} diff --git a/mise.toml b/mise.toml index 584a5ae..dbd53ca 100644 --- a/mise.toml +++ b/mise.toml @@ -20,6 +20,7 @@ config_roots = [ "examples/spa-vite-dynamic", "examples/ssr-react-router", "examples/ssr-tanstack-start", + "examples/config-cjs", ] [tasks.setup] diff --git a/package/src/__tests__/lib.test-d.ts b/package/src/__tests__/lib.test-d.ts index 4439c8e..bbd3e26 100644 --- a/package/src/__tests__/lib.test-d.ts +++ b/package/src/__tests__/lib.test-d.ts @@ -1,7 +1,7 @@ import { assertType, describe, expectTypeOf, test } from "vitest"; import * as z from "zod"; import type { AssertEnvVarNames, Config, Defaults, Env, Vars } from "../lib/index.ts"; -import { defineEnv, schema } from "../lib/index.ts"; +import { defineEnv, schema, selectConfig } from "../lib/index.ts"; const S = schema({ server: { PORT: z.coerce.number(), HOST: z.string() }, @@ -136,3 +136,18 @@ describe("defineEnv() return type overloads", () => { defineEnv({ schema: S, config: "src/config/*.ts" }); }); }); + +describe("selectConfig()", () => { + const development: Config = { server: { PORT: 3000, HOST: "x" } }; + const production: Config = { server: { PORT: 8080, HOST: "y" } }; + + test("returns the map's value type (T), never `T | undefined`", () => { + const config = selectConfig({ development, production }); + expectTypeOf(config).toEqualTypeOf>(); + }); + + test("result pipes into defineEnv as a sync config -> Env", () => { + const env = defineEnv({ schema: S, config: selectConfig({ development, production }) }); + expectTypeOf(env).toEqualTypeOf>(); + }); +}); diff --git a/package/src/__tests__/select-config.test.ts b/package/src/__tests__/select-config.test.ts new file mode 100644 index 0000000..49351bb --- /dev/null +++ b/package/src/__tests__/select-config.test.ts @@ -0,0 +1,56 @@ +import { afterEach, describe, expect, it } from "vitest"; +import * as z from "zod"; +import { defineEnv, schema, selectConfig } from "../lib/index.ts"; + +const S = schema({ + server: { PORT: z.coerce.number().int(), HOST: z.string() }, +}); + +const development = { server: { PORT: 3000, HOST: "localhost" } }; +const production = { server: { PORT: 8080, HOST: "0.0.0.0" } }; + +afterEach(() => { + delete process.env.ENV; +}); + +describe("selectConfig", () => { + it("returns the config for the current env (envName)", () => { + process.env.ENV = "development"; + expect(selectConfig({ development, production })).toBe(development); + }); + + it("respects ENV when selecting", () => { + process.env.ENV = "production"; + expect(selectConfig({ development, production })).toBe(production); + }); + + it("supports custom env names", () => { + process.env.ENV = "staging"; + const staging = { server: { PORT: 5000, HOST: "staging.local" } }; + expect(selectConfig({ development, staging })).toBe(staging); + }); + + it("throws listing available keys when the current env has no entry", () => { + process.env.ENV = "staging"; + expect(() => selectConfig({ development, production })).toThrow( + 'selectConfig: no config for env "staging". Available: development, production.', + ); + }); + + it("reports (none) when the map is empty", () => { + process.env.ENV = "development"; + expect(() => selectConfig({})).toThrow('selectConfig: no config for env "development". Available: (none).'); + }); + + it("pipes into defineEnv as the (sync) config source", () => { + process.env.ENV = "production"; + const env = defineEnv({ + schema: S, + config: selectConfig({ development, production }), + runtimeEnv: {}, + }); + // selectConfig read process.env.ENV and picked the production config + expect(env.server.PORT).toBe(8080); + expect(env.server.HOST).toBe("0.0.0.0"); + }); +}); diff --git a/package/src/lib/index.ts b/package/src/lib/index.ts index b0280a6..859e789 100644 --- a/package/src/lib/index.ts +++ b/package/src/lib/index.ts @@ -2,6 +2,7 @@ export { ENV_GLOBAL_ID, ENV_SCRIPT_ID } from "./const.ts"; export { defineEnv } from "./define-env.ts"; export { envName, readEnv } from "./runtime.ts"; export { schema } from "./schema.ts"; +export { selectConfig } from "./select-config.ts"; export type { AssertEnvVarNames, diff --git a/package/src/lib/select-config.ts b/package/src/lib/select-config.ts new file mode 100644 index 0000000..e06d358 --- /dev/null +++ b/package/src/lib/select-config.ts @@ -0,0 +1,32 @@ +import { envName } from "./runtime.ts"; + +/** + * Synchronously pick the config for the current environment from a map keyed by + * env name. The sync, runtime-agnostic counterpart to `loadConfig` for config + * files that can't use top-level `await` — files that tooling loads via + * `require()` or bundles to CJS, where top-level await is rejected + * (`ERR_REQUIRE_ASYNC_MODULE`). + * + * Pair it with static `import`s so the bundler/runtime resolves and transpiles + * each config file at parse time — no dynamic `import()`, no `await`: + * + * ```ts + * import development from "./config/development.ts"; + * import production from "./config/production.ts"; + * + * const config = selectConfig({ development, production }); + * ``` + * + * Selects by `envName()`. Throws when the current env has no entry — the map is + * explicit, so a miss is almost always a typo or a forgotten environment. To + * select a non-current env, set `ENV=…` in the process env first (same rule as + * `loadConfig`). + */ +export function selectConfig(configs: Record): T { + const env = envName(); + if (!Object.hasOwn(configs, env)) { + const available = Object.keys(configs).join(", ") || "(none)"; + throw new Error(`selectConfig: no config for env "${env}". Available: ${available}.`); + } + return configs[env] as T; +}