Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/package-description.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@variablelab/env": patch
---

The package description is now env's tagline, "Only declared variables, checked at boot."
15 changes: 1 addition & 14 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,5 @@
# env

Contract-first environment configuration with typed schemas. Runtime-agnostic core (Node, Bun, Deno, browser, Workers, Edge) with opt-in adapters for Node, Vite, React (SSR), and a curated set of Zod-based environment schema primitives.
The monorepo of `@variablelab/env`: the library, its docs site and its examples.

See [docs/development.md](./docs/development.md) for setup, tooling, and commands.

## Repository structure

Monorepo managed with pnpm workspaces + Turborepo:

- [`package/`](./package/README.md) — the `@variablelab/env` npm package (source & tests)
- [`docsite/`](./docsite/README.md) — Fumadocs; published at [lab.variable.land/env/docs](https://lab.variable.land/env/docs)
- [`examples/`](./examples/README.md) — runnable example apps with Playwright e2e suites
- [`docs/`](./docs) — repository docs (`contributing.md`, `development.md`)

## Release

- This project use changesets to manage releases, never touch the `CHANGELOG.md` or bump `package.json` file directly, it is generated by changesets.
24 changes: 12 additions & 12 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ Branch from `main` with a short prefix that mirrors the change type:
- `feat/<short-slug>`: a new feature or new public API.
- `fix/<short-slug>`: a bug fix.
- `docs/<short-slug>`: a docs-only change.
- `chore/<short-slug>`: tooling, CI, dependencies, or a refactor with no behavior change.
- `ci/<short-slug>`: a change to the CI workflows.
- `chore/<short-slug>`: tooling, dependencies, or a refactor with no behavior change.
- `test/<short-slug>`: a test-only change.

The `feat/` and `fix/` prefixes also publish a preview release (see [Preview releases](#preview-releases)).
Expand All @@ -31,14 +32,14 @@ Commits follow [Conventional Commits](https://www.conventionalcommits.org/), kep
feat(zod): add zEnum primitive for case-insensitive enums
fix(react): EnvScript no longer double-encodes the JSON payload
docs: clarify resolution order for runtimeEnv
chore(ci): bump jdx/mise-action to v2
ci: bump jdx/mise-action to v4.3.0
```

The pull request title takes the same form, because it becomes the squash-merge commit.

### Checklist

- [ ] The branch is named `feat/*`, `fix/*`, `docs/*`, `chore/*` or `test/*`, and is up to date with `main`.
- [ ] The branch is named `feat/*`, `fix/*`, `docs/*`, `ci/*`, `chore/*` or `test/*`, and is up to date with `main`.
- [ ] The pull request title follows Conventional Commits.
- [ ] Tests cover the change: unit tests in `package/src/__tests__/`, and e2e tests in `examples/<name>/test/e2e/` when relevant.
- [ ] `mise run lib:test` and `mise run check` pass locally.
Expand Down Expand Up @@ -92,21 +93,20 @@ When pull requests with changesets land on `main`, the release job in [`ci.yml`]

Examples are runtime-isolated demos. Each one installs the packed `@variablelab/env` tarball, as an outside consumer would, with its own lockfile and outside the pnpm workspace. To add one:

1. Create the directory under `examples/<name>/` with the source for the demo, and give it ports no other example uses (see [Ports](../examples/README.md#ports)).
2. Add a `package.json` that consumes `@variablelab/env` from the local tarball:
1. Create the directory under `examples/<name>/` with the source for the demo, and give it ports no other example uses (the [examples list](../examples/README.md) gives the ports of each example).
2. Add a `package.json` that consumes `@variablelab/env` from the local tarball, with the runtime's own devDependencies, such as `@types/bun` for Bun or `@types/deno` for Deno:
```jsonc
"@variablelab/env": "file:../../package/.local/variablelab-env.tgz"
```
A pnpm example also copies the `.npmrc` from an existing pnpm example.
3. Add a `mise.toml` with the runtime tools (`node`, `bun`, `deno` or `pnpm`) and the standard tasks (`setup`, `start`, `test:e2e`, `check`). Use the existing example with the closest runtime as a template.
3. Add a `mise.toml` with the runtime tools (`node`, `bun`, `deno` or `pnpm`) and the [per-example tasks](development.md#per-example-tasks) in the development guide. Use the existing example with the closest runtime as a template.
4. Add a `biome.json` extending `@rrlab/biome-config`, except in a Deno-only example.
5. Add the path to `[monorepo].config_roots` in the root [`mise.toml`](../mise.toml).
6. Add a row to the matrix in [`.github/workflows/e2e.yml`](../.github/workflows/e2e.yml).
7. Update the table in [`examples/README.md`](../examples/README.md).
8. Run the install and the e2e suite, and commit the generated lockfile.
9. From the new directory, run `mise lock -p linux-x64,linux-x64-musl,linux-arm64,linux-arm64-musl,macos-x64,macos-arm64,windows-x64` and commit its `mise.lock`. CI installs tools with `--locked`, so an example without one fails E2E.
5. Add a row to the matrix in [`.github/workflows/e2e.yml`](../.github/workflows/e2e.yml).
6. Add the example to the list in [`examples/README.md`](../examples/README.md).
7. Run the install and the e2e suite, and commit the generated lockfile.
8. From the new directory, run `mise lock -p linux-x64,linux-x64-musl,linux-arm64,linux-arm64-musl,macos-x64,macos-arm64,windows-x64` and commit its `mise.lock`. CI installs tools with `--locked`, so an example without one fails E2E.

The setup mechanics, such as which package manager fits which runtime, why Deno needs `nodeModulesDir: "manual"` and the rule to depend on `//:env:pack`, are in the [development guide](development.md#working-on-the-examples-examples).
The setup mechanics are in the development guide: [how the examples consume the library](development.md#how-the-examples-consume-the-library), [why Deno needs `nodeModulesDir: "manual"`](development.md#deno-cant-import-the-library-from-the-tarball) and [the rule to depend on `//:env:pack`](development.md#task-not-found-envpack). [`examples/README.md`](../examples/README.md) gives the package manager of each example.

## Code review

Expand Down
44 changes: 11 additions & 33 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ env/
├── docs/ # Repository docs (contributing.md, development.md)
├── mise.toml # Tool versions + task orchestration
├── mise.lock # Locked tool versions + checksums (each example has its own)
├── mise.audit.toml # The dependency audit's tools, added with MISE_ENV=audit
├── mise.audit.lock # Locked versions + checksums of those tools
├── pnpm-workspace.yaml # Workspace for `package` + `docsite` only
├── lefthook.yml # Git hooks (Biome and the type check on commit, tests on push)
└── biome.json # Root biome config (excludes examples/**)
Expand Down Expand Up @@ -70,7 +72,7 @@ mise run check # JS & TS check across package + docsite
mise run test:e2e # the e2e suite of every example
```

Run `mise tasks` from anywhere in the repo to see the full list.
Run `mise tasks` to list the root tasks and those of the directory you are in, or `mise tasks ls --all` to list every example's tasks too.

### Root tasks (in [`mise.toml`](../mise.toml))

Expand All @@ -88,13 +90,13 @@ Run `mise tasks` from anywhere in the repo to see the full list.
| `examples:check` | JS & TS check across every example. |
| `examples:bump` | `reinstall` the tarball in every example. |

### Per-example tasks (the same in every `examples/*/mise.toml`)
### Per-example tasks

| Task | What it does |
| ------------- | ------------------------------------------------------------------------------------ |
| `setup` | Pack env and install deps with the example's own package manager (depends on `//:env:pack`). |
| `reinstall` | Force-reinstall the env tarball. |
| `start` | Run the server (or `dev`, `build` and `preview` in the Vite-based ones). |
| `start` | Run the server. The SPA examples have `dev`, `build` and `preview` instead, and the SSR and Next.js examples add `dev` and `build`. |
| `test:e2e` | Playwright. |
| `check` | JS & TS check. |

Expand All @@ -119,13 +121,14 @@ On every pull request to `main`:
- **E2E** ([`e2e.yml`](../.github/workflows/e2e.yml)): a matrix job per example, when the pull request touches `examples/`, `package/`, the workflow itself, the pnpm lockfile or workspace, or the root mise files. Each job sets up its own runtime with `jdx/mise-action` v4.3.0 (with `working_directory: examples/<name>`), packs the library with `mise run env:pack`, and runs `mise run //examples/<name>:test:e2e`.
- **Examples** ([`examples.yml`](../.github/workflows/examples.yml)): one job, on the same paths as E2E, that installs each example's tools, packs the library and runs `mise run examples:check`.
- **Workflow audit** ([`workflow-audit.yml`](../.github/workflows/workflow-audit.yml)): zizmor on the workflows in `.github/`.
- **Docsite** ([`docsite.yml`](../.github/workflows/docsite.yml)): the Build job runs `pnpm --filter docsite build`, which builds the docs site without deploying it. [Workers Builds](../docsite/README.md#deploying) deploys `main`.

CI, E2E and Examples use the experimental mise monorepo feature (`MISE_EXPERIMENTAL=1`).

Outside pull requests:

- **Dependency audit** ([`dependency-audit.yml`](../.github/workflows/dependency-audit.yml)): every Monday, an audit of every lockfile in the repo. It keeps an issue labeled `dependency-audit` open while it finds known vulnerabilities.
- **Docsite** ([`docsite.yml`](../.github/workflows/docsite.yml)): a Lighthouse SEO audit of the docs site, run by hand.
- **Docsite** ([`docsite.yml`](../.github/workflows/docsite.yml)): the Audit SEO job, a Lighthouse SEO audit of the docs site, run by hand.

### Git hooks

Expand Down Expand Up @@ -190,12 +193,12 @@ Content lives in `docsite/content/docs/` as MDX. [`docsite/README.md`](../docsit

## Working on the examples (`examples/*`)

Every example has the same task names, so you can `cd` into any of them and run:
Every example has `setup`, `reinstall`, `test:e2e` and `check`, so you can `cd` into any of them and run:

```sh
mise run setup # pack env + install deps (uses the example's native PM)
mise run reinstall # force-reinstall the env tarball
mise run start # run the server / dev server
mise run start # the server (`mise run dev` in the SPA examples)
mise run test:e2e # Playwright
mise run check # JS & TS check
```
Expand All @@ -219,36 +222,11 @@ Every example declares the dependency as a local file tarball:

### The examples

Each item names the runtime, the package manager and what the example shows. [`examples/README.md`](../examples/README.md) lists the entry points each one exercises.

- [`backend-node`](../examples/backend-node): Node 24, with pnpm. A plain Node server.
- [`backend-bun`](../examples/backend-bun): Bun 1.2.4, with bun. A native Bun consumer.
- [`backend-deno`](../examples/backend-deno): Deno 2 for the server and Node 24 for the test runner, with bun. Deno can't extract `file:` tarballs natively, so `bun install` hydrates `node_modules/` (flat layout) and Deno reads it through `nodeModulesDir: "manual"`.
- [`backend-node-cjs`](../examples/backend-node-cjs): Node 24, with pnpm. `loadConfig` (sync), with the server booted from a CommonJS `require()` entry.
- [`backend-bun-cjs`](../examples/backend-bun-cjs): Bun 1.2.4, with bun. `loadConfig` (sync) under a Bun CommonJS `require()` entry.
- [`backend-deno-cjs`](../examples/backend-deno-cjs): Deno 2 for the server and Node 24 for the test runner, with bun. `loadConfig` (sync) under a Deno CommonJS `require()` entry.
- [`worker-cloudflare`](../examples/worker-cloudflare): Cloudflare Workers, with pnpm. It checks `runtimeEnv: c.env` per request, without the `nodejs_compat` flag.
- [`edge-nextjs`](../examples/edge-nextjs): Next.js Edge, with pnpm. It checks the Edge runtime path.
- [`spa-vite-plugin`](../examples/spa-vite-plugin): Vite and React, with pnpm. The `envConfig()` plugin and the `#config` alias.
- [`spa-vite-dynamic`](../examples/spa-vite-dynamic): Vite and React, with pnpm. The dynamic `import()` pattern.
- [`ssr-react-router`](../examples/ssr-react-router): React Router 7, with pnpm. `<EnvScript />` for SSR and hydration.
- [`ssr-tanstack-start`](../examples/ssr-tanstack-start): TanStack Start, with pnpm. `<EnvScript />` through TanStack Start.

The `backend-*`, `worker-*` and `edge-nextjs` examples drive Playwright in HTTP-only mode (the `request` fixture). The SPA and SSR examples drive a real Chromium browser, so they need `mise run playwright:install` first, which `setup` already runs.
The examples, with the runtime, package manager, entry points, ports and what each one shows, are listed in [`examples/README.md`](../examples/README.md).

### Adding a new example

The full step-by-step is in the [contributing guide](contributing.md#adding-a-new-example). The shape is:

1. A new directory under `examples/<name>/`.
2. A `package.json` with `"@variablelab/env": "file:../../package/.local/variablelab-env.tgz"` and the runtime's own devDependencies, such as `@types/bun` for Bun or `@types/deno` for Deno.
3. A `mise.toml` with `[tools]` and the standard tasks, copied from the closest existing example.
4. A `biome.json` extending `@rrlab/biome-config`, except in a Deno-only example.
5. The path in `[monorepo].config_roots` in the root [`mise.toml`](../mise.toml).
6. A row in the matrix in [`.github/workflows/e2e.yml`](../.github/workflows/e2e.yml).
7. A row in the table in [`examples/README.md`](../examples/README.md).
8. `mise run setup` from the new directory, and a commit of the generated lockfile.
9. `mise lock` for every platform from the new directory, and a commit of its `mise.lock` (CI installs tools with `--locked`).
Follow [Adding a new example](contributing.md#adding-a-new-example) in the contributing guide.

## Troubleshooting

Expand Down
4 changes: 2 additions & 2 deletions docsite/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# docsite — agent notes

Workspace-specific guidance. The general project layout and run commands live in [README.md](./README.md).
Workspace-specific guidance. Run commands are in [docs/development.md](../docs/development.md#working-on-the-docs-docsite), and the layout is in [README.md](./README.md).

## SEO audit (Lighthouse CI)

Expand All @@ -19,6 +19,6 @@ The pinned `@lhci/cli@X.Y.Z` version lives in two places that **must be kept in

When bumping the version, update both — otherwise CI keeps restoring the old `~/.cache/pnpm/dlx` and the new bin is re-downloaded every run, defeating the cache.

URLs and budgets live in [`.lighthouserc.cjs`](./.lighthouserc.cjs). The URL list is parsed from `dist/client/env/sitemap.xml` (produced by prerendering [`src/routes/sitemap[.]xml.ts`](./src/routes/sitemap[.]xml.ts) during `vite build`) — new docs are picked up automatically. Budgets: SEO and best-practices pinned at 100 (`error`); accessibility ≥90 and performance ≥50 (`warn`). Any regression in `meta-description`, `document-title`, `is-crawlable`, etc. fails the PR.
URLs and budgets live in [`.lighthouserc.cjs`](./.lighthouserc.cjs). The URL list is parsed from `dist/client/env/sitemap.xml` (produced by prerendering [`src/routes/sitemap[.]xml.ts`](./src/routes/sitemap[.]xml.ts) during `vite build`) — new docs are picked up automatically. Budgets: SEO and best-practices pinned at 100 (`error`); accessibility ≥90 and performance ≥50 (`warn`). Any regression in `meta-description`, `document-title`, `is-crawlable` and so on fails the audit run. In CI the audit is the "Audit SEO" job of `docsite.yml`, which runs only by hand (`workflow_dispatch`); [docs/development.md](../docs/development.md#workflows) lists what runs on a pull request.

**Why Lighthouse CI instead of Unlighthouse:** an earlier version of this workflow used Unlighthouse. It worked but `unlighthouse-ci` leaks Chrome process handles after a successful scan and never exits cleanly — under GitHub Actions the step would hang several minutes after the audit finished. LHCI handles process cleanup correctly and is the more battle-tested CI primitive.
1 change: 0 additions & 1 deletion docsite/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ pnpm dev # local dev server
pnpm build # production build (Vite + Cloudflare adapter)
pnpm preview # preview the built bundle
pnpm run deploy # build + wrangler deploy
pnpm test:types # typechecks
```

`docsite/` lives in the small pnpm workspace alongside [`package/`](../package). It is **not** runtime-isolated like the [`examples/`](../examples) — those each declare their own runtime via mise and use a packed tarball of `@variablelab/env`.
Expand Down
6 changes: 3 additions & 3 deletions docsite/content/docs/api-reference/core.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ description: Exports from the @variablelab/env entrypoint — the runtime-agnost
icon: Box
---

The `@variablelab/env` entrypoint is the only one you always import — it contains the runtime-agnostic core. Adapter entrypoints (`/node`, `/vite`, `/react`, `/zod`) build on top of it.
The `@variablelab/env` entrypoint is the only one you always import — it contains the runtime-agnostic core. The other entrypoints are opt-in, and the [API reference overview](/docs/api-reference) lists them.

## Exports

| Export | Kind | Summary |
| ------------ | -------- | ------------------------------------------------------------------------------------ |
| `schema` | Function | Declare the contract — branches and Standard Schema leaves. |
| `defineEnv` | Function | Merge config + environment variables, validate, and return a typed `env` object. |
| `defineEnv` | Function | Merge config + environment variables, validate, and return a typed `env` object: `Promise<Env<S>>` when `config` is a Promise, otherwise `Env<S>`. |
| `envName` | Function | Detect the current environment name across runtimes — `development`, `production`, … |
| `readEnv` | Function | Read raw values from the active source (`process.env` on the server, `window.__env` in the browser). Returns `{}` on runtimes without `process` (Workers) — pass `runtimeEnv` to `defineEnv` instead. |
| `Config` | Type | The input shape of per-environment config files for a given schema. |
| `Config` | Type | The shape of per-environment config files for a given schema: the output type of every leaf (post-coercion), with every key optional. |
| `Env` | Type | The output shape of the resolved `env` object for a given schema. |

<Callout type="info">
Expand Down
4 changes: 2 additions & 2 deletions docsite/content/docs/api-reference/index.mdx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
title: Overview
description: Every export of @variablelab/env, organized by the entrypoint you import it from.
description: The exports of @variablelab/env, organized by the entrypoint you import them from.
icon: Map
---

The package is split into five entrypoints. Each page here documents one of them — signature, options, return type, and a minimal usage snippet. For "when to reach for this" prose, follow the link back to the guide that uses it.
The package is split into entrypoints. Each page here lists the exports of one of them. For "when to reach for this" prose, follow the link back to the guide that uses it.

## Entrypoints

Expand Down
Loading
Loading