diff --git a/.changeset/loadconfig-sync.md b/.changeset/loadconfig-sync.md new file mode 100644 index 0000000..d3f1939 --- /dev/null +++ b/.changeset/loadconfig-sync.md @@ -0,0 +1,25 @@ +--- +"@vlandoss/env": minor +--- + +**`loadConfig` (`@vlandoss/env/fs`) is now synchronous.** It loads the config with `require()` and returns `Config` directly instead of a `Promise`, so it works in app code **and** in config files that tooling loads via `require()` or bundles to CJS (where a top-level `await` is rejected). + +**Migration — drop the `await`:** + +```diff +- const config = await loadConfig(Env); ++ const config = loadConfig(Env); +``` + +`await loadConfig(...)` still returns the right value at runtime (awaiting a non-promise yields the value), but the `await` keeps the module asynchronous — so a config file that keeps it still can't be `require()`d. Remove it. + +Auto-discovery now also covers `.cjs` / `.cts` (was: `.ts` `.mts` `.js` `.mjs` `.json`). The options form gains an optional `cwd` so callers can override `process.cwd()` when the working directory isn't the project root. + +Files are loaded with `require()`. Runtime requirements by extension: +- `.ts` / `.mts` / `.cts` — needs native TS stripping (native in Bun/Deno, **Node ≥22.18**). +- `.mjs` / `.js` / `.cjs` — needs `require(esm)` (native in Bun/Deno, **Node ≥22.12**). +- `.json` — works on any supported Node. + +The package's `engines` is **Node ≥22.12** (the `require(esm)` baseline). The `.ts` strip requirement is documented per-extension; consumers using only `.json`/`.mjs`/`.cjs` configs aren't blocked by an over-broad floor. + +CJS configs (`module.exports = {...}`): a CJS exports object that owns a `default` property name is **no longer** silently stripped — `loadConfig` discriminates ESM namespaces by `Symbol.toStringTag === "Module"` and returns CJS exports as-is, so sibling keys survive. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 542b1d3..974ba0c 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -8,7 +8,7 @@ How to set up, work in, and ship from this monorepo. For contribution convention env/ ├── package/ # @vlandoss/env library — published to npm ├── docsite/ # Fumadocs site → env.oss.variable.land (Cloudflare Workers) -├── examples/ # 9 runtime-isolated demos (Node, Bun, Deno, Workers, Edge, Vite, SSR) +├── examples/ # 12 runtime-isolated demos (Node, Bun, Deno, Workers, Edge, Vite, SSR) ├── docs/ # Repository docs (CONTRIBUTING.md, DEVELOPMENT.md) ├── mise.toml # Tool versions + task orchestration ├── pnpm-workspace.yaml # Workspace for `package` + `docsite` only @@ -134,13 +134,16 @@ Every example declares the dependency as a local file tarball: The tarball is generated by `mise run env:pack` (which calls `pnpm pack` inside `package/`). This guarantees each example consumes the package **as published** — same `files`, same `publishConfig.exports`, same peerDeps. -### The 9 examples +### The 12 examples | Example | Runtime | PM | Why | | -------------------------------------------------------- | ---------------------------------------- | ---- | ------------------------------------------------------------------ | | [`backend-node`](../examples/backend-node) | Node 24 | pnpm | Plain Node server | | [`backend-bun`](../examples/backend-bun) | Bun 1.2.4 | bun | Native Bun consumer | | [`backend-deno`](../examples/backend-deno) | Deno 2 (server) + Node 24 (test runner) | bun | Deno can't extract `file:` tarballs natively — we use `bun install` to hydrate `node_modules/` (flat layout) and let Deno read it via `nodeModulesDir: "manual"` | +| [`backend-node-cjs`](../examples/backend-node-cjs) | Node 24 | pnpm | `loadConfig` (sync) — server booted from a CommonJS `require()` entry | +| [`backend-bun-cjs`](../examples/backend-bun-cjs) | Bun 1.2.4 | bun | `loadConfig` (sync) under a Bun CommonJS `require()` entry | +| [`backend-deno-cjs`](../examples/backend-deno-cjs) | Deno 2 (server) + Node 24 (test runner) | bun | `loadConfig` (sync) under a Deno CommonJS `require()` entry | | [`worker-cloudflare`](../examples/worker-cloudflare) | Cloudflare Workers | pnpm | Validates `runtimeEnv: c.env` per-request; no `nodejs_compat` flag | | [`edge-nextjs`](../examples/edge-nextjs) | Next.js Edge | pnpm | Validates the Edge runtime path | | [`spa-vite-plugin`](../examples/spa-vite-plugin) | Vite + React | pnpm | `envConfig()` plugin + `#config` alias | diff --git a/docsite/content/docs/api-reference/fs.mdx b/docsite/content/docs/api-reference/fs.mdx index 22f845d..8ba623e 100644 --- a/docsite/content/docs/api-reference/fs.mdx +++ b/docsite/content/docs/api-reference/fs.mdx @@ -6,13 +6,13 @@ icon: HardDrive The `@vlandoss/env/fs` entrypoint is the file-system adapter. It auto-discovers per-environment config files on disk so you don't have to wire imports by hand. -Works on any runtime that exposes a Node-compatible filesystem: **Node ≥20**, **Bun**, and **Deno** (via its Node-compat layer). It does **not** work on Workers, Edge, or any environment without a filesystem — those should resolve config at build time (see [`@vlandoss/env/vite`](/docs/api-reference/vite)) or pass it explicitly to `defineEnv`. +Works on any runtime that exposes a Node-compatible filesystem: **Node ≥22.12** (≥22.18 for `.ts`/`.mts`/`.cts` configs), **Bun**, and **Deno** (via its Node-compat layer). It does **not** work on Workers, Edge, or any environment without a filesystem — those should resolve config at build time (see [`@vlandoss/env/vite`](/docs/api-reference/vite)) or pass it explicitly to `defineEnv`. ## Exports | Export | Kind | Summary | | ------------ | -------- | ------------------------------------------------------------------------------------ | -| `loadConfig` | Function | Discover and load `[src/]config/.{ts,mts,js,mjs,json}` for the current env. | +| `loadConfig` | Function | **Synchronously** discover and load `[src/]config/.{ts,mts,cts,js,mjs,cjs,json}` for the current env. Returns `Config` directly (loads via `require()`), so it works in app code and in config files that tooling loads synchronously. Accepts an optional `pattern` (template) and `cwd` (override `process.cwd()`). | Full signatures and option tables are coming soon. Until then, see @@ -20,7 +20,13 @@ Works on any runtime that exposes a Node-compatible filesystem: **Node ≥20**, - Loading `.ts` / `.mts` files at runtime depends on the host runtime's TypeScript support: native in **Bun** and **Deno**, and in **Node ≥22.6** behind `--experimental-strip-types` (stable in 23.6). `.js` / `.mjs` / `.json` work everywhere. + `loadConfig` loads files with `require()`. Runtime requirements by extension: + + - `.ts` / `.mts` / `.cts` — needs native TypeScript stripping (native in **Bun** and **Deno**, **Node ≥22.18**). + - `.mjs` / `.js` / `.cjs` — needs `require(esm)` (native in Bun/Deno, **Node ≥22.12**). + - `.json` — works on any supported Node. + + Module loads are cached by the host's module system: editing a `.ts`/`.mjs`/`.cjs` config in a long-running process won't be picked up until restart. `.json` files are re-read on every call. ## See also diff --git a/docsite/content/docs/getting-started/quickstart.mdx b/docsite/content/docs/getting-started/quickstart.mdx index 1952c8f..2811b7b 100644 --- a/docsite/content/docs/getting-started/quickstart.mdx +++ b/docsite/content/docs/getting-started/quickstart.mdx @@ -61,11 +61,11 @@ import { defineEnv } from "@vlandoss/env"; import { loadConfig } from "@vlandoss/env/fs"; import { Env } from "./schema.ts"; -const config = await loadConfig(Env); +const config = loadConfig(Env); export const env = defineEnv({ schema: Env, config }); ``` -`loadConfig(Env)` auto-discovers `[src/]config/.{ts,mts,js,mjs,json}` under `process.cwd()`. `defineEnv` then merges `config` with `process.env` and validates against the schema. If anything is missing or wrong, it throws naming the dot-path of the offending leaf. +`loadConfig(Env)` **synchronously** auto-discovers `[src/]config/.{ts,mts,cts,js,mjs,cjs,json}` under `process.cwd()` (no `await`). `defineEnv` then merges `config` with `process.env` and validates against the schema. If anything is missing or wrong, it throws naming the dot-path of the offending leaf. ## 4. Read typed values diff --git a/docsite/content/docs/guides/custom-modes.mdx b/docsite/content/docs/guides/custom-modes.mdx index 1aac52b..5d4cb1b 100644 --- a/docsite/content/docs/guides/custom-modes.mdx +++ b/docsite/content/docs/guides/custom-modes.mdx @@ -44,7 +44,7 @@ src/ production.ts ``` -The plugin's discovery scans `[src/]config/.{ts,mts,js,mjs,json}` and pulls in the right one — same algorithm as `loadConfig` in `@vlandoss/env/fs`. +The plugin's discovery scans `[src/]config/.{ts,mts,cts,js,mjs,cjs,json}` and pulls in the right one — same algorithm as `loadConfig` in `@vlandoss/env/fs`. ## Verifying it works diff --git a/docsite/content/docs/guides/fs-loadconfig.mdx b/docsite/content/docs/guides/fs-loadconfig.mdx index 1f9eb90..b8f1514 100644 --- a/docsite/content/docs/guides/fs-loadconfig.mdx +++ b/docsite/content/docs/guides/fs-loadconfig.mdx @@ -48,7 +48,7 @@ import { defineEnv } from "@vlandoss/env"; import { loadConfig } from "@vlandoss/env/fs"; import { Env } from "./schema.ts"; -const config = await loadConfig(Env); +const config = loadConfig(Env); export const env = defineEnv({ schema: Env, @@ -69,7 +69,7 @@ server.listen(env.server.PORT, env.server.HOST); - **`schema()`** is the contract. Leaves use Standard Schema validators (Zod here, but any Standard Schema lib works). The `@vlandoss/env/zod` primitives (`e.port`, `e.host`, `e.logLevel`, `e.bool`) are opinionated single-purpose schemas — see the [Zod primitives reference](/docs/api-reference/zod). - **`config/.ts`** is the typed, versioned default per environment. `satisfies EnvConfig` is what gives you compile-time errors on typos. **Secrets do not go here** — leave them out of `config/*` and put them in `process.env`. -- **`loadConfig(Env)`** is the short form: it auto-discovers `[src/]config/.{ts,mts,js,mjs,json}` under `process.cwd()` and returns the first match (or `{}` if none). +- **`loadConfig(Env)`** is the short form: it **synchronously** auto-discovers `[src/]config/.{ts,mts,cts,js,mjs,cjs,json}` under `process.cwd()` and returns the first match (or `{}` if none) — no `await`. - **`defineEnv`** merges `defaults` (none here) → `config` → `process.env` and validates the result. See [Resolution order](/docs/concepts/resolution). - **`vars: { db: { URL: "DATABASE_URL" } }`** overrides the convention for one leaf. `db.URL` would default to `DB_URL`; here we map it to the conventional `DATABASE_URL`. See [Env-var naming](/docs/concepts/env-var-naming). @@ -78,7 +78,7 @@ server.listen(env.server.PORT, env.server.HOST); If your config directory doesn't follow the convention, use the long form: ```ts -const config = await loadConfig({ schema: Env, pattern: "config/{env}.ts" }); +const config = loadConfig({ schema: Env, pattern: "config/{env}.ts" }); ``` The pattern **must** contain `{env}`, and it throws if the resolved file doesn't exist (the short form silently falls back to `{}`). @@ -87,8 +87,26 @@ 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. +## Resolving from a custom `cwd` + +Both auto-discovery and the `{env}` template resolve paths against `process.cwd()` by default. If the process working directory isn't the project root (monorepo task runners, orchestrators, SSR workers launched from elsewhere), pass `cwd` explicitly: + +```ts +const config = loadConfig({ schema: Env, cwd: appRoot }); // auto-discovery +const config = loadConfig({ schema: Env, pattern: "config/{env}.ts", cwd: appRoot }); // template +``` + +## Config files loaded synchronously (`require()` / CJS) + +Because `loadConfig` is synchronous, it also works in **config files that tooling loads synchronously** — files pulled in via `require()` or bundled to CJS, where a top-level `await` would be rejected (`ERR_REQUIRE_ASYNC_MODULE`, or a build-time _"top-level await is not supported with the cjs output format"_). The wiring is exactly the same as above; there's no `await` to trip over. + +See the runnable [`backend-node-cjs`](https://github.com/variableland/env/tree/main/examples/backend-node-cjs), [`backend-bun-cjs`](https://github.com/variableland/env/tree/main/examples/backend-bun-cjs), and [`backend-deno-cjs`](https://github.com/variableland/env/tree/main/examples/backend-deno-cjs) examples — each boots its server from a CommonJS `require()` entry. + ## 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. +- `loadConfig` loads files with `require()`. Runtime requirements by extension: + - `.ts` / `.mts` / `.cts` — native TypeScript stripping (native in Bun/Deno, **Node ≥22.18**). + - `.mjs` / `.js` / `.cjs` — `require(esm)` (native in Bun/Deno, **Node ≥22.12**). + - `.json` — works on any supported Node. +- Module loads are cached by Node/Bun/Deno's module system. Editing a `.ts`/`.mjs`/`.cjs` config in a long-running process isn't picked up until the process restarts; `.json` files are re-read on every call. diff --git a/docsite/content/docs/guides/spa-vite-plugin.mdx b/docsite/content/docs/guides/spa-vite-plugin.mdx index 9804b9f..3686966 100644 --- a/docsite/content/docs/guides/spa-vite-plugin.mdx +++ b/docsite/content/docs/guides/spa-vite-plugin.mdx @@ -80,7 +80,7 @@ Each command produces a separate `dist/` whose bundle contains **only** the matc ## What each piece does -- **`envConfig()` plugin** registers `resolve.alias["#config"]` pointing at `[src/]config/.{ts,mts,js,mjs,json}` (same discovery algorithm as `loadConfig`). It also injects `__ENV_NAME__ = JSON.stringify(mode)` so `envName()` returns the right mode in the browser — see [Custom modes](/docs/guides/custom-modes). +- **`envConfig()` plugin** registers `resolve.alias["#config"]` pointing at `[src/]config/.{ts,mts,cts,js,mjs,cjs,json}` (same discovery algorithm as `loadConfig`). It also injects `__ENV_NAME__ = JSON.stringify(mode)` so `envName()` returns the right mode in the browser — see [Custom modes](/docs/guides/custom-modes). - **`#config.d.ts`** declares the type of the alias for TypeScript. Vite resolves the runtime import; this just stops `tsc` from complaining. - **`defineEnv({ schema, config })`** runs synchronously here — `config` is a plain object, not a Promise. diff --git a/docsite/content/docs/guides/ssr.mdx b/docsite/content/docs/guides/ssr.mdx index 2a0b604..9edb403 100644 --- a/docsite/content/docs/guides/ssr.mdx +++ b/docsite/content/docs/guides/ssr.mdx @@ -62,7 +62,7 @@ import { defineEnv } from "@vlandoss/env"; import { loadConfig } from "@vlandoss/env/fs"; import { ServerEnv } from "./schema.server.ts"; -const config = await loadConfig({ schema: ServerEnv, pattern: "app/config/{env}.ts" }); +const config = loadConfig({ schema: ServerEnv, pattern: "app/config/{env}.ts" }); export const env = defineEnv({ schema: ServerEnv, diff --git a/docsite/content/docs/index.mdx b/docsite/content/docs/index.mdx index 6fe294f..de237c0 100644 --- a/docsite/content/docs/index.mdx +++ b/docsite/content/docs/index.mdx @@ -38,7 +38,7 @@ import { Env } from "./schema.ts"; export const env = defineEnv({ schema: Env, - config: await loadConfig(Env) + config: loadConfig(Env) }); ``` diff --git a/docsite/src/components/landing/data.ts b/docsite/src/components/landing/data.ts index 8abca84..eda2d32 100644 --- a/docsite/src/components/landing/data.ts +++ b/docsite/src/components/landing/data.ts @@ -17,7 +17,7 @@ export const WIRE_CODE = `import { defineEnv } from "@vlandoss/env"; import { loadConfig } from "@vlandoss/env/fs"; import { Env } from "./schema.ts"; -const config = await loadConfig(Env); +const config = loadConfig(Env); export const env = defineEnv({ schema: Env, @@ -116,7 +116,7 @@ export const WHY_POINTS = [ ] as const; export const LANDING_META = { - version: "v0.2.1", + version: "v0.3.0", vlandUrl: "https://variable.land", githubUrl: "https://github.com/variableland/env", npmUrl: "https://www.npmjs.com/package/@vlandoss/env", diff --git a/examples/README.md b/examples/README.md index b8176dd..3df208b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,12 +1,15 @@ # `@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 exercise real `env` failure modes (missing required vars, wrong types, per-mode config isolation, SSR↔client hydration drift, config loaded synchronously from a CommonJS/`require()` entry). | Example | Runtime | Package manager | `env` entries exercised | | -------------------------------------------- | ---------------------------------------- | --------------- | -------------------------------------------------------------------------------- | | [`backend-node`](./backend-node) | Node.js 24 | pnpm | `@vlandoss/env` + `@vlandoss/env/fs` + `@vlandoss/env/zod` | | [`backend-bun`](./backend-bun) | Bun 1.2.4 | bun | `@vlandoss/env` + `@vlandoss/env/fs` + `@vlandoss/env/zod` | | [`backend-deno`](./backend-deno) | Deno 2 (server) + Node 24 (test runner) | bun | `@vlandoss/env` + `@vlandoss/env/fs` + `@vlandoss/env/zod` | +| [`backend-node-cjs`](./backend-node-cjs) | Node.js 24 | pnpm | `@vlandoss/env` + `@vlandoss/env/fs` (sync `loadConfig` via `require()`) + `@vlandoss/env/zod` | +| [`backend-bun-cjs`](./backend-bun-cjs) | Bun 1.2.4 | bun | `@vlandoss/env` + `@vlandoss/env/fs` (sync `loadConfig` via `require()`) + `@vlandoss/env/zod` | +| [`backend-deno-cjs`](./backend-deno-cjs) | Deno 2 (server) + Node 24 (test runner) | bun | `@vlandoss/env` + `@vlandoss/env/fs` (sync `loadConfig` via `require()`) + `@vlandoss/env/zod` | | [`worker-cloudflare`](./worker-cloudflare) | Cloudflare Workers (wrangler) | pnpm | `@vlandoss/env` (`runtimeEnv: c.env`, per-request) + `@vlandoss/env/zod` | | [`edge-nextjs`](./edge-nextjs) | Next.js Edge runtime | pnpm | `@vlandoss/env` + `@vlandoss/env/zod` (no FS on Edge) | | [`spa-vite-plugin`](./spa-vite-plugin) | Node.js (Vite) | pnpm | `@vlandoss/env` + `@vlandoss/env/vite` (`envConfig()` + `#config`) | @@ -33,7 +36,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 12 e2e suites ``` ## Run a single example diff --git a/examples/backend-bun-cjs/.gitignore b/examples/backend-bun-cjs/.gitignore new file mode 100644 index 0000000..169a2af --- /dev/null +++ b/examples/backend-bun-cjs/.gitignore @@ -0,0 +1,3 @@ +node_modules +playwright-report +test-results diff --git a/examples/backend-bun-cjs/README.md b/examples/backend-bun-cjs/README.md new file mode 100644 index 0000000..8c45156 --- /dev/null +++ b/examples/backend-bun-cjs/README.md @@ -0,0 +1,18 @@ +# backend-bun-cjs + +Same Bun + Hono server as [`backend-bun`](../backend-bun), but the server is +booted from a **CommonJS entry** ([`server.cjs`](./server.cjs) → +`require("./src/server.ts")`). + +That `require()` works because `loadConfig` is **synchronous** — `src/env/index.ts` +has no top-level `await`. Write `await loadConfig(...)` instead and the `await` +makes the module async, so Bun rejects the same `require()` as an async module. +This mirrors how tools that load a config via `require()` (or bundle it to CJS) +reject top-level await. + +The Playwright e2e (`test/e2e`) boots the server through `server.cjs` and hits +`/env` — proving the config loaded synchronously through the require path. + +```sh +mise run //examples/backend-bun-cjs:test:e2e +``` diff --git a/examples/backend-bun-cjs/biome.json b/examples/backend-bun-cjs/biome.json new file mode 100644 index 0000000..5b190a0 --- /dev/null +++ b/examples/backend-bun-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/backend-bun-cjs/bun.lock b/examples/backend-bun-cjs/bun.lock new file mode 100644 index 0000000..58dfaba --- /dev/null +++ b/examples/backend-bun-cjs/bun.lock @@ -0,0 +1,274 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "example-backend-bun", + "dependencies": { + "@vlandoss/env": "file:../../package/.local/vlandoss-env.tgz", + "hono": "4.12.18", + "zod": "4.3.6", + }, + "devDependencies": { + "@biomejs/biome": "2.4.15", + "@playwright/test": "1.59.1", + "@rrlab/biome-config": "0.0.2", + "@rrlab/biome-plugin": "1.1.0", + "@rrlab/cli": "1.1.0", + "@rrlab/ts-config": "0.0.2", + "@rrlab/ts-plugin": "1.1.0", + "@types/bun": "1.3.14", + "playwright": "1.59.1", + "typescript": "6.0.3", + }, + }, + }, + "packages": { + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + + "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + + "@bgotink/kdl": ["@bgotink/kdl@0.4.0", "", {}, "sha512-F0uJCjo5FQvFdcGF5QbYVNfcGiRWlocuzyIdQxottZF2+gu6L2xjMGEu9PIpse2hifAca/19vIospgaETCKxIg=="], + + "@biomejs/biome": ["@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" }, "bin": { "biome": "bin/biome" } }, "sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-/5KHXYMfSJs1fNXiX30xFtI8JcCFV6zaVVLxOa0M2sfqBKHkpQhRTv94yxQWxeTY2lzo2OuTlNvPC+hDQt2wcQ=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-owaAMZD/T4LrD0ELNCk0Km3qrRHuM0X6EAyVE1FSqGY0rbLoiDLrO4Us2tllm6cAeB2Ioa9C2C08NZPdr8+0Ug=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZPcxznxm0pogHBLZhYntyR3sR+MrZjqJIKEr7ZqVen0Rl+P/4upVmfYXjftizi9RoqZntg33fv/1fbdhbYXpEQ=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.15", "", { "os": "linux", "cpu": "x64" }, "sha512-0jj7THz12GbUOLmMibktK6DZjqz2zV64KFxyBtcFTKPiiOIY0a7vns1elpO1dERvxpsZ5ik0oFfz0oGwFde1+g=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.15", "", { "os": "linux", "cpu": "x64" }, "sha512-CNq/9W38SYSH023lfcQ4KKU8K0YX8T//FZUhcgtMMRABDojx5XsMV7jlweAvGSl389wJQB29Qo6Zb/a+jdvt+w=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-ouhkYdlhp/1GghEJPdWwD/Vi3gQ1nFxuSpMolWsbq3Lsq3QUR4jl6UdhhscdCugKU5vOEuMiJhvKj66O0OCq+w=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.15", "", { "os": "win32", "cpu": "x64" }, "sha512-zBrGq5mx5wwpnow4+2BxUvleDM+GNd4sLbPaMapsSLQLD0NGRCquqPBTgN+7XkUteHvj7M+BstuI8tmnV7+HgQ=="], + + "@clack/core": ["@clack/core@0.5.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow=="], + + "@clack/prompts": ["@clack/prompts@0.11.0", "", { "dependencies": { "@clack/core": "0.5.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw=="], + + "@gwhitney/detect-indent": ["@gwhitney/detect-indent@7.0.1", "", {}, "sha512-7bQW+gkKa2kKZPeJf6+c6gFK9ARxQfn+FKy9ScTBppyKRWH2KzsmweXUoklqeEiHiNVWaeP5csIdsNq6w7QhzA=="], + + "@playwright/test": ["@playwright/test@1.59.1", "", { "dependencies": { "playwright": "1.59.1" }, "bin": { "playwright": "cli.js" } }, "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg=="], + + "@pnpm/constants": ["@pnpm/constants@1001.3.1", "", {}, "sha512-2hf0s4pVrVEH8RvdJJ7YRKjQdiG8m0iAT26TTqXnCbK30kKwJW69VLmP5tED5zstmDRXcOeH5eRcrpkdwczQ9g=="], + + "@pnpm/core-loggers": ["@pnpm/core-loggers@1001.0.9", "", { "dependencies": { "@pnpm/types": "1001.3.0" }, "peerDependencies": { "@pnpm/logger": ">=1001.0.0 <1002.0.0" } }, "sha512-pW58m3ssrwVjwhlmTXDW1dh1sv2y6R2Gl5YvQInjM2d01/5mre/sYAY4MK3XfgEShZJQxv6wVXDUvyHHJ0oizg=="], + + "@pnpm/error": ["@pnpm/error@1000.1.0", "", { "dependencies": { "@pnpm/constants": "1001.3.1" } }, "sha512-Dqc2IJJPjUatwc9Letw+vG29rnaMrDGi5g6WCx1HiZYm0obXbTmLygeRafMbgf+sLKXrWE1shOeiayQuczBdoA=="], + + "@pnpm/fs.find-packages": ["@pnpm/fs.find-packages@1000.0.24", "", { "dependencies": { "@pnpm/read-project-manifest": "1001.2.6", "@pnpm/types": "1001.3.0", "@pnpm/util.lex-comparator": "^3.0.2", "p-filter": "^2.1.0", "tinyglobby": "^0.2.15" } }, "sha512-6r2lpvoljgTvQ+CiJYz3jCunzO1PM6g1Cqc3xon49he8sgg8BatMsNxcGnuZWK//du80+ylS/uBXKxwuHMuHUw=="], + + "@pnpm/graceful-fs": ["@pnpm/graceful-fs@1000.1.0", "", { "dependencies": { "graceful-fs": "^4.2.11" } }, "sha512-EsMX4slK0qJN2AR0/AYohY5m0HQNYGMNe+jhN74O994zp22/WbX+PbkIKyw3UQn39yQm2+z6SgwklDxbeapsmQ=="], + + "@pnpm/logger": ["@pnpm/logger@1001.0.1", "", { "dependencies": { "bole": "^5.0.17", "split2": "^4.2.0" } }, "sha512-gdwlAMXC4Wc0s7Dmg/4wNybMEd/4lSd9LsXQxeg/piWY0PPXjgz1IXJWnVScx6dZRaaodWP3c1ornrw8mZdFZw=="], + + "@pnpm/manifest-utils": ["@pnpm/manifest-utils@1002.0.5", "", { "dependencies": { "@pnpm/core-loggers": "1001.0.9", "@pnpm/error": "1000.1.0", "@pnpm/semver.peer-range": "1000.0.0", "@pnpm/types": "1001.3.0", "semver": "^7.7.4" }, "peerDependencies": { "@pnpm/logger": "^1001.0.1" } }, "sha512-2DSwQ6pP73IuJS5mCCtPd5fibJwuAdufXKuSL/Oq1n6AggCqy8616Xea1X3RH3z5dL4mn7Z4EZ+vnX8jX3Wrfw=="], + + "@pnpm/read-project-manifest": ["@pnpm/read-project-manifest@1001.2.6", "", { "dependencies": { "@gwhitney/detect-indent": "7.0.1", "@pnpm/error": "1000.1.0", "@pnpm/graceful-fs": "1000.1.0", "@pnpm/manifest-utils": "1002.0.5", "@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" }, "peerDependencies": { "@pnpm/logger": "^1001.0.1" } }, "sha512-BcNO50lAkE4m9JaJ0WmG3m/DH/qLSvMgZywtmb/dfyyLVu5nDZfDqmOd8U+f1NhLcLMbBK6AnS3hyUqZYvw9Vg=="], + + "@pnpm/semver.peer-range": ["@pnpm/semver.peer-range@1000.0.0", "", { "dependencies": { "semver": "^7.6.2" } }, "sha512-r6VzkrdH7ZKjPmAogTNvxuV/UyS/xwHNme+ZuEFiG0UthZgqudDftYtKmG20fcfrjG1lgJbbWICA8KvZy7mmbw=="], + + "@pnpm/text.comments-parser": ["@pnpm/text.comments-parser@1000.0.0", "", { "dependencies": { "strip-comments-strings": "1.2.0" } }, "sha512-ivv/esrETOq9uMiKOC0ddVZ1BktEGsfsMQ9RWmrDpwPiqFSqWsIspnquxTBmm5GflC5N06fbqjGOpulZVYo3vQ=="], + + "@pnpm/types": ["@pnpm/types@1001.3.0", "", {}, "sha512-NLTXheat/u7OEGg5M5vF6Z85zx8uKUZE0+whtX/sbFV2XL48RdnOWGPTKYuVVkv8M+launaLUTgGEXNs/ess2w=="], + + "@pnpm/util.lex-comparator": ["@pnpm/util.lex-comparator@3.0.2", "", {}, "sha512-blFO4Ws97tWv/SNE6N39ZdGmZBrocXnBOfVp0ln4kELmns4pGPZizqyRtR8EjfOLMLstbmNCTReBoDvLz1isVg=="], + + "@pnpm/write-project-manifest": ["@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" } }, "sha512-zG68fk03ryot7TWUl9S/ShQ91uHWzIL9sVr2aQCuNHJo8G9kjsG6S0p58Zj/voahdDQeakZYYBSJ0mjNZeiJnw=="], + + "@rrlab/biome-config": ["@rrlab/biome-config@0.0.2", "", { "peerDependencies": { "@biomejs/biome": ">=2.0.0" } }, "sha512-b54jSWnYejnTSemC/arKz8glA/5W/iLhyEmd/aznYZEXT18pHtRtYz76bMJHZfElmcU3woGC2adHtXBI3/Sing=="], + + "@rrlab/biome-plugin": ["@rrlab/biome-plugin@1.1.0", "", { "dependencies": { "@vlandoss/clibuddy": "0.7.0", "comment-json": "4.2.5" }, "peerDependencies": { "@biomejs/biome": ">=2.0.0", "@rrlab/cli": "^1.1.0" } }, "sha512-DJll0Yfw3jifr7wUz20sDz7Ow6dCOtsvJYT6su3zM7niPb0KZ7OI3xrc4wPgf5dbsgbttKyv+6dzeKQsROmg7Q=="], + + "@rrlab/cli": ["@rrlab/cli@1.1.0", "", { "dependencies": { "@clack/prompts": "0.11.0", "@usage-spec/commander": "1.1.0", "@vlandoss/clibuddy": "0.7.0", "@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" }, "bin": { "rr": "bin" } }, "sha512-jK+NGnxfr5dfZze7gynYpmvMiecPxC+Z67GuHVxqSW6pM4gr1nw8q8OHALl4urmZbDnXmKMAIoSh/acvfp/YbA=="], + + "@rrlab/ts-config": ["@rrlab/ts-config@0.0.2", "", { "dependencies": { "@total-typescript/tsconfig": "1.0.4" }, "peerDependencies": { "@types/node": ">=20", "typescript": ">=5.0.0" }, "optionalPeers": ["@types/node"] }, "sha512-Tt+XP7TE21Ev17kvpmhjrnmHWx/LI7iJ3cz7sTaYgTWkiFTicr9h9NCwcHaVDswCXo52jKyaQZOHfBkACgXB7Q=="], + + "@rrlab/ts-plugin": ["@rrlab/ts-plugin@1.1.0", "", { "dependencies": { "@vlandoss/clibuddy": "0.7.0", "comment-json": "4.2.5" }, "peerDependencies": { "@rrlab/cli": "^1.1.0", "typescript": ">=5.0.0" } }, "sha512-uHQOP7fgwFeLsiP36+0TMR7iDjb1LH8mP1d9loRTzMwrtlcl2CqM+5DmIJBdwb6n/sBe2+Ej6wW61IL8q+5o5Q=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@total-typescript/tsconfig": ["@total-typescript/tsconfig@1.0.4", "", {}, "sha512-fO4ctMPGz1kOFOQ4RCPBRBfMy3gDn+pegUfrGyUFRMv/Rd0ZM3/SHH3hFCYG4u6bPLG8OlmOGcBLDexvyr3A5w=="], + + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + + "@types/node": ["@types/node@25.8.0", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ=="], + + "@usage-spec/commander": ["@usage-spec/commander@1.1.0", "", { "dependencies": { "@usage-spec/core": "^1.1.0", "commander": "^14.0.3" } }, "sha512-hVv+ccKtcPaiaywLrm7Q/Nb4nGdRD319FBhfmTWQq3yUlS1SK/pmwyn0+BFQGAYN4uOMxvYDrqPH+qXpmINrkg=="], + + "@usage-spec/core": ["@usage-spec/core@1.1.0", "", { "dependencies": { "@bgotink/kdl": "^0.4.0" } }, "sha512-OjcN6IWdvuxN6bZYknTPIx9n/UTZODtGNaQEQe3yN7Y5DluH8/UJ8GeG+C0hNBWc/OePc0n9MmBw3rTbtoRVKg=="], + + "@vlandoss/clibuddy": ["@vlandoss/clibuddy@0.7.0", "", { "dependencies": { "@pnpm/fs.find-packages": "1000.0.24", "@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" } }, "sha512-Z/2oPgBW3xrkL0L8Ug6WRmDmaYR7X8V38IaIMIcZhMCnlTXZALrajfj8n/ZAe07Y/A0kkoXz/ZIgJKYFoX+Twg=="], + + "@vlandoss/env": ["@vlandoss/env@../../package/.local/vlandoss-env.tgz", { "dependencies": { "@standard-schema/spec": "1.1.0", "defu": "6.1.7", "type-fest": "5.4.4" }, "peerDependencies": { "react": ">=19", "react-dom": ">=19", "vite": ">=5", "zod": "^4" }, "optionalPeers": ["react", "react-dom", "vite", "zod"] }], + + "@vlandoss/loggy": ["@vlandoss/loggy@0.2.1", "", { "dependencies": { "consola": "3.4.2", "debug": "4.4.3" } }, "sha512-M/Lx4FTF54+EQalmGGKpsk52LzdHVS6s38JWzObMDJqZ8Odx3Q3US2kpTGeOZIWx/alCRucne+ud+zQIuzl9DA=="], + + "ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "array-timsort": ["array-timsort@1.0.3", "", {}, "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ=="], + + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "bole": ["bole@5.0.29", "", { "dependencies": { "fast-safe-stringify": "^2.0.7", "individual": "^3.0.0" } }, "sha512-eYR9i2ubLv5/4TFGyZsQ1cVH4jF9+qLJA72Aow+E7ZZQfqHqQNUZeX3w+pVWF76PQyjl5eDKf2xylyOOX76ozA=="], + + "brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], + + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + + "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + + "comment-json": ["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" } }, "sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw=="], + + "confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="], + + "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], + + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + + "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-own-prop": ["has-own-prop@2.0.0", "", {}, "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ=="], + + "hono": ["hono@4.12.18", "", {}, "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "individual": ["individual@3.0.0", "", {}, "sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g=="], + + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "lru-cache": ["lru-cache@11.3.6", "", {}, "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A=="], + + "magicast": ["magicast@0.3.5", "", { "dependencies": { "@babel/parser": "^7.25.4", "@babel/types": "^7.25.4", "source-map-js": "^1.2.0" } }, "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ=="], + + "memoize": ["memoize@10.2.0", "", { "dependencies": { "mimic-function": "^5.0.1" } }, "sha512-DeC6b7QBrZsRs3Y02A6A7lQyzFbsQbqgjI6UW0GigGWV+u1s25TycMr0XHZE4cJce7rY/vyw2ctMQqfDkIhUEA=="], + + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + + "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nypm": ["nypm@0.6.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "pathe": "^2.0.3", "pkg-types": "^2.0.0", "tinyexec": "^0.3.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-mn8wBFV9G9+UFHIrq+pZ2r2zL4aPau/by3kJb3cM7+5tQHMt6HGQB8FDIeKFYp8o0D2pnH6nVsO88N4AmUxIWg=="], + + "p-filter": ["p-filter@2.1.0", "", { "dependencies": { "p-map": "^2.0.0" } }, "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw=="], + + "p-map": ["p-map@2.1.0", "", {}, "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + + "playwright": ["playwright@1.59.1", "", { "dependencies": { "playwright-core": "1.59.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw=="], + + "playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="], + + "read-yaml-file": ["read-yaml-file@2.1.0", "", { "dependencies": { "js-yaml": "^4.0.0", "strip-bom": "^4.0.0" } }, "sha512-UkRNRIwnhG+y7hpqnycCL/xbTk7+ia9VuVTC0S+zVbwd65DI9eUpRMfsWIGrCWxTU/mi+JW8cHQCrv+zfCbEPQ=="], + + "repeat-string": ["repeat-string@1.6.1", "", {}, "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w=="], + + "rimraf": ["rimraf@6.1.3", "", { "dependencies": { "glob": "^13.0.3", "package-json-from-dist": "^1.0.1" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA=="], + + "semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="], + + "strip-bom": ["strip-bom@4.0.0", "", {}, "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w=="], + + "strip-comments-strings": ["strip-comments-strings@1.2.0", "", {}, "sha512-zwF4bmnyEjZwRhaak9jUWNxc0DoeKBJ7lwSN/LEc8dQXZcUFG6auaaTQJokQWXopLdM3iTx01nQT8E4aL29DAQ=="], + + "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], + + "tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="], + + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + + "type-fest": ["type-fest@5.4.4", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw=="], + + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], + + "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + + "write-file-atomic": ["write-file-atomic@5.0.1", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw=="], + + "write-yaml-file": ["write-yaml-file@5.0.0", "", { "dependencies": { "js-yaml": "^4.1.0", "write-file-atomic": "^5.0.1" } }, "sha512-FdNA4RyH1L43TlvGG8qOMIfcEczwA5ij+zLXUy3Z83CjxhLvcV7/Q/8pk22wnCgYw7PJhtK+7lhO+qqyT4NdvQ=="], + + "yaml": ["yaml@2.8.4", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog=="], + + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "nypm/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + } +} diff --git a/examples/backend-bun-cjs/mise.toml b/examples/backend-bun-cjs/mise.toml new file mode 100644 index 0000000..b87ae50 --- /dev/null +++ b/examples/backend-bun-cjs/mise.toml @@ -0,0 +1,34 @@ +[tools] +bun = "1.2.4" +node = "24" # for `rr` and `playwright` (node-based tooling) + +[env] +_.path = ["{{config_root}}/node_modules/.bin"] + +[tasks.setup] +description = "Pack env + install deps (bun)" +depends = ["//:env:pack"] +sources = [ + "package.json", + "bun.lock", + "{{config_root}}/../../package/.local/vlandoss-env.tgz", +] +run = "bun install" + +[tasks.reinstall] +description = "Force-reinstall the env tarball" +depends = ["//:env:pack"] +run = "bun install --force" + +[tasks.start] +depends = ["setup"] +run = "bun server.cjs" + +[tasks."test:e2e"] +depends = ["setup"] +run = "playwright test" + +[tasks.check] +description = "JS & TS check" +depends = ["setup"] +run = "rr check" diff --git a/examples/backend-bun-cjs/package.json b/examples/backend-bun-cjs/package.json new file mode 100644 index 0000000..5976bcc --- /dev/null +++ b/examples/backend-bun-cjs/package.json @@ -0,0 +1,23 @@ +{ + "name": "example-backend-bun-cjs", + "version": "0.0.0", + "private": true, + "type": "module", + "dependencies": { + "@vlandoss/env": "file:../../package/.local/vlandoss-env.tgz", + "hono": "4.12.18", + "zod": "4.3.6" + }, + "devDependencies": { + "@biomejs/biome": "2.4.15", + "@playwright/test": "1.59.1", + "@rrlab/biome-config": "0.0.2", + "@rrlab/biome-plugin": "1.1.0", + "@rrlab/cli": "1.1.0", + "@rrlab/ts-config": "0.0.2", + "@rrlab/ts-plugin": "1.1.0", + "@types/bun": "1.3.14", + "playwright": "1.59.1", + "typescript": "6.0.3" + } +} diff --git a/examples/backend-bun-cjs/playwright.config.ts b/examples/backend-bun-cjs/playwright.config.ts new file mode 100644 index 0000000..6d4b74b --- /dev/null +++ b/examples/backend-bun-cjs/playwright.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from "@playwright/test"; + +const PORT = 3002; +const BASE_URL = `http://127.0.0.1:${PORT}`; + +export default defineConfig({ + testDir: "./test/e2e", + fullyParallel: false, + workers: 1, + reporter: "list", + use: { baseURL: BASE_URL }, + // No `projects` — these tests don't drive a browser. The `request` fixture + // is HTTP-only and ignores `browserName`. + webServer: { + command: "bun server.cjs", + url: `${BASE_URL}/health`, + reuseExistingServer: false, + stdout: "pipe", + stderr: "pipe", + env: { + NODE_ENV: "development", + DATABASE_URL: "postgres://localhost/dev", + }, + }, +}); diff --git a/examples/backend-bun-cjs/run-run.config.mts b/examples/backend-bun-cjs/run-run.config.mts new file mode 100644 index 0000000..c5cd9f7 --- /dev/null +++ b/examples/backend-bun-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/backend-bun-cjs/server.cjs b/examples/backend-bun-cjs/server.cjs new file mode 100644 index 0000000..8fbff73 --- /dev/null +++ b/examples/backend-bun-cjs/server.cjs @@ -0,0 +1,2 @@ +// CommonJS entry: load the server the way config-loading tools load a config — via require(). +require("./src/server.ts"); diff --git a/examples/backend-bun-cjs/src/config/development.ts b/examples/backend-bun-cjs/src/config/development.ts new file mode 100644 index 0000000..8a12642 --- /dev/null +++ b/examples/backend-bun-cjs/src/config/development.ts @@ -0,0 +1,7 @@ +import type { EnvConfig } from "../env/schema.ts"; + +export default { + log: { LEVEL: "debug" }, + server: { PORT: 3002, HOST: "127.0.0.1" }, + db: { URL: "postgres://localhost/dev", LOGGING: true }, +} satisfies EnvConfig; diff --git a/examples/backend-bun-cjs/src/config/production.ts b/examples/backend-bun-cjs/src/config/production.ts new file mode 100644 index 0000000..7527b53 --- /dev/null +++ b/examples/backend-bun-cjs/src/config/production.ts @@ -0,0 +1,7 @@ +import type { EnvConfig } from "../env/schema.ts"; + +export default { + log: { LEVEL: "info" }, + server: { PORT: 3002, HOST: "0.0.0.0" }, + db: { LOGGING: false }, +} satisfies EnvConfig; diff --git a/examples/backend-bun-cjs/src/env/index.ts b/examples/backend-bun-cjs/src/env/index.ts new file mode 100644 index 0000000..4b4f6a5 --- /dev/null +++ b/examples/backend-bun-cjs/src/env/index.ts @@ -0,0 +1,11 @@ +import { defineEnv } from "@vlandoss/env"; +import { loadConfig } from "@vlandoss/env/fs"; +import { Env } from "./schema.ts"; + +export const env = defineEnv({ + schema: Env, + config: loadConfig(Env), + vars: { + db: { URL: "DATABASE_URL" }, + }, +}); diff --git a/examples/backend-bun-cjs/src/env/schema.ts b/examples/backend-bun-cjs/src/env/schema.ts new file mode 100644 index 0000000..ff4e98d --- /dev/null +++ b/examples/backend-bun-cjs/src/env/schema.ts @@ -0,0 +1,14 @@ +import { type Config, schema } from "@vlandoss/env"; +import * as e from "@vlandoss/env/zod"; +import * as z from "zod"; + +export const Env = schema({ + log: { LEVEL: e.logLevel }, + server: { PORT: e.port, HOST: e.host }, + db: { + URL: z.url(), + LOGGING: e.bool.default(false), + }, +}); + +export type EnvConfig = Config; diff --git a/examples/backend-bun-cjs/src/server.ts b/examples/backend-bun-cjs/src/server.ts new file mode 100644 index 0000000..36b50bb --- /dev/null +++ b/examples/backend-bun-cjs/src/server.ts @@ -0,0 +1,15 @@ +import { Hono } from "hono"; +import { env } from "./env/index.ts"; + +// biome-ignore format: I prefer to keep the code as is for better readability +const app = new Hono() + .get("/health", (c) => c.json({ ok: true })) + .get("/env", (c) => c.json({ env })); + +const server = Bun.serve({ + port: env.server.PORT, + hostname: env.server.HOST, + fetch: app.fetch, +}); + +console.log(`[${env.$name}] listening on http://${server.hostname}:${server.port}`); diff --git a/examples/backend-bun-cjs/test/e2e/server.spec.ts b/examples/backend-bun-cjs/test/e2e/server.spec.ts new file mode 100644 index 0000000..ab9e16d --- /dev/null +++ b/examples/backend-bun-cjs/test/e2e/server.spec.ts @@ -0,0 +1,25 @@ +import { expect, test } from "@playwright/test"; + +test("loads development config via auto-discovery", async ({ request }) => { + const res = await request.get("/env"); + + expect(res.ok()).toBe(true); + expect(await res.json()).toStrictEqual({ + env: { + $name: "development", + IS_DEV: true, + IS_PROD: false, + IS_TEST: false, + log: { LEVEL: "debug" }, + server: { PORT: 3002, HOST: "127.0.0.1" }, + db: { URL: "postgres://localhost/dev", LOGGING: true }, + }, + }); +}); + +test("/health responds with current env name", async ({ request }) => { + const res = await request.get("/health"); + + expect(res.ok()).toBe(true); + expect(await res.json()).toStrictEqual({ ok: true }); +}); diff --git a/examples/backend-bun-cjs/test/e2e/validation.spec.ts b/examples/backend-bun-cjs/test/e2e/validation.spec.ts new file mode 100644 index 0000000..f016db8 --- /dev/null +++ b/examples/backend-bun-cjs/test/e2e/validation.spec.ts @@ -0,0 +1,68 @@ +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { expect, test } from "@playwright/test"; + +// Boot through the CommonJS entry (`server.cjs` -> `require("./src/server.ts")`), +// the same require()-based path the e2e webServer uses. +const SERVER_CJS = fileURLToPath(new URL("../../server.cjs", import.meta.url)); +const CWD = fileURLToPath(new URL("../../", import.meta.url)); + +function runServer(env: Record) { + // Strip any seeded NODE_ENV/DATABASE_URL from the runner shell so the + // spawned child only sees what the test explicitly passes. + const baseEnv = { ...process.env }; + delete baseEnv.NODE_ENV; + delete baseEnv.DATABASE_URL; + + const result = spawnSync("bun", [SERVER_CJS], { + cwd: CWD, + env: { ...baseEnv, ...env }, + encoding: "utf8", + timeout: 8_000, + }); + + return { + code: result.status, + combined: (result.stdout ?? "") + (result.stderr ?? ""), + }; +} + +test.describe("env validation at boot", () => { + test("crashes when DATABASE_URL is missing in production", () => { + const { code, combined } = runServer({ + NODE_ENV: "production", + DATABASE_URL: "", + }); + expect(code).not.toBe(0); + expect(combined).toMatch(/db\.URL|DATABASE_URL/i); + }); + + test("crashes when PORT is non-numeric (coerce fails positive int)", () => { + const { code, combined } = runServer({ + NODE_ENV: "development", + DATABASE_URL: "postgres://localhost/dev", + SERVER_PORT: "not-a-number", + }); + expect(code).not.toBe(0); + expect(combined).toMatch(/server\.PORT|PORT/i); + }); + + test("crashes for unknown log level enum value", () => { + const { code, combined } = runServer({ + NODE_ENV: "development", + DATABASE_URL: "postgres://localhost/dev", + LOG_LEVEL: "verbose", + }); + expect(code).not.toBe(0); + expect(combined).toMatch(/log\.LEVEL|LEVEL/i); + }); + + test("crashes when no config file matches the env name (auto-discovery returns {} then validate kicks in)", () => { + const { code, combined } = runServer({ + NODE_ENV: "staging", + DATABASE_URL: "postgres://localhost/dev", + }); + expect(code).not.toBe(0); + expect(combined).toMatch(/Invalid value at/); + }); +}); diff --git a/examples/backend-bun-cjs/tsconfig.json b/examples/backend-bun-cjs/tsconfig.json new file mode 100644 index 0000000..2a1de2a --- /dev/null +++ b/examples/backend-bun-cjs/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@rrlab/ts-config/no-dom/app", + "compilerOptions": { + "types": ["bun"] + }, + "include": ["src", "test", "playwright.config.ts"] +} diff --git a/examples/backend-bun/src/env/index.ts b/examples/backend-bun/src/env/index.ts index be6e9b7..b647cb6 100644 --- a/examples/backend-bun/src/env/index.ts +++ b/examples/backend-bun/src/env/index.ts @@ -2,7 +2,7 @@ import { defineEnv } from "@vlandoss/env"; import { loadConfig } from "@vlandoss/env/fs"; import { Env } from "./schema.ts"; -const config = await loadConfig(Env); +const config = loadConfig(Env); export const env = defineEnv({ schema: Env, diff --git a/examples/backend-deno-cjs/.gitignore b/examples/backend-deno-cjs/.gitignore new file mode 100644 index 0000000..169a2af --- /dev/null +++ b/examples/backend-deno-cjs/.gitignore @@ -0,0 +1,3 @@ +node_modules +playwright-report +test-results diff --git a/examples/backend-deno-cjs/README.md b/examples/backend-deno-cjs/README.md new file mode 100644 index 0000000..bf90fa7 --- /dev/null +++ b/examples/backend-deno-cjs/README.md @@ -0,0 +1,21 @@ +# backend-deno-cjs + +Same Deno + Hono server as [`backend-deno`](../backend-deno), but the server is +booted from a **CommonJS entry** ([`server.cjs`](./server.cjs) → +`require("./src/server.ts")`). + +That `require()` works because `loadConfig` is **synchronous** — `src/env/index.ts` +has no top-level `await`. Write `await loadConfig(...)` instead and the `await` +makes the module async, so Deno rejects the same `require()` ("Top-level await is +not allowed in synchronous evaluation"). This mirrors how tools that load a +config via `require()` (or bundle it to CJS) reject top-level await. + +The Playwright e2e (`test/e2e`) boots the server through `server.cjs` and hits +`/env` — proving the config loaded synchronously through the require path. + +```sh +mise run //examples/backend-deno-cjs:test:e2e +``` + +> Like `backend-deno`, `node_modules/` is hydrated with `bun install` (Deno +> can't extract local `file:` tarballs) and read via `nodeModulesDir: "manual"`. diff --git a/examples/backend-deno-cjs/biome.json b/examples/backend-deno-cjs/biome.json new file mode 100644 index 0000000..5b190a0 --- /dev/null +++ b/examples/backend-deno-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/backend-deno-cjs/bun.lock b/examples/backend-deno-cjs/bun.lock new file mode 100644 index 0000000..016662a --- /dev/null +++ b/examples/backend-deno-cjs/bun.lock @@ -0,0 +1,273 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "example-backend-deno", + "dependencies": { + "@vlandoss/env": "file:../../package/.local/vlandoss-env.tgz", + "hono": "4.12.18", + "zod": "4.3.6", + }, + "devDependencies": { + "@biomejs/biome": "2.4.15", + "@playwright/test": "1.59.1", + "@rrlab/biome-config": "0.0.2", + "@rrlab/biome-plugin": "1.1.0", + "@rrlab/cli": "1.1.0", + "@rrlab/ts-config": "0.0.2", + "@rrlab/ts-plugin": "1.1.0", + "@types/deno": "2.5.0", + "@types/node": "24.12.4", + "playwright": "1.59.1", + "typescript": "6.0.3", + }, + }, + }, + "packages": { + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + + "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + + "@bgotink/kdl": ["@bgotink/kdl@0.4.0", "", {}, "sha512-F0uJCjo5FQvFdcGF5QbYVNfcGiRWlocuzyIdQxottZF2+gu6L2xjMGEu9PIpse2hifAca/19vIospgaETCKxIg=="], + + "@biomejs/biome": ["@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" }, "bin": { "biome": "bin/biome" } }, "sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-/5KHXYMfSJs1fNXiX30xFtI8JcCFV6zaVVLxOa0M2sfqBKHkpQhRTv94yxQWxeTY2lzo2OuTlNvPC+hDQt2wcQ=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-owaAMZD/T4LrD0ELNCk0Km3qrRHuM0X6EAyVE1FSqGY0rbLoiDLrO4Us2tllm6cAeB2Ioa9C2C08NZPdr8+0Ug=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZPcxznxm0pogHBLZhYntyR3sR+MrZjqJIKEr7ZqVen0Rl+P/4upVmfYXjftizi9RoqZntg33fv/1fbdhbYXpEQ=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.15", "", { "os": "linux", "cpu": "x64" }, "sha512-0jj7THz12GbUOLmMibktK6DZjqz2zV64KFxyBtcFTKPiiOIY0a7vns1elpO1dERvxpsZ5ik0oFfz0oGwFde1+g=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.15", "", { "os": "linux", "cpu": "x64" }, "sha512-CNq/9W38SYSH023lfcQ4KKU8K0YX8T//FZUhcgtMMRABDojx5XsMV7jlweAvGSl389wJQB29Qo6Zb/a+jdvt+w=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-ouhkYdlhp/1GghEJPdWwD/Vi3gQ1nFxuSpMolWsbq3Lsq3QUR4jl6UdhhscdCugKU5vOEuMiJhvKj66O0OCq+w=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.15", "", { "os": "win32", "cpu": "x64" }, "sha512-zBrGq5mx5wwpnow4+2BxUvleDM+GNd4sLbPaMapsSLQLD0NGRCquqPBTgN+7XkUteHvj7M+BstuI8tmnV7+HgQ=="], + + "@clack/core": ["@clack/core@0.5.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow=="], + + "@clack/prompts": ["@clack/prompts@0.11.0", "", { "dependencies": { "@clack/core": "0.5.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw=="], + + "@gwhitney/detect-indent": ["@gwhitney/detect-indent@7.0.1", "", {}, "sha512-7bQW+gkKa2kKZPeJf6+c6gFK9ARxQfn+FKy9ScTBppyKRWH2KzsmweXUoklqeEiHiNVWaeP5csIdsNq6w7QhzA=="], + + "@playwright/test": ["@playwright/test@1.59.1", "", { "dependencies": { "playwright": "1.59.1" }, "bin": { "playwright": "cli.js" } }, "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg=="], + + "@pnpm/constants": ["@pnpm/constants@1001.3.1", "", {}, "sha512-2hf0s4pVrVEH8RvdJJ7YRKjQdiG8m0iAT26TTqXnCbK30kKwJW69VLmP5tED5zstmDRXcOeH5eRcrpkdwczQ9g=="], + + "@pnpm/core-loggers": ["@pnpm/core-loggers@1001.0.9", "", { "dependencies": { "@pnpm/types": "1001.3.0" }, "peerDependencies": { "@pnpm/logger": ">=1001.0.0 <1002.0.0" } }, "sha512-pW58m3ssrwVjwhlmTXDW1dh1sv2y6R2Gl5YvQInjM2d01/5mre/sYAY4MK3XfgEShZJQxv6wVXDUvyHHJ0oizg=="], + + "@pnpm/error": ["@pnpm/error@1000.1.0", "", { "dependencies": { "@pnpm/constants": "1001.3.1" } }, "sha512-Dqc2IJJPjUatwc9Letw+vG29rnaMrDGi5g6WCx1HiZYm0obXbTmLygeRafMbgf+sLKXrWE1shOeiayQuczBdoA=="], + + "@pnpm/fs.find-packages": ["@pnpm/fs.find-packages@1000.0.24", "", { "dependencies": { "@pnpm/read-project-manifest": "1001.2.6", "@pnpm/types": "1001.3.0", "@pnpm/util.lex-comparator": "^3.0.2", "p-filter": "^2.1.0", "tinyglobby": "^0.2.15" } }, "sha512-6r2lpvoljgTvQ+CiJYz3jCunzO1PM6g1Cqc3xon49he8sgg8BatMsNxcGnuZWK//du80+ylS/uBXKxwuHMuHUw=="], + + "@pnpm/graceful-fs": ["@pnpm/graceful-fs@1000.1.0", "", { "dependencies": { "graceful-fs": "^4.2.11" } }, "sha512-EsMX4slK0qJN2AR0/AYohY5m0HQNYGMNe+jhN74O994zp22/WbX+PbkIKyw3UQn39yQm2+z6SgwklDxbeapsmQ=="], + + "@pnpm/logger": ["@pnpm/logger@1001.0.1", "", { "dependencies": { "bole": "^5.0.17", "split2": "^4.2.0" } }, "sha512-gdwlAMXC4Wc0s7Dmg/4wNybMEd/4lSd9LsXQxeg/piWY0PPXjgz1IXJWnVScx6dZRaaodWP3c1ornrw8mZdFZw=="], + + "@pnpm/manifest-utils": ["@pnpm/manifest-utils@1002.0.5", "", { "dependencies": { "@pnpm/core-loggers": "1001.0.9", "@pnpm/error": "1000.1.0", "@pnpm/semver.peer-range": "1000.0.0", "@pnpm/types": "1001.3.0", "semver": "^7.7.4" }, "peerDependencies": { "@pnpm/logger": "^1001.0.1" } }, "sha512-2DSwQ6pP73IuJS5mCCtPd5fibJwuAdufXKuSL/Oq1n6AggCqy8616Xea1X3RH3z5dL4mn7Z4EZ+vnX8jX3Wrfw=="], + + "@pnpm/read-project-manifest": ["@pnpm/read-project-manifest@1001.2.6", "", { "dependencies": { "@gwhitney/detect-indent": "7.0.1", "@pnpm/error": "1000.1.0", "@pnpm/graceful-fs": "1000.1.0", "@pnpm/manifest-utils": "1002.0.5", "@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" }, "peerDependencies": { "@pnpm/logger": "^1001.0.1" } }, "sha512-BcNO50lAkE4m9JaJ0WmG3m/DH/qLSvMgZywtmb/dfyyLVu5nDZfDqmOd8U+f1NhLcLMbBK6AnS3hyUqZYvw9Vg=="], + + "@pnpm/semver.peer-range": ["@pnpm/semver.peer-range@1000.0.0", "", { "dependencies": { "semver": "^7.6.2" } }, "sha512-r6VzkrdH7ZKjPmAogTNvxuV/UyS/xwHNme+ZuEFiG0UthZgqudDftYtKmG20fcfrjG1lgJbbWICA8KvZy7mmbw=="], + + "@pnpm/text.comments-parser": ["@pnpm/text.comments-parser@1000.0.0", "", { "dependencies": { "strip-comments-strings": "1.2.0" } }, "sha512-ivv/esrETOq9uMiKOC0ddVZ1BktEGsfsMQ9RWmrDpwPiqFSqWsIspnquxTBmm5GflC5N06fbqjGOpulZVYo3vQ=="], + + "@pnpm/types": ["@pnpm/types@1001.3.0", "", {}, "sha512-NLTXheat/u7OEGg5M5vF6Z85zx8uKUZE0+whtX/sbFV2XL48RdnOWGPTKYuVVkv8M+launaLUTgGEXNs/ess2w=="], + + "@pnpm/util.lex-comparator": ["@pnpm/util.lex-comparator@3.0.2", "", {}, "sha512-blFO4Ws97tWv/SNE6N39ZdGmZBrocXnBOfVp0ln4kELmns4pGPZizqyRtR8EjfOLMLstbmNCTReBoDvLz1isVg=="], + + "@pnpm/write-project-manifest": ["@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" } }, "sha512-zG68fk03ryot7TWUl9S/ShQ91uHWzIL9sVr2aQCuNHJo8G9kjsG6S0p58Zj/voahdDQeakZYYBSJ0mjNZeiJnw=="], + + "@rrlab/biome-config": ["@rrlab/biome-config@0.0.2", "", { "peerDependencies": { "@biomejs/biome": ">=2.0.0" } }, "sha512-b54jSWnYejnTSemC/arKz8glA/5W/iLhyEmd/aznYZEXT18pHtRtYz76bMJHZfElmcU3woGC2adHtXBI3/Sing=="], + + "@rrlab/biome-plugin": ["@rrlab/biome-plugin@1.1.0", "", { "dependencies": { "@vlandoss/clibuddy": "0.7.0", "comment-json": "4.2.5" }, "peerDependencies": { "@biomejs/biome": ">=2.0.0", "@rrlab/cli": "^1.1.0" } }, "sha512-DJll0Yfw3jifr7wUz20sDz7Ow6dCOtsvJYT6su3zM7niPb0KZ7OI3xrc4wPgf5dbsgbttKyv+6dzeKQsROmg7Q=="], + + "@rrlab/cli": ["@rrlab/cli@1.1.0", "", { "dependencies": { "@clack/prompts": "0.11.0", "@usage-spec/commander": "1.1.0", "@vlandoss/clibuddy": "0.7.0", "@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" }, "bin": { "rr": "bin" } }, "sha512-jK+NGnxfr5dfZze7gynYpmvMiecPxC+Z67GuHVxqSW6pM4gr1nw8q8OHALl4urmZbDnXmKMAIoSh/acvfp/YbA=="], + + "@rrlab/ts-config": ["@rrlab/ts-config@0.0.2", "", { "dependencies": { "@total-typescript/tsconfig": "1.0.4" }, "peerDependencies": { "@types/node": ">=20", "typescript": ">=5.0.0" }, "optionalPeers": ["@types/node"] }, "sha512-Tt+XP7TE21Ev17kvpmhjrnmHWx/LI7iJ3cz7sTaYgTWkiFTicr9h9NCwcHaVDswCXo52jKyaQZOHfBkACgXB7Q=="], + + "@rrlab/ts-plugin": ["@rrlab/ts-plugin@1.1.0", "", { "dependencies": { "@vlandoss/clibuddy": "0.7.0", "comment-json": "4.2.5" }, "peerDependencies": { "@rrlab/cli": "^1.1.0", "typescript": ">=5.0.0" } }, "sha512-uHQOP7fgwFeLsiP36+0TMR7iDjb1LH8mP1d9loRTzMwrtlcl2CqM+5DmIJBdwb6n/sBe2+Ej6wW61IL8q+5o5Q=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@total-typescript/tsconfig": ["@total-typescript/tsconfig@1.0.4", "", {}, "sha512-fO4ctMPGz1kOFOQ4RCPBRBfMy3gDn+pegUfrGyUFRMv/Rd0ZM3/SHH3hFCYG4u6bPLG8OlmOGcBLDexvyr3A5w=="], + + "@types/deno": ["@types/deno@2.5.0", "", {}, "sha512-g8JS38vmc0S87jKsFzre+0ZyMOUDHPVokEJymSCRlL57h6f/FdKPWBXgdFh3Z8Ees9sz11qt9VWELU9Y9ZkiVw=="], + + "@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="], + + "@usage-spec/commander": ["@usage-spec/commander@1.1.0", "", { "dependencies": { "@usage-spec/core": "^1.1.0", "commander": "^14.0.3" } }, "sha512-hVv+ccKtcPaiaywLrm7Q/Nb4nGdRD319FBhfmTWQq3yUlS1SK/pmwyn0+BFQGAYN4uOMxvYDrqPH+qXpmINrkg=="], + + "@usage-spec/core": ["@usage-spec/core@1.1.0", "", { "dependencies": { "@bgotink/kdl": "^0.4.0" } }, "sha512-OjcN6IWdvuxN6bZYknTPIx9n/UTZODtGNaQEQe3yN7Y5DluH8/UJ8GeG+C0hNBWc/OePc0n9MmBw3rTbtoRVKg=="], + + "@vlandoss/clibuddy": ["@vlandoss/clibuddy@0.7.0", "", { "dependencies": { "@pnpm/fs.find-packages": "1000.0.24", "@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" } }, "sha512-Z/2oPgBW3xrkL0L8Ug6WRmDmaYR7X8V38IaIMIcZhMCnlTXZALrajfj8n/ZAe07Y/A0kkoXz/ZIgJKYFoX+Twg=="], + + "@vlandoss/env": ["@vlandoss/env@../../package/.local/vlandoss-env.tgz", { "dependencies": { "@standard-schema/spec": "1.1.0", "defu": "6.1.7", "type-fest": "5.4.4" }, "peerDependencies": { "react": ">=19", "react-dom": ">=19", "vite": ">=5", "zod": "^4" }, "optionalPeers": ["react", "react-dom", "vite", "zod"] }], + + "@vlandoss/loggy": ["@vlandoss/loggy@0.2.1", "", { "dependencies": { "consola": "3.4.2", "debug": "4.4.3" } }, "sha512-M/Lx4FTF54+EQalmGGKpsk52LzdHVS6s38JWzObMDJqZ8Odx3Q3US2kpTGeOZIWx/alCRucne+ud+zQIuzl9DA=="], + + "ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "array-timsort": ["array-timsort@1.0.3", "", {}, "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ=="], + + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "bole": ["bole@5.0.29", "", { "dependencies": { "fast-safe-stringify": "^2.0.7", "individual": "^3.0.0" } }, "sha512-eYR9i2ubLv5/4TFGyZsQ1cVH4jF9+qLJA72Aow+E7ZZQfqHqQNUZeX3w+pVWF76PQyjl5eDKf2xylyOOX76ozA=="], + + "brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], + + "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + + "comment-json": ["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" } }, "sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw=="], + + "confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="], + + "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], + + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + + "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-own-prop": ["has-own-prop@2.0.0", "", {}, "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ=="], + + "hono": ["hono@4.12.18", "", {}, "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "individual": ["individual@3.0.0", "", {}, "sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g=="], + + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "lru-cache": ["lru-cache@11.3.6", "", {}, "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A=="], + + "magicast": ["magicast@0.3.5", "", { "dependencies": { "@babel/parser": "^7.25.4", "@babel/types": "^7.25.4", "source-map-js": "^1.2.0" } }, "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ=="], + + "memoize": ["memoize@10.2.0", "", { "dependencies": { "mimic-function": "^5.0.1" } }, "sha512-DeC6b7QBrZsRs3Y02A6A7lQyzFbsQbqgjI6UW0GigGWV+u1s25TycMr0XHZE4cJce7rY/vyw2ctMQqfDkIhUEA=="], + + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + + "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nypm": ["nypm@0.6.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "pathe": "^2.0.3", "pkg-types": "^2.0.0", "tinyexec": "^0.3.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-mn8wBFV9G9+UFHIrq+pZ2r2zL4aPau/by3kJb3cM7+5tQHMt6HGQB8FDIeKFYp8o0D2pnH6nVsO88N4AmUxIWg=="], + + "p-filter": ["p-filter@2.1.0", "", { "dependencies": { "p-map": "^2.0.0" } }, "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw=="], + + "p-map": ["p-map@2.1.0", "", {}, "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + + "playwright": ["playwright@1.59.1", "", { "dependencies": { "playwright-core": "1.59.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw=="], + + "playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="], + + "read-yaml-file": ["read-yaml-file@2.1.0", "", { "dependencies": { "js-yaml": "^4.0.0", "strip-bom": "^4.0.0" } }, "sha512-UkRNRIwnhG+y7hpqnycCL/xbTk7+ia9VuVTC0S+zVbwd65DI9eUpRMfsWIGrCWxTU/mi+JW8cHQCrv+zfCbEPQ=="], + + "repeat-string": ["repeat-string@1.6.1", "", {}, "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w=="], + + "rimraf": ["rimraf@6.1.3", "", { "dependencies": { "glob": "^13.0.3", "package-json-from-dist": "^1.0.1" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA=="], + + "semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="], + + "strip-bom": ["strip-bom@4.0.0", "", {}, "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w=="], + + "strip-comments-strings": ["strip-comments-strings@1.2.0", "", {}, "sha512-zwF4bmnyEjZwRhaak9jUWNxc0DoeKBJ7lwSN/LEc8dQXZcUFG6auaaTQJokQWXopLdM3iTx01nQT8E4aL29DAQ=="], + + "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], + + "tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="], + + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + + "type-fest": ["type-fest@5.4.4", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw=="], + + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "write-file-atomic": ["write-file-atomic@5.0.1", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw=="], + + "write-yaml-file": ["write-yaml-file@5.0.0", "", { "dependencies": { "js-yaml": "^4.1.0", "write-file-atomic": "^5.0.1" } }, "sha512-FdNA4RyH1L43TlvGG8qOMIfcEczwA5ij+zLXUy3Z83CjxhLvcV7/Q/8pk22wnCgYw7PJhtK+7lhO+qqyT4NdvQ=="], + + "yaml": ["yaml@2.8.4", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog=="], + + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "nypm/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + } +} diff --git a/examples/backend-deno-cjs/deno.jsonc b/examples/backend-deno-cjs/deno.jsonc new file mode 100644 index 0000000..39ef49b --- /dev/null +++ b/examples/backend-deno-cjs/deno.jsonc @@ -0,0 +1,8 @@ +{ + // `manual` = Deno reads `node_modules/` but doesn't manage it. We hydrate + // the directory with `bun install` because Deno itself can't extract local + // `file:` tarballs — both `auto` and `manual` modes leave the `.tgz` as a + // dangling symlink. Bun extracts it correctly into a flat layout that Deno + // can walk. + "nodeModulesDir": "manual" +} diff --git a/examples/backend-deno-cjs/mise.toml b/examples/backend-deno-cjs/mise.toml new file mode 100644 index 0000000..1ae8cc5 --- /dev/null +++ b/examples/backend-deno-cjs/mise.toml @@ -0,0 +1,37 @@ +[tools] +deno = "2" +node = "24" # for `rr` (run-run) and `playwright` test runner; the server itself runs on Deno +bun = "1.2.4" # used only to hydrate `node_modules/` — Deno can't extract `file:` tarballs (bun does, with a flat layout Deno can read) + +[env] +_.path = ["{{config_root}}/node_modules/.bin"] + +[tasks.setup] +description = "Pack env + hydrate node_modules with bun (Deno can't extract `file:` tarballs); Deno reads it via nodeModulesDir: 'manual'" +depends = ["//:env:pack"] +sources = [ + "package.json", + "bun.lock", + "deno.jsonc", + "{{config_root}}/../../package/.local/vlandoss-env.tgz", +] +run = "bun install" + +[tasks.reinstall] +description = "Force-reinstall the env tarball" +depends = ["//:env:pack"] +run = "bun install --force" + +[tasks.start] +depends = ["setup"] +run = "deno run --allow-net --allow-env --allow-read server.cjs" + +[tasks."test:e2e"] +description = "Playwright (node) drives a Deno-launched webServer" +depends = ["setup"] +run = "playwright test" + +[tasks.check] +description = "JS & TS check" +depends = ["setup"] +run = "rr check" diff --git a/examples/backend-deno-cjs/package.json b/examples/backend-deno-cjs/package.json new file mode 100644 index 0000000..689c608 --- /dev/null +++ b/examples/backend-deno-cjs/package.json @@ -0,0 +1,24 @@ +{ + "name": "example-backend-deno-cjs", + "version": "0.0.0", + "private": true, + "type": "module", + "dependencies": { + "@vlandoss/env": "file:../../package/.local/vlandoss-env.tgz", + "hono": "4.12.18", + "zod": "4.3.6" + }, + "devDependencies": { + "@biomejs/biome": "2.4.15", + "@playwright/test": "1.59.1", + "@rrlab/biome-config": "0.0.2", + "@rrlab/biome-plugin": "1.1.0", + "@rrlab/cli": "1.1.0", + "@rrlab/ts-config": "0.0.2", + "@rrlab/ts-plugin": "1.1.0", + "@types/deno": "2.5.0", + "@types/node": "24.12.4", + "playwright": "1.59.1", + "typescript": "6.0.3" + } +} diff --git a/examples/backend-deno-cjs/playwright.config.ts b/examples/backend-deno-cjs/playwright.config.ts new file mode 100644 index 0000000..ff25770 --- /dev/null +++ b/examples/backend-deno-cjs/playwright.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from "@playwright/test"; + +const PORT = 3003; +const BASE_URL = `http://127.0.0.1:${PORT}`; + +export default defineConfig({ + testDir: "./test/e2e", + fullyParallel: false, + workers: 1, + reporter: "list", + use: { baseURL: BASE_URL }, + // No `projects` — these tests don't drive a browser. The `request` fixture + // is HTTP-only and ignores `browserName`. + webServer: { + command: "deno run --allow-net --allow-env --allow-read server.cjs", + url: `${BASE_URL}/health`, + reuseExistingServer: false, + stdout: "pipe", + stderr: "pipe", + env: { + NODE_ENV: "development", + DATABASE_URL: "postgres://localhost/dev", + }, + }, +}); diff --git a/examples/backend-deno-cjs/run-run.config.mts b/examples/backend-deno-cjs/run-run.config.mts new file mode 100644 index 0000000..c5cd9f7 --- /dev/null +++ b/examples/backend-deno-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/backend-deno-cjs/server.cjs b/examples/backend-deno-cjs/server.cjs new file mode 100644 index 0000000..8fbff73 --- /dev/null +++ b/examples/backend-deno-cjs/server.cjs @@ -0,0 +1,2 @@ +// CommonJS entry: load the server the way config-loading tools load a config — via require(). +require("./src/server.ts"); diff --git a/examples/backend-deno-cjs/src/config/development.ts b/examples/backend-deno-cjs/src/config/development.ts new file mode 100644 index 0000000..d22289a --- /dev/null +++ b/examples/backend-deno-cjs/src/config/development.ts @@ -0,0 +1,7 @@ +import type { EnvConfig } from "../env/schema.ts"; + +export default { + log: { LEVEL: "debug" }, + server: { PORT: 3003, HOST: "127.0.0.1" }, + db: { URL: "postgres://localhost/dev", LOGGING: true }, +} satisfies EnvConfig; diff --git a/examples/backend-deno-cjs/src/config/production.ts b/examples/backend-deno-cjs/src/config/production.ts new file mode 100644 index 0000000..34fd7b2 --- /dev/null +++ b/examples/backend-deno-cjs/src/config/production.ts @@ -0,0 +1,7 @@ +import type { EnvConfig } from "../env/schema.ts"; + +export default { + log: { LEVEL: "info" }, + server: { PORT: 3003, HOST: "0.0.0.0" }, + db: { LOGGING: false }, +} satisfies EnvConfig; diff --git a/examples/backend-deno-cjs/src/env/index.ts b/examples/backend-deno-cjs/src/env/index.ts new file mode 100644 index 0000000..4b4f6a5 --- /dev/null +++ b/examples/backend-deno-cjs/src/env/index.ts @@ -0,0 +1,11 @@ +import { defineEnv } from "@vlandoss/env"; +import { loadConfig } from "@vlandoss/env/fs"; +import { Env } from "./schema.ts"; + +export const env = defineEnv({ + schema: Env, + config: loadConfig(Env), + vars: { + db: { URL: "DATABASE_URL" }, + }, +}); diff --git a/examples/backend-deno-cjs/src/env/schema.ts b/examples/backend-deno-cjs/src/env/schema.ts new file mode 100644 index 0000000..ff4e98d --- /dev/null +++ b/examples/backend-deno-cjs/src/env/schema.ts @@ -0,0 +1,14 @@ +import { type Config, schema } from "@vlandoss/env"; +import * as e from "@vlandoss/env/zod"; +import * as z from "zod"; + +export const Env = schema({ + log: { LEVEL: e.logLevel }, + server: { PORT: e.port, HOST: e.host }, + db: { + URL: z.url(), + LOGGING: e.bool.default(false), + }, +}); + +export type EnvConfig = Config; diff --git a/examples/backend-deno-cjs/src/server.ts b/examples/backend-deno-cjs/src/server.ts new file mode 100644 index 0000000..0886de0 --- /dev/null +++ b/examples/backend-deno-cjs/src/server.ts @@ -0,0 +1,11 @@ +import { Hono } from "hono"; +import { env } from "./env/index.ts"; + +// biome-ignore format: I prefer to keep the code as is for better readability +const app = new Hono() + .get("/health", (c) => c.json({ ok: true })) + .get("/env", (c) => c.json({ env })); + +const server = Deno.serve({ port: env.server.PORT, hostname: env.server.HOST }, app.fetch); + +console.log(`[${env.$name}] listening on http://${server.addr.hostname}:${server.addr.port}`); diff --git a/examples/backend-deno-cjs/test/e2e/server.spec.ts b/examples/backend-deno-cjs/test/e2e/server.spec.ts new file mode 100644 index 0000000..762fabc --- /dev/null +++ b/examples/backend-deno-cjs/test/e2e/server.spec.ts @@ -0,0 +1,25 @@ +import { expect, test } from "@playwright/test"; + +test("loads development config via auto-discovery", async ({ request }) => { + const res = await request.get("/env"); + + expect(res.ok()).toBe(true); + expect(await res.json()).toStrictEqual({ + env: { + $name: "development", + IS_DEV: true, + IS_PROD: false, + IS_TEST: false, + log: { LEVEL: "debug" }, + server: { PORT: 3003, HOST: "127.0.0.1" }, + db: { URL: "postgres://localhost/dev", LOGGING: true }, + }, + }); +}); + +test("/health responds with current env name", async ({ request }) => { + const res = await request.get("/health"); + + expect(res.ok()).toBe(true); + expect(await res.json()).toStrictEqual({ ok: true }); +}); diff --git a/examples/backend-deno-cjs/test/e2e/validation.spec.ts b/examples/backend-deno-cjs/test/e2e/validation.spec.ts new file mode 100644 index 0000000..07ba4d9 --- /dev/null +++ b/examples/backend-deno-cjs/test/e2e/validation.spec.ts @@ -0,0 +1,68 @@ +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { expect, test } from "@playwright/test"; + +// Boot through the CommonJS entry (`server.cjs` -> `require("./src/server.ts")`), +// the same require()-based path the e2e webServer uses. +const SERVER_CJS = fileURLToPath(new URL("../../server.cjs", import.meta.url)); +const CWD = fileURLToPath(new URL("../../", import.meta.url)); + +function runServer(env: Record) { + // Strip any seeded NODE_ENV/DATABASE_URL from the runner shell so the + // spawned child only sees what the test explicitly passes. + const baseEnv = { ...process.env }; + delete baseEnv.NODE_ENV; + delete baseEnv.DATABASE_URL; + + const result = spawnSync("deno", ["run", "--allow-net", "--allow-env", "--allow-read", SERVER_CJS], { + cwd: CWD, + env: { ...baseEnv, ...env }, + encoding: "utf8", + timeout: 8_000, + }); + + return { + code: result.status, + combined: (result.stdout ?? "") + (result.stderr ?? ""), + }; +} + +test.describe("env validation at boot", () => { + test("crashes when DATABASE_URL is missing in production", () => { + const { code, combined } = runServer({ + NODE_ENV: "production", + DATABASE_URL: "", + }); + expect(code).not.toBe(0); + expect(combined).toMatch(/db\.URL|DATABASE_URL/i); + }); + + test("crashes when PORT is non-numeric (coerce fails positive int)", () => { + const { code, combined } = runServer({ + NODE_ENV: "development", + DATABASE_URL: "postgres://localhost/dev", + SERVER_PORT: "not-a-number", + }); + expect(code).not.toBe(0); + expect(combined).toMatch(/server\.PORT|PORT/i); + }); + + test("crashes for unknown log level enum value", () => { + const { code, combined } = runServer({ + NODE_ENV: "development", + DATABASE_URL: "postgres://localhost/dev", + LOG_LEVEL: "verbose", + }); + expect(code).not.toBe(0); + expect(combined).toMatch(/log\.LEVEL|LEVEL/i); + }); + + test("crashes when no config file matches the env name (auto-discovery returns {} then validate kicks in)", () => { + const { code, combined } = runServer({ + NODE_ENV: "staging", + DATABASE_URL: "postgres://localhost/dev", + }); + expect(code).not.toBe(0); + expect(combined).toMatch(/Invalid value at/); + }); +}); diff --git a/examples/backend-deno-cjs/tsconfig.json b/examples/backend-deno-cjs/tsconfig.json new file mode 100644 index 0000000..3645858 --- /dev/null +++ b/examples/backend-deno-cjs/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@rrlab/ts-config/no-dom/app", + "compilerOptions": { + "types": ["node", "@types/deno"] + }, + "include": ["src", "test", "playwright.config.ts"] +} diff --git a/examples/backend-deno/src/env/index.ts b/examples/backend-deno/src/env/index.ts index be6e9b7..b647cb6 100644 --- a/examples/backend-deno/src/env/index.ts +++ b/examples/backend-deno/src/env/index.ts @@ -2,7 +2,7 @@ import { defineEnv } from "@vlandoss/env"; import { loadConfig } from "@vlandoss/env/fs"; import { Env } from "./schema.ts"; -const config = await loadConfig(Env); +const config = loadConfig(Env); export const env = defineEnv({ schema: Env, diff --git a/examples/backend-node-cjs/.gitignore b/examples/backend-node-cjs/.gitignore new file mode 100644 index 0000000..169a2af --- /dev/null +++ b/examples/backend-node-cjs/.gitignore @@ -0,0 +1,3 @@ +node_modules +playwright-report +test-results diff --git a/examples/backend-node-cjs/README.md b/examples/backend-node-cjs/README.md new file mode 100644 index 0000000..87c606b --- /dev/null +++ b/examples/backend-node-cjs/README.md @@ -0,0 +1,18 @@ +# backend-node-cjs + +Same Node + Hono server as [`backend-node`](../backend-node), but the server is +booted from a **CommonJS entry** ([`server.cjs`](./server.cjs) → +`require("./src/server.ts")`). + +That `require()` works because `loadConfig` is **synchronous** — `src/env/index.ts` +has no top-level `await`. Write `await loadConfig(...)` instead and the `await` +makes the module async, so the same `require()` throws `ERR_REQUIRE_ASYNC_MODULE`. +This mirrors how tools that load a config via `require()` (or bundle it to CJS) +reject top-level await. + +The Playwright e2e (`test/e2e`) boots the server through `server.cjs` and hits +`/env` — proving the config loaded synchronously through the require path. + +```sh +mise run //examples/backend-node-cjs:test:e2e +``` diff --git a/examples/backend-node-cjs/biome.json b/examples/backend-node-cjs/biome.json new file mode 100644 index 0000000..5b190a0 --- /dev/null +++ b/examples/backend-node-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/backend-node-cjs/mise.toml b/examples/backend-node-cjs/mise.toml new file mode 100644 index 0000000..e0705eb --- /dev/null +++ b/examples/backend-node-cjs/mise.toml @@ -0,0 +1,35 @@ +[tools] +node = "24" +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.start] +depends = ["setup"] +run = "node server.cjs" + +[tasks."test:e2e"] +depends = ["setup"] +run = "playwright test" + +[tasks.check] +description = "JS & TS check" +depends = ["setup"] +run = "rr check" diff --git a/examples/backend-node-cjs/package.json b/examples/backend-node-cjs/package.json new file mode 100644 index 0000000..0719fb2 --- /dev/null +++ b/examples/backend-node-cjs/package.json @@ -0,0 +1,27 @@ +{ + "name": "example-backend-node-cjs", + "version": "0.0.0", + "private": true, + "type": "module", + "dependencies": { + "@hono/node-server": "2.0.2", + "@vlandoss/env": "file:../../package/.local/vlandoss-env.tgz", + "hono": "4.12.18", + "zod": "4.3.6" + }, + "devDependencies": { + "@biomejs/biome": "2.4.15", + "@playwright/test": "1.59.1", + "@rrlab/biome-config": "0.0.2", + "@rrlab/biome-plugin": "1.1.0", + "@rrlab/cli": "1.1.0", + "@rrlab/ts-config": "0.0.2", + "@rrlab/ts-plugin": "1.1.0", + "@types/node": "24.12.4", + "playwright": "1.59.1", + "typescript": "6.0.3" + }, + "engines": { + "node": ">=22.18.0" + } +} diff --git a/examples/backend-node-cjs/playwright.config.ts b/examples/backend-node-cjs/playwright.config.ts new file mode 100644 index 0000000..ba6347d --- /dev/null +++ b/examples/backend-node-cjs/playwright.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from "@playwright/test"; + +const PORT = 3001; +const BASE_URL = `http://127.0.0.1:${PORT}`; + +export default defineConfig({ + testDir: "./test/e2e", + fullyParallel: false, + workers: 1, + reporter: "list", + use: { baseURL: BASE_URL }, + // No `projects` — these tests don't drive a browser. The `request` fixture + // is HTTP-only and ignores `browserName`. + webServer: { + command: "node server.cjs", + url: `${BASE_URL}/health`, + reuseExistingServer: false, + stdout: "pipe", + stderr: "pipe", + env: { + NODE_ENV: "development", + DATABASE_URL: "postgres://localhost/dev", + }, + }, +}); diff --git a/examples/backend-node-cjs/pnpm-lock.yaml b/examples/backend-node-cjs/pnpm-lock.yaml new file mode 100644 index 0000000..6e5e8bd --- /dev/null +++ b/examples/backend-node-cjs/pnpm-lock.yaml @@ -0,0 +1,1050 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@hono/node-server': + specifier: 2.0.2 + version: 2.0.2(hono@4.12.18) + '@vlandoss/env': + specifier: file:../../package/.local/vlandoss-env.tgz + version: file:../../package/.local/vlandoss-env.tgz(zod@4.3.6) + hono: + specifier: 4.12.18 + version: 4.12.18 + zod: + specifier: 4.3.6 + version: 4.3.6 + devDependencies: + '@biomejs/biome': + specifier: 2.4.15 + version: 2.4.15 + '@playwright/test': + specifier: 1.59.1 + version: 1.59.1 + '@rrlab/biome-config': + specifier: 0.0.2 + version: 0.0.2(@biomejs/biome@2.4.15) + '@rrlab/biome-plugin': + specifier: 1.1.0 + version: 1.1.0(@biomejs/biome@2.4.15)(@pnpm/logger@1001.0.1)(@rrlab/cli@1.1.0(@pnpm/logger@1001.0.1)) + '@rrlab/cli': + specifier: 1.1.0 + version: 1.1.0(@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: 1.1.0 + version: 1.1.0(@pnpm/logger@1001.0.1)(@rrlab/cli@1.1.0(@pnpm/logger@1001.0.1))(typescript@6.0.3) + '@types/node': + specifier: 24.12.4 + version: 24.12.4 + playwright: + specifier: 1.59.1 + version: 1.59.1 + typescript: + specifier: 6.0.3 + version: 6.0.3 + +packages: + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + 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.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + 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'} + + '@hono/node-server@2.0.2': + resolution: {integrity: sha512-tXlTi1h/4V7sDe7i97IVP+9re9ZU7wXZZggnR5ucCRclf1+AX6YhGStrR5w8bLj+3Mlyl0pKfBh9gqTqqnGKfQ==} + engines: {node: '>=20'} + peerDependencies: + hono: ^4 + + '@playwright/test@1.59.1': + resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} + engines: {node: '>=18'} + hasBin: true + + '@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@1.1.0': + resolution: {integrity: sha512-DJll0Yfw3jifr7wUz20sDz7Ow6dCOtsvJYT6su3zM7niPb0KZ7OI3xrc4wPgf5dbsgbttKyv+6dzeKQsROmg7Q==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@biomejs/biome': '>=2.0.0' + '@rrlab/cli': ^1.1.0 + + '@rrlab/cli@1.1.0': + resolution: {integrity: sha512-jK+NGnxfr5dfZze7gynYpmvMiecPxC+Z67GuHVxqSW6pM4gr1nw8q8OHALl4urmZbDnXmKMAIoSh/acvfp/YbA==} + 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@1.1.0': + resolution: {integrity: sha512-uHQOP7fgwFeLsiP36+0TMR7iDjb1LH8mP1d9loRTzMwrtlcl2CqM+5DmIJBdwb6n/sBe2+Ej6wW61IL8q+5o5Q==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@rrlab/cli': ^1.1.0 + 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.7.0': + resolution: {integrity: sha512-Z/2oPgBW3xrkL0L8Ug6WRmDmaYR7X8V38IaIMIcZhMCnlTXZALrajfj8n/ZAe07Y/A0kkoXz/ZIgJKYFoX+Twg==} + engines: {node: '>=20.0.0'} + + '@vlandoss/env@file:../../package/.local/vlandoss-env.tgz': + resolution: {integrity: sha512-QbjtkuZTLSs1riHmy7RM2Q0wMJsOugOpCsk5eNZBpd7C4s3Wy2mxsjRv6a+uvOsxgnMFYwkPNiIICM1sFlUJRQ==, tarball: file:../../package/.local/vlandoss-env.tgz} + version: 0.2.1 + engines: {node: '>=22.12.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 + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + 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'} + + hono@4.12.18: + resolution: {integrity: sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==} + engines: {node: '>=16.9.0'} + + 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.3.6: + resolution: {integrity: sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==} + 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==} + + playwright-core@1.59.1: + resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.59.1: + resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==} + engines: {node: '>=18'} + hasBin: true + + 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.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + 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.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@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': {} + + '@hono/node-server@2.0.2(hono@4.12.18)': + dependencies: + hono: 4.12.18 + + '@playwright/test@1.59.1': + dependencies: + playwright: 1.59.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.0 + + '@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.0 + + '@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@1.1.0(@biomejs/biome@2.4.15)(@pnpm/logger@1001.0.1)(@rrlab/cli@1.1.0(@pnpm/logger@1001.0.1))': + dependencies: + '@biomejs/biome': 2.4.15 + '@rrlab/cli': 1.1.0(@pnpm/logger@1001.0.1) + '@vlandoss/clibuddy': 0.7.0(@pnpm/logger@1001.0.1) + comment-json: 4.2.5 + transitivePeerDependencies: + - '@pnpm/logger' + + '@rrlab/cli@1.1.0(@pnpm/logger@1001.0.1)': + dependencies: + '@clack/prompts': 0.11.0 + '@usage-spec/commander': 1.1.0 + '@vlandoss/clibuddy': 0.7.0(@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@1.1.0(@pnpm/logger@1001.0.1)(@rrlab/cli@1.1.0(@pnpm/logger@1001.0.1))(typescript@6.0.3)': + dependencies: + '@rrlab/cli': 1.1.0(@pnpm/logger@1001.0.1) + '@vlandoss/clibuddy': 0.7.0(@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.7.0(@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 + + fsevents@2.3.2: + optional: true + + 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: {} + + hono@4.12.18: {} + + 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.3.6: {} + + 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.0 + 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.0 + 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.3.6 + 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 + + playwright-core@1.59.1: {} + + playwright@1.59.1: + dependencies: + playwright-core: 1.59.1 + optionalDependencies: + fsevents: 2.3.2 + + 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.0: {} + + 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/backend-node-cjs/run-run.config.mts b/examples/backend-node-cjs/run-run.config.mts new file mode 100644 index 0000000..c5cd9f7 --- /dev/null +++ b/examples/backend-node-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/backend-node-cjs/server.cjs b/examples/backend-node-cjs/server.cjs new file mode 100644 index 0000000..8fbff73 --- /dev/null +++ b/examples/backend-node-cjs/server.cjs @@ -0,0 +1,2 @@ +// CommonJS entry: load the server the way config-loading tools load a config — via require(). +require("./src/server.ts"); diff --git a/examples/backend-node-cjs/src/config/development.ts b/examples/backend-node-cjs/src/config/development.ts new file mode 100644 index 0000000..c155ed3 --- /dev/null +++ b/examples/backend-node-cjs/src/config/development.ts @@ -0,0 +1,7 @@ +import type { EnvConfig } from "../env/schema.ts"; + +export default { + log: { LEVEL: "debug" }, + server: { PORT: 3001, HOST: "127.0.0.1" }, + db: { URL: "postgres://localhost/dev", LOGGING: true }, +} satisfies EnvConfig; diff --git a/examples/backend-node-cjs/src/config/production.ts b/examples/backend-node-cjs/src/config/production.ts new file mode 100644 index 0000000..0ecabe0 --- /dev/null +++ b/examples/backend-node-cjs/src/config/production.ts @@ -0,0 +1,7 @@ +import type { EnvConfig } from "../env/schema.ts"; + +export default { + log: { LEVEL: "info" }, + server: { PORT: 3001, HOST: "0.0.0.0" }, + db: { LOGGING: false }, +} satisfies EnvConfig; diff --git a/examples/backend-node-cjs/src/env/index.ts b/examples/backend-node-cjs/src/env/index.ts new file mode 100644 index 0000000..4b4f6a5 --- /dev/null +++ b/examples/backend-node-cjs/src/env/index.ts @@ -0,0 +1,11 @@ +import { defineEnv } from "@vlandoss/env"; +import { loadConfig } from "@vlandoss/env/fs"; +import { Env } from "./schema.ts"; + +export const env = defineEnv({ + schema: Env, + config: loadConfig(Env), + vars: { + db: { URL: "DATABASE_URL" }, + }, +}); diff --git a/examples/backend-node-cjs/src/env/schema.ts b/examples/backend-node-cjs/src/env/schema.ts new file mode 100644 index 0000000..ff4e98d --- /dev/null +++ b/examples/backend-node-cjs/src/env/schema.ts @@ -0,0 +1,14 @@ +import { type Config, schema } from "@vlandoss/env"; +import * as e from "@vlandoss/env/zod"; +import * as z from "zod"; + +export const Env = schema({ + log: { LEVEL: e.logLevel }, + server: { PORT: e.port, HOST: e.host }, + db: { + URL: z.url(), + LOGGING: e.bool.default(false), + }, +}); + +export type EnvConfig = Config; diff --git a/examples/backend-node-cjs/src/server.ts b/examples/backend-node-cjs/src/server.ts new file mode 100644 index 0000000..99cd90a --- /dev/null +++ b/examples/backend-node-cjs/src/server.ts @@ -0,0 +1,12 @@ +import { serve } from "@hono/node-server"; +import { Hono } from "hono"; +import { env } from "./env/index.ts"; + +// biome-ignore format: I prefer to keep the code as is for better readability +const app = new Hono() + .get("/health", (c) => c.json({ ok: true })) + .get("/env", (c) => c.json({ env })); + +serve({ fetch: app.fetch, port: env.server.PORT, hostname: env.server.HOST }, () => { + console.log(`[${env.$name}] listening on http://${env.server.HOST}:${env.server.PORT}`); +}); diff --git a/examples/backend-node-cjs/test/e2e/server.spec.ts b/examples/backend-node-cjs/test/e2e/server.spec.ts new file mode 100644 index 0000000..a7a506c --- /dev/null +++ b/examples/backend-node-cjs/test/e2e/server.spec.ts @@ -0,0 +1,25 @@ +import { expect, test } from "@playwright/test"; + +test("loads development config via auto-discovery", async ({ request }) => { + const res = await request.get("/env"); + + expect(res.ok()).toBe(true); + expect(await res.json()).toStrictEqual({ + env: { + $name: "development", + IS_DEV: true, + IS_PROD: false, + IS_TEST: false, + log: { LEVEL: "debug" }, + server: { PORT: 3001, HOST: "127.0.0.1" }, + db: { URL: "postgres://localhost/dev", LOGGING: true }, + }, + }); +}); + +test("/health responds with current env name", async ({ request }) => { + const res = await request.get("/health"); + + expect(res.ok()).toBe(true); + expect(await res.json()).toStrictEqual({ ok: true }); +}); diff --git a/examples/backend-node-cjs/test/e2e/validation.spec.ts b/examples/backend-node-cjs/test/e2e/validation.spec.ts new file mode 100644 index 0000000..642b034 --- /dev/null +++ b/examples/backend-node-cjs/test/e2e/validation.spec.ts @@ -0,0 +1,68 @@ +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { expect, test } from "@playwright/test"; + +// Boot through the CommonJS entry (`server.cjs` -> `require("./src/server.ts")`), +// the same require()-based path the e2e webServer uses. +const SERVER_CJS = fileURLToPath(new URL("../../server.cjs", import.meta.url)); +const CWD = fileURLToPath(new URL("../../", import.meta.url)); + +function runServer(env: Record) { + // Strip any seeded NODE_ENV/DATABASE_URL from the runner shell so the + // spawned child only sees what the test explicitly passes. + const baseEnv = { ...process.env }; + delete baseEnv.NODE_ENV; + delete baseEnv.DATABASE_URL; + + const result = spawnSync("node", [SERVER_CJS], { + cwd: CWD, + env: { ...baseEnv, ...env }, + encoding: "utf8", + timeout: 8_000, + }); + + return { + code: result.status, + combined: (result.stdout ?? "") + (result.stderr ?? ""), + }; +} + +test.describe("env validation at boot", () => { + test("crashes when DATABASE_URL is missing in production", () => { + const { code, combined } = runServer({ + NODE_ENV: "production", + DATABASE_URL: "", + }); + expect(code).not.toBe(0); + expect(combined).toMatch(/db\.URL|DATABASE_URL/i); + }); + + test("crashes when PORT is non-numeric (coerce fails positive int)", () => { + const { code, combined } = runServer({ + NODE_ENV: "development", + DATABASE_URL: "postgres://localhost/dev", + SERVER_PORT: "not-a-number", + }); + expect(code).not.toBe(0); + expect(combined).toMatch(/server\.PORT|PORT/i); + }); + + test("crashes for unknown log level enum value", () => { + const { code, combined } = runServer({ + NODE_ENV: "development", + DATABASE_URL: "postgres://localhost/dev", + LOG_LEVEL: "verbose", + }); + expect(code).not.toBe(0); + expect(combined).toMatch(/log\.LEVEL|LEVEL/i); + }); + + test("crashes when no config file matches the env name (auto-discovery returns {} then validate kicks in)", () => { + const { code, combined } = runServer({ + NODE_ENV: "staging", + DATABASE_URL: "postgres://localhost/dev", + }); + expect(code).not.toBe(0); + expect(combined).toMatch(/Invalid value at/); + }); +}); diff --git a/examples/backend-node-cjs/tsconfig.json b/examples/backend-node-cjs/tsconfig.json new file mode 100644 index 0000000..6a5e7a2 --- /dev/null +++ b/examples/backend-node-cjs/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@rrlab/ts-config/no-dom/app", + "include": ["src", "test", "playwright.config.ts"] +} diff --git a/examples/backend-node/pnpm-lock.yaml b/examples/backend-node/pnpm-lock.yaml index 937e67b..a5e2c03 100644 --- a/examples/backend-node/pnpm-lock.yaml +++ b/examples/backend-node/pnpm-lock.yaml @@ -271,9 +271,9 @@ packages: engines: {node: '>=20.0.0'} '@vlandoss/env@file:../../package/.local/vlandoss-env.tgz': - resolution: {integrity: sha512-WqMW2GO7Ir804EbXERSgbSz44GI7UdYksMFEMGTReMTEg/G3juegiX5W1leth0sElHmVuFhGGRaLzbKWo+NVgg==, tarball: file:../../package/.local/vlandoss-env.tgz} + resolution: {integrity: sha512-68fxC1E6fzAnrm+2Pkcb1zIqqA0YMpCr011QKx5mBqLAyN2My1EcIPcFA31KmmvyHTOk33+ufYCESLLVXISzMA==, tarball: file:../../package/.local/vlandoss-env.tgz} version: 0.2.1 - engines: {node: '>=20.0.0'} + engines: {node: '>=22.18.0'} peerDependencies: react: '>=19' react-dom: '>=19' diff --git a/examples/backend-node/src/env/index.ts b/examples/backend-node/src/env/index.ts index be6e9b7..b647cb6 100644 --- a/examples/backend-node/src/env/index.ts +++ b/examples/backend-node/src/env/index.ts @@ -2,7 +2,7 @@ import { defineEnv } from "@vlandoss/env"; import { loadConfig } from "@vlandoss/env/fs"; import { Env } from "./schema.ts"; -const config = await loadConfig(Env); +const config = loadConfig(Env); export const env = defineEnv({ schema: Env, diff --git a/examples/ssr-react-router/app/env/env.server.ts b/examples/ssr-react-router/app/env/env.server.ts index 0e9209d..048d570 100644 --- a/examples/ssr-react-router/app/env/env.server.ts +++ b/examples/ssr-react-router/app/env/env.server.ts @@ -2,7 +2,7 @@ import { defineEnv } from "@vlandoss/env"; import { loadConfig } from "@vlandoss/env/fs"; import { ServerEnv } from "./schema.server.ts"; -const config = await loadConfig({ schema: ServerEnv, pattern: "app/config/{env}.ts" }); +const config = loadConfig({ schema: ServerEnv, pattern: "app/config/{env}.ts" }); export const env = defineEnv({ schema: ServerEnv, diff --git a/examples/ssr-react-router/pnpm-lock.yaml b/examples/ssr-react-router/pnpm-lock.yaml index 7de517e..8a72cd8 100644 --- a/examples/ssr-react-router/pnpm-lock.yaml +++ b/examples/ssr-react-router/pnpm-lock.yaml @@ -891,9 +891,9 @@ packages: engines: {node: '>=20.0.0'} '@vlandoss/env@file:../../package/.local/vlandoss-env.tgz': - resolution: {integrity: sha512-WqMW2GO7Ir804EbXERSgbSz44GI7UdYksMFEMGTReMTEg/G3juegiX5W1leth0sElHmVuFhGGRaLzbKWo+NVgg==, tarball: file:../../package/.local/vlandoss-env.tgz} + resolution: {integrity: sha512-sY/lLIpm0BBNCYmO1VrJL+9Ss/k4VCFxMhgvKNAo47ypl55PEnAHdaYqgL6gWIRa5bvZqTCMOfRe6G3GBe9mnw==, tarball: file:../../package/.local/vlandoss-env.tgz} version: 0.2.1 - engines: {node: '>=20.0.0'} + engines: {node: '>=22.18.0'} peerDependencies: react: '>=19' react-dom: '>=19' diff --git a/package/package.json b/package/package.json index 0063156..3251a38 100644 --- a/package/package.json +++ b/package/package.json @@ -71,7 +71,7 @@ } }, "engines": { - "node": ">=20.0.0" + "node": ">=22.12.0" }, "devDependencies": { "@rrlab/ts-config": "catalog:", diff --git a/package/src/__tests__/fixtures-cjs/config/development.cjs b/package/src/__tests__/fixtures-cjs/config/development.cjs new file mode 100644 index 0000000..0a17b08 --- /dev/null +++ b/package/src/__tests__/fixtures-cjs/config/development.cjs @@ -0,0 +1,3 @@ +module.exports = { + server: { PORT: 8888, HOST: "cjs.local" }, +}; diff --git a/package/src/__tests__/fixtures-cjs/config/staging.cjs b/package/src/__tests__/fixtures-cjs/config/staging.cjs new file mode 100644 index 0000000..9e2d56c --- /dev/null +++ b/package/src/__tests__/fixtures-cjs/config/staging.cjs @@ -0,0 +1,8 @@ +// Exports an object that literally has a `default` key alongside the real +// config. `unwrapDefault` must NOT strip this object to its `default` value; +// for CJS (no `Symbol.toStringTag === "Module"`), `module.exports` IS the +// config, sibling keys included. +module.exports = { + default: "not-the-config", + server: { PORT: 9999, HOST: "cjs.staging" }, +}; diff --git a/package/src/__tests__/fixtures-ts/config/development.ts b/package/src/__tests__/fixtures-ts/config/development.ts new file mode 100644 index 0000000..c27cdfd --- /dev/null +++ b/package/src/__tests__/fixtures-ts/config/development.ts @@ -0,0 +1,6 @@ +export default { + server: { + PORT: 4242, + HOST: "ts.local", + }, +}; diff --git a/package/src/__tests__/fs.test.ts b/package/src/__tests__/fs.test.ts index 663ea85..26ab2d1 100644 --- a/package/src/__tests__/fs.test.ts +++ b/package/src/__tests__/fs.test.ts @@ -9,6 +9,8 @@ const S = schema({ const fixturesDir = new URL("./fixtures", import.meta.url).pathname; const mjsDir = new URL("./fixtures-mjs", import.meta.url).pathname; +const tsDir = new URL("./fixtures-ts", import.meta.url).pathname; +const cjsDir = new URL("./fixtures-cjs", import.meta.url).pathname; const originalCwd = process.cwd(); afterEach(() => { @@ -21,34 +23,55 @@ describe("loadConfig(schema) — auto-discovery", () => { process.env.ENV = "development"; }); - it("discovers config/.json under cwd", async () => { + it("discovers config/.json under cwd", () => { process.chdir(fixturesDir); - const config = await loadConfig(S); - const env = defineEnv({ schema: S, config, runtimeEnv: {} }); + const env = defineEnv({ schema: S, config: loadConfig(S), runtimeEnv: {} }); expect(env.server.PORT).toBe(3000); expect(env.server.HOST).toBe("localhost"); }); - it("falls back to src/config/.json when root has no match", async () => { + it("loads a .ts config via require()", () => { + process.chdir(tsDir); + const env = defineEnv({ schema: S, config: loadConfig(S), runtimeEnv: {} }); + expect(env.server.PORT).toBe(4242); + expect(env.server.HOST).toBe("ts.local"); + }); + + it("loads a .cjs config (module.exports IS the config — sibling keys preserved)", () => { + process.chdir(cjsDir); + const env = defineEnv({ schema: S, config: loadConfig(S), runtimeEnv: {} }); + expect(env.server.PORT).toBe(8888); + expect(env.server.HOST).toBe("cjs.local"); + }); + + it("does NOT strip a CJS exports object that owns a `default` property name", () => { + // staging.cjs: `module.exports = { default: "not-the-config", server: { PORT: 9999, ... } }`. + // A CJS module is NOT an ES namespace (no Symbol.toStringTag === "Module"), + // so `unwrapDefault` must return the whole exports — sibling `server` MUST survive. + process.env.ENV = "staging"; + process.chdir(cjsDir); + const env = defineEnv({ schema: S, config: loadConfig(S), runtimeEnv: {} }); + expect(env.server.PORT).toBe(9999); + expect(env.server.HOST).toBe("cjs.staging"); + }); + + it("falls back to src/config/.json when root has no match", () => { process.env.ENV = "staging"; process.chdir(fixturesDir); - const config = await loadConfig(S); - const env = defineEnv({ schema: S, config, runtimeEnv: {} }); + const env = defineEnv({ schema: S, config: loadConfig(S), runtimeEnv: {} }); expect(env.server.PORT).toBe(5000); expect(env.server.HOST).toBe("staging.example.com"); }); - it("picks up .mjs when no .ts/.mts/.js precedes (extension priority)", async () => { + it("picks up .mjs when no .ts/.mts/.js precedes (extension priority)", () => { process.chdir(mjsDir); - const config = await loadConfig(S); - const env = defineEnv({ schema: S, config, runtimeEnv: {} }); + const env = defineEnv({ schema: S, config: loadConfig(S), runtimeEnv: {} }); expect(env.server.PORT).toBe(7777); }); - it("returns an empty object when nothing matches (silent fallback)", async () => { + it("returns an empty object when nothing matches (silent fallback)", () => { process.chdir(originalCwd); // no config/ folder at package root - const config = await loadConfig(S); - expect(config).toEqual({}); + expect(loadConfig(S)).toEqual({}); }); }); @@ -57,47 +80,59 @@ describe("loadConfig({ schema, pattern }) — template", () => { process.env.ENV = "development"; }); - it("substitutes {env} and loads the resolved file", async () => { + it("substitutes {env} and loads the resolved file", () => { process.chdir(fixturesDir); - const config = await loadConfig({ schema: S, pattern: "config/{env}.json" }); - const env = defineEnv({ schema: S, config, runtimeEnv: {} }); + const env = defineEnv({ schema: S, config: loadConfig({ schema: S, pattern: "config/{env}.json" }), runtimeEnv: {} }); expect(env.server.PORT).toBe(3000); }); - it("works for .mjs", async () => { + it("works for .mjs", () => { process.chdir(mjsDir); - const config = await loadConfig({ schema: S, pattern: "config/{env}.mjs" }); - const env = defineEnv({ schema: S, config, runtimeEnv: {} }); - expect(env.server.PORT).toBe(7777); - expect(env.server.HOST).toBe("mjs.local"); + const config = loadConfig({ schema: S, pattern: "config/{env}.mjs" }); + expect(config).toEqual({ server: { PORT: 7777, HOST: "mjs.local" } }); }); - it("respects the current ENV when substituting {env}", async () => { + it("respects the current ENV when substituting {env}", () => { process.env.ENV = "production"; process.chdir(fixturesDir); - const config = await loadConfig({ schema: S, pattern: "config/{env}.json" }); - const env = defineEnv({ schema: S, config, runtimeEnv: {} }); + const env = defineEnv({ schema: S, config: loadConfig({ schema: S, pattern: "config/{env}.json" }), runtimeEnv: {} }); expect(env.server.PORT).toBe(8080); expect(env.server.HOST).toBe("0.0.0.0"); }); - it("throws when the resolved file does not exist", async () => { + it("throws when the resolved file does not exist", () => { process.env.ENV = "missing"; process.chdir(fixturesDir); - await expect(loadConfig({ schema: S, pattern: "config/{env}.json" })).rejects.toThrow(/No config file found at/); + expect(() => loadConfig({ schema: S, pattern: "config/{env}.json" })).toThrow(/No config file found at/); }); - it("throws when the pattern is missing the {env} placeholder", async () => { + it("throws when the pattern is missing the {env} placeholder", () => { process.chdir(fixturesDir); - await expect(loadConfig({ schema: S, pattern: "config/development.json" })).rejects.toThrow( - /must contain the "\{env\}" placeholder/, - ); + expect(() => loadConfig({ schema: S, pattern: "config/development.json" })).toThrow(/must contain the "\{env\}" placeholder/); }); - it("throws when a .mjs module has no default export", async () => { + it("throws when a .mjs module has no default export", () => { process.env.ENV = "broken"; process.chdir(mjsDir); - await expect(loadConfig({ schema: S, pattern: "config/{env}.mjs" })).rejects.toThrow(/must have a default export/); + expect(() => loadConfig({ schema: S, pattern: "config/{env}.mjs" })).toThrow(/must have a default export/); + }); +}); + +describe("loadConfig — `cwd` option", () => { + beforeEach(() => { + process.env.ENV = "development"; + }); + + it("auto-discovery resolves against `cwd` instead of process.cwd()", () => { + // No chdir — process.cwd() is the package root (no config/ folder there). + const env = defineEnv({ schema: S, config: loadConfig({ schema: S, cwd: fixturesDir }), runtimeEnv: {} }); + expect(env.server.PORT).toBe(3000); + expect(env.server.HOST).toBe("localhost"); + }); + + it("template `pattern` resolves against `cwd` instead of process.cwd()", () => { + const config = loadConfig({ schema: S, pattern: "config/{env}.mjs", cwd: mjsDir }); + expect(config).toEqual({ server: { PORT: 7777, HOST: "mjs.local" } }); }); }); @@ -107,24 +142,24 @@ describe("loadConfig — return type", () => { process.chdir(fixturesDir); }); - it("returns Promise> from the short form", async () => { - const config = await loadConfig(S); + it("returns Config directly (synchronous, not a promise)", () => { + const config = loadConfig(S); expectTypeOf(config).toEqualTypeOf>(); const env = defineEnv({ schema: S, config, runtimeEnv: {} }); expect(env.server.PORT).toBe(3000); }); - it("returns Promise> from the options form", async () => { - const config = await loadConfig({ schema: S, pattern: "config/{env}.json" }); + it("returns Config from the options form", () => { + const config = loadConfig({ schema: S, pattern: "config/{env}.json" }); expectTypeOf(config).toEqualTypeOf>(); }); }); describe("loadConfig + defineEnv composition", () => { - it("typical Node app pattern: await loadConfig(schema) then pass to defineEnv", async () => { + it("typical app pattern: loadConfig(schema) then pass to defineEnv (no await)", () => { process.env.ENV = "staging"; process.chdir(fixturesDir); - const config = await loadConfig({ schema: S, pattern: "src/config/{env}.json" }); + const config = loadConfig({ schema: S, pattern: "src/config/{env}.json" }); const env = defineEnv({ schema: S, config, diff --git a/package/src/fs.ts b/package/src/fs.ts index cea8c6c..69ab4ba 100644 --- a/package/src/fs.ts +++ b/package/src/fs.ts @@ -1,102 +1,167 @@ -import { readFile, stat } from "node:fs/promises"; +import { readFileSync, statSync } from "node:fs"; +import { createRequire } from "node:module"; import path from "node:path"; import { pathToFileURL } from "node:url"; import { envName } from "./lib/runtime.ts"; import type { Config, Schema } from "./lib/types.ts"; import { isSchema } from "./lib/validate.ts"; -const EXTENSIONS = [".ts", ".mts", ".js", ".mjs", ".json"]; +const EXTENSIONS = [".ts", ".mts", ".cts", ".js", ".mjs", ".cjs", ".json"]; const DIRS = ["config", "src/config"]; -async function isFile(p: string): Promise { - try { - return (await stat(p)).isFile(); - } catch { - return false; - } -} - -async function loadFile(absPath: string): Promise { - if (absPath.endsWith(".json")) { - const raw = await readFile(absPath, "utf8"); - return JSON.parse(raw); - } - - const mod = (await import(pathToFileURL(absPath).href)) as { default?: unknown }; - if (!("default" in mod)) { - throw new Error(`Config file "${absPath}" must have a default export`); - } - return mod.default; -} +// ─── Pure helpers (no I/O) ─────────────────────────────────────────────────── /** - * Auto-discovery: looks for `[src/]config/.{ts,mts,js,mjs,json}` in - * `process.cwd()`, in that order. Returns `undefined` when nothing matches. + * Auto-discovery candidates: `[src/]config/.{ts,mts,cts,js,mjs,cjs,json}` + * under `cwd`, in priority order (extension outer, dir inner). */ -async function autoDiscover(env: string, cwd: string): Promise { +function candidatePaths(env: string, cwd: string): string[] { + const out: string[] = []; for (const ext of EXTENSIONS) { for (const dir of DIRS) { - const candidate = path.join(cwd, dir, `${env}${ext}`); - if (await isFile(candidate)) { - return loadFile(candidate); - } + out.push(path.join(cwd, dir, `${env}${ext}`)); } } - return undefined; + return out; } -async function resolveTemplate(pattern: string, env: string, cwd: string): Promise { +/** Resolve a `{env}` template to an absolute path. Throws if the placeholder is missing. */ +function templatePath(pattern: string, env: string, cwd: string): string { if (!pattern.includes("{env}")) { throw new Error( `loadConfig pattern must contain the "{env}" placeholder. Got: "${pattern}". Drop the pattern argument to use auto-discovery, or include "{env}" to substitute the current env name.`, ); } - const resolved = path.resolve(cwd, pattern.replace(/\{env\}/g, env)); - if (!(await isFile(resolved))) { - throw new Error(`No config file found at "${resolved}" (env="${env}", pattern="${pattern}")`); + return path.resolve(cwd, pattern.replace(/\{env\}/g, env)); +} + +function notFound(resolved: string, env: string, pattern: string): Error { + return new Error(`No config file found at "${resolved}" (env="${env}", pattern="${pattern}")`); +} + +const isJson = (p: string): boolean => p.endsWith(".json"); + +/** + * Normalize a loaded module to the config object. Discriminates by + * `Symbol.toStringTag === "Module"` (set by `require(esm)` on Node, Bun, and + * Deno) so we don't confuse a CJS object that happens to own a `default` + * property name with a real ESM namespace: + * + * - ES module namespace: take `.default`, or throw if the config forgot + * `export default`. + * - CommonJS `module.exports`: use the value as-is (sibling keys preserved). + */ +function unwrapDefault(absPath: string, mod: unknown): unknown { + if (mod === null || typeof mod !== "object") return mod; + const isNamespace = (mod as Record)[Symbol.toStringTag] === "Module"; + if (isNamespace) { + if ("default" in (mod as object)) return (mod as { default: unknown }).default; + throw new Error(`Config file "${absPath}" must have a default export`); + } + // CJS `module.exports`: the value IS the config. + return mod; +} + +function normalize( + input: S | LoadConfigOptions, +): { + schema: S; + pattern?: string; + cwd?: string; +} { + return isSchema(input) ? { schema: input as S } : input; +} + +// ─── I/O ────────────────────────────────────────────────────────────────────── + +function isFile(p: string): boolean { + try { + return statSync(p).isFile(); + } catch { + return false; } - return loadFile(resolved); } +function loadFile(absPath: string): unknown { + if (isJson(absPath)) { + return JSON.parse(readFileSync(absPath, "utf8")); + } + // `require` loads the module synchronously. On Node ≥22.12 `require()` accepts + // ES modules and on ≥22.18 it strips TypeScript natively; Bun and Deno do both. + // We always require an ABSOLUTE path, so the `createRequire` referrer is only a + // formality — basing it on the config file (not `import.meta.url`) keeps this + // clean when the calling config is bundled to CJS (no `empty-import-meta` warning). + const require = createRequire(pathToFileURL(absPath)); + return unwrapDefault(absPath, require(absPath)); +} + +// ─── Public API ────────────────────────────────────────────────────────────── + export type LoadConfigOptions = { - /** The schema typing this config. Anchors the return type to `Promise>`. */ + /** The schema typing this config. Anchors the return type to `Config`. */ schema: S; /** * Layout template with the `{env}` placeholder, e.g. `"src/config/{env}.ts"`. * `{env}` is replaced with the current `envName()` and the resulting path is loaded. * Drop this option to fall back on auto-discovery. */ - pattern: string; + pattern?: string; + /** + * Base directory to resolve config paths against. Defaults to `process.cwd()`. + * Pass an explicit `cwd` when the process working directory isn't the project + * root (orchestrators, monorepo runners, SSR workers launched from elsewhere). + */ + cwd?: string; }; /** - * Load a config object for use with `defineEnv({ config })`. Returns - * `Promise>` so it pipes into `defineEnv` without a cast. + * Synchronously load a config object for use with `defineEnv({ config })`. + * Returns `Config` directly, so it works in app code and in config files + * that a tool loads via `require()` or bundles to CJS. * * Two call shapes: * * - `loadConfig(schema)` — **auto-discovery**. Scans - * `[src/]config/.{ts,mts,js,mjs,json}` under `process.cwd()` and - * returns the first match. Returns `{}` when nothing is found (silent fallback). + * `[src/]config/.{ts,mts,cts,js,mjs,cjs,json}` under `process.cwd()` + * and returns the first match. Returns `{}` when nothing is found (silent fallback). * - * - `loadConfig({ schema, pattern })` — **template**. The `pattern` must - * contain `{env}`, which is replaced with the current env name. Throws if - * the resolved file doesn't exist. + * - `loadConfig({ schema, pattern?, cwd? })` — **options form**. + * - `pattern` (with `{env}`) resolves a single explicit path; throws if missing. + * - `cwd` overrides `process.cwd()` (useful when the working directory isn't + * the project root). Applies to both auto-discovery and template resolution. * * `schema` is a typing anchor only — runtime validation happens in `defineEnv` * after merging with `runtimeEnv`. No glob, no direct-path, no `env` override: * if you need to load a non-current env, set `ENV=…` in the process env first. + * + * **Module resolution & caching:** files are loaded with `require()`. Loading a + * `.ts`/`.mts`/`.cts` config needs `require(esm)` + native TypeScript stripping + * — native on Bun and Deno, and on **Node ≥22.18**. `.mjs`/`.js`/`.cjs` only + * need `require(esm)` (Node ≥22.12). `.json` works on any supported Node. + * Module loads are cached by Node/Bun/Deno's module system: repeated calls in + * the same process for the same path return the cached module — edits to a + * `.ts`/`.mjs`/etc. config are NOT picked up until the process restarts. + * `.json` files are re-read on every call. */ -export function loadConfig(schema: S): Promise>; -export function loadConfig(options: LoadConfigOptions): Promise>; -export async function loadConfig(input: S | LoadConfigOptions): Promise> { - const opts: LoadConfigOptions | { schema: S; pattern?: undefined } = isSchema(input) ? { schema: input as S } : input; +export function loadConfig(schema: S): Config; +export function loadConfig(options: LoadConfigOptions): Config; +export function loadConfig(input: S | LoadConfigOptions): Config { + const { pattern, cwd: cwdOpt } = normalize(input); const env = envName(); - const cwd = process.cwd(); + const cwd = cwdOpt ?? process.cwd(); + + if (pattern === undefined) { + for (const candidate of candidatePaths(env, cwd)) { + if (isFile(candidate)) { + return loadFile(candidate) as Config; + } + } + return {} as Config; + } - if (opts.pattern === undefined) { - const result = await autoDiscover(env, cwd); - return (result ?? {}) as Config; + const resolved = templatePath(pattern, env, cwd); + if (!isFile(resolved)) { + throw notFound(resolved, env, pattern); } - return (await resolveTemplate(opts.pattern, env, cwd)) as Config; + return loadFile(resolved) as Config; } diff --git a/package/src/vite.ts b/package/src/vite.ts index 53fa274..34ceb7f 100644 --- a/package/src/vite.ts +++ b/package/src/vite.ts @@ -3,7 +3,7 @@ import path from "node:path"; import type { Plugin } from "vite"; import { BUILD_TIME_ENV_NAME_ID } from "./lib/const.ts"; -const EXTENSIONS = [".ts", ".mts", ".js", ".mjs", ".json"]; +const EXTENSIONS = [".ts", ".mts", ".cts", ".js", ".mjs", ".cjs", ".json"]; const DIRS = ["config", "src/config"]; /** @@ -39,7 +39,7 @@ export type EnvConfigOptions = { * Vite plugin that: * * 1. Resolves an alias (`#config` by default) to the config file matching - * Vite's `mode`. Discovery is `[src/]config/.{ts,mts,js,mjs,json}` — + * Vite's `mode`. Discovery is `[src/]config/.{ts,mts,cts,js,mjs,cjs,json}` — * same algorithm as `loadConfig` in `@vlandoss/env/fs`. Only the * matched file enters the bundle. * 2. Injects `define: { __ENV_NAME__: JSON.stringify(mode) }`. The core's @@ -97,7 +97,7 @@ export function envConfig(options: EnvConfigOptions = {}): Plugin { load(id) { if (id === VIRTUAL_MISSING_ID) { throw new Error( - `@vlandoss/env/vite: no config file found for mode "${resolvedMode}" — searched [src/]config/${resolvedMode}.{ts,mts,js,mjs,json} under ${cwd}`, + `@vlandoss/env/vite: no config file found for mode "${resolvedMode}" — searched [src/]config/${resolvedMode}.{ts,mts,cts,js,mjs,cjs,json} under ${cwd}`, ); } return undefined;