diff --git a/.changeset/readme-structure.md b/.changeset/readme-structure.md new file mode 100644 index 0000000..1050e70 --- /dev/null +++ b/.changeset/readme-structure.md @@ -0,0 +1,5 @@ +--- +"@variablelab/env": patch +--- + +The README gains a usage example, an entry points table and absolute links, and the package now ships its MIT LICENSE. diff --git a/CLAUDE.md b/CLAUDE.md index 605e463..a1fe952 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ 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. -See [README.md](./README.md) for setup, tooling, and commands. +See [docs/development.md](./docs/development.md) for setup, tooling, and commands. ## Repository structure @@ -11,7 +11,7 @@ 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`) +- [`docs/`](./docs) — repository docs (`contributing.md`, `development.md`) ## Release diff --git a/LICENSE b/LICENSE index 7e7c82c..f65a6b1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 rcrd +Copyright (c) 2026 rcrd Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index eb31338..cdfdc79 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,33 @@

- +

-

env

+

env

Only declared variables, checked at boot.

-[![npm version](https://img.shields.io/npm/v/@variablelab/env?label=%40variablelab%2Fenv&color=blue)](https://www.npmjs.com/package/@variablelab/env) -[![CI](https://github.com/withvariablelab/env/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/withvariablelab/env/actions/workflows/ci.yml) -[![changesets](https://img.shields.io/badge/maintained%20with-changesets-176de3.svg)](https://github.com/changesets/changesets) +This repository holds env: the `@variablelab/env` library, its docs site and its examples. -Contract-first environment configuration with typed schemas — runtime-agnostic core (Node, Bun, Deno, browser, Workers, Edge) with opt-in adapters for Node, Vite, and React (SSR). - -```bash +```sh pnpm add @variablelab/env ``` -**[lab.variable.land/env/docs](https://lab.variable.land/env/docs)** — full docs +Usage and the entry points are in the library's [README](package/README.md), and everything else is in the [docs](https://lab.variable.land/env/docs/getting-started/installation). ## Repository -This is the monorepo. Three things live here: - -| Path | What it is | -| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | -| [`package/`](./package) | The [`@variablelab/env`](./package) library — published to npm | -| [`docsite/`](./docsite) | The Fumadocs site behind [lab.variable.land/env/docs](https://lab.variable.land/env/docs), on Cloudflare Workers | -| [`examples/`](./examples) | 9 runtime-isolated demos (Node, Bun, Deno, Workers, Edge, Vite SPA, SSR) — each one is a real consumer of the published tarball | - -## Working on it - -The whole thing is orchestrated with [mise](https://mise.jdx.dev). Get the runtimes, install, run the e2e suites: - -```bash -mise install # installs node + pnpm; bun/deno added on demand by examples -mise run setup # root deps + pack the library + install every example + Playwright browsers -mise run test:e2e # runs the e2e suite of every example -``` - -Day-to-day: - -```bash -mise run docsite # docs site dev server -mise run lib:test # unit tests for @variablelab/env -mise run lib:build # build @variablelab/env -mise run check # JS & TS check across package + docsite -``` - -Run `mise tasks` for the full list. - -For everything else — the per-task reference, the per-example layout, how to iterate on the library while seeing changes in the examples, troubleshooting — see [DEVELOPMENT.md](./docs/DEVELOPMENT.md). +- [`package/`](package): the `@variablelab/env` library, published to npm. +- [`docsite/`](docsite): the docs at [lab.variable.land/env/docs](https://lab.variable.land/env/docs), built with Fumadocs and served by Cloudflare Workers. +- [`examples/`](examples): runtime-isolated apps for Node, Bun, Deno, Workers, Edge, a Vite SPA and SSR. Each one installs the packed library, as an outside consumer would. ## Contributing -Issues, ideas, and PRs welcome. Read [CONTRIBUTING.md](./docs/CONTRIBUTING.md) before opening a PR — it covers the branch / commit / changeset conventions and the release flow. +The [contributing guide](docs/contributing.md) covers issues, pull requests and releases, and the [development guide](docs/development.md) covers setting up the repo and running its checks. ## License -[MIT](./LICENSE) © [Variable Land](https://variable.land) +The code is [MIT](LICENSE). The Sign in `package/brand/` is not: its use is set by [BRAND-USE.md](https://github.com/withvariablelab/brand/blob/main/BRAND-USE.md). + +

+  Variable Land +

diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md deleted file mode 100644 index 2eedf8f..0000000 --- a/docs/CONTRIBUTING.md +++ /dev/null @@ -1,138 +0,0 @@ -# Contributing to `@variablelab/env` - -Thanks for taking the time to contribute. This document covers the conventions we follow so your PR can land smoothly. For local setup, the per-task reference, and how the monorepo is wired, see [DEVELOPMENT.md](./DEVELOPMENT.md). - -## Ways to contribute - -- **Report a bug.** Open an issue with a minimal repro and the actual vs. expected behavior. Include runtime (Node / Bun / Deno / Workers / Edge), `@variablelab/env` version, and the schema validator you use. -- **Propose a feature.** Open an issue first so we can align on scope before you spend time on a PR. We bias toward small, composable APIs. -- **Improve the docs.** Even one-paragraph corrections are welcome. Docs live in [`docsite/content/docs/`](../docsite/content/docs). -- **Add an example.** Demos that exercise a runtime or framework we don't cover yet are great PRs. See [adding a new example](#adding-a-new-example) below. -- **Send a fix or feature PR.** Read the rest of this document first. - -## Before you open a PR - -1. **An issue exists for non-trivial changes.** For typo fixes, dead-code removal, or a single-file bug fix, just open the PR. For anything that adds API, changes behavior, or touches public types, open an issue first. -2. **Your branch is up to date with `main`.** -3. **`mise run test:e2e` passes locally** (or at least the suites your change touches). -4. **You added a changeset** if the change affects the published `@variablelab/env`. See [Changesets](#changesets). - -## Branch and commit conventions - -### Branch names - -Use a short prefix that mirrors the change type: - -- `feat/` — new feature or new public API -- `fix/` — bug fix -- `docs/` — docs-only changes -- `chore/` — tooling, CI, dependencies, refactors with no behavior change -- `test/` — test-only changes - -The CI release workflow uses the `feat/*` and `fix/*` prefixes to publish preview releases under the `pr-` dist-tag (see [Preview releases](#preview-releases)). - -### Commit messages - -We follow [Conventional Commits](https://www.conventionalcommits.org/), kept terse: - -``` -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 -``` - -The PR title follows the same format — that title becomes the squash-merge commit. - -## Changesets - -We version and release with [Changesets](https://github.com/changesets/changesets). Every PR that affects what gets shipped to npm needs a changeset. - -Add one as part of your PR: - -```sh -pnpm changeset -``` - -That command walks you through: - -1. Which packages changed (almost always just `@variablelab/env`). -2. The bump type — `patch` (bug fix), `minor` (new feature, backwards-compatible), `major` (breaking change). -3. A user-facing description (it lands in the changelog). - -It writes a markdown file under `.changeset/` — commit it with the rest of your changes. - -**You don't need a changeset when:** - -- Only `docsite/`, `examples/`, CI files, or other non-published files changed. -- The PR is a pure `chore/*` that doesn't touch `package/src/`. - -Don't bump versions or edit `CHANGELOG.md` by hand — Changesets does both during release. - -## Pull request checklist - -- [ ] Branch named `feat/*`, `fix/*`, `docs/*`, `chore/*`, or `test/*` -- [ ] PR title follows Conventional Commits -- [ ] Tests cover the change (unit in `package/src/__tests__/`, e2e in `examples//test/e2e/` if relevant) -- [ ] `mise run lib:test` and `mise run check` pass locally (JS & TS check) -- [ ] `mise run test:e2e` passes locally for affected examples -- [ ] Changeset added (if the published `@variablelab/env` changed) -- [ ] Docs updated (if public API or behavior changed) - -## CI - -Two workflows run on every PR: - -- **CI** ([`.github/workflows/ci.yml`](../.github/workflows/ci.yml)) — static checks, unit tests, and library build inside the small pnpm workspace (`package` + `docsite`). -- **E2E** ([`.github/workflows/e2e.yml`](../.github/workflows/e2e.yml)) — a matrix job per example, each provisioning its own runtime via [`jdx/mise-action`](https://github.com/jdx/mise-action). - -Both must be green to merge. - -## Adding a new example - -Examples are runtime-isolated demos. Each one is a real consumer of the published `@variablelab/env` tarball — no shared lockfile, no workspace cheating. To add one: - -1. Create the directory under `examples//` with the source for the demo. -2. Add a `package.json` that consumes `@variablelab/env` from the local tarball: - ```jsonc - "@variablelab/env": "file:../../package/.local/variablelab-env.tgz" - ``` -3. Add a `mise.toml` with the runtime tools (`node` / `bun` / `deno` / `pnpm`) and the standard tasks (`setup`, `start`, `test:e2e`, `check`). Use any of the existing examples as a template — pick the one with the closest runtime. -4. Add a `biome.json` extending `@rrlab/biome-config` (skip this for Deno-only examples). -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 + e2e 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. - -The detailed setup mechanics — which package manager fits which runtime, why Deno needs `nodeModulesDir: "manual"`, the depends-on-`//:env:pack` rule — are documented in [DEVELOPMENT.md](./DEVELOPMENT.md#working-on-the-examples-examples). - -## Preview releases - -PRs from branches named `feat/*` or `fix/*` publish a preview build of `@variablelab/env` to npm under the `pr-` dist-tag, so reviewers can install the unmerged version: - -```sh -pnpm install @variablelab/env@pr-123 -``` - -The preview is republished on every push to the PR. - -## Release flow - -When PRs with changesets land on `main`, the [release job](../.github/workflows/ci.yml) (`changesets/action`, running as `vland-bot`) opens or updates a "chore: update versions" PR. Merging that PR runs the release job again, which publishes the new versions to npm and tags the release on GitHub. - -Maintainers don't need to manually bump versions or edit `CHANGELOG.md` — both are generated. - -## Code review expectations - -- We aim to leave a first review within a few business days. If you don't hear back in a week, ping the PR. -- Reviews focus on: correctness, type safety at API boundaries, test coverage of failure modes, runtime portability (does this break Deno / Workers?), and docs alignment. -- We're conservative about new public surface area. Smaller, composable additions are easier to land than big new namespaces. - -## Code of conduct - -Be kind and assume good faith. We don't have a formal CoC document yet — if your interaction wouldn't fly in a respectful workplace, don't post it here. - ---- - -Questions? Open a [discussion](https://github.com/withvariablelab/env/discussions) or ping us in the PR. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md deleted file mode 100644 index 15e0efd..0000000 --- a/docs/DEVELOPMENT.md +++ /dev/null @@ -1,285 +0,0 @@ -# Development - -How to set up, work in, and ship from this monorepo. For contribution conventions (branches, commits, changesets, PR flow), see [CONTRIBUTING.md](./CONTRIBUTING.md). - -## What's in here - -``` -env/ -├── package/ # @variablelab/env library — published to npm -├── docsite/ # Fumadocs site → lab.variable.land/env/docs (Cloudflare Workers) -├── 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 -├── mise.lock # Locked tool versions + checksums (each example has its own) -├── pnpm-workspace.yaml # Workspace for `package` + `docsite` only -├── lefthook.yml # Git hooks (jscheck on commit, tests on push) -└── biome.json # Root biome config (excludes examples/**) -``` - -There are two layers, by design: - -- **The pnpm workspace** (`package` + `docsite`) — these two are siblings and share the catalog'd dependency versions. -- **The runtime-isolated examples** (`examples/*`) — each one declares its own runtime, brings its own package manager (pnpm / bun / deno), has its own lockfile, and consumes `@variablelab/env` from a packed tarball. They are **not** part of the pnpm workspace; they're orchestrated by mise. - -This split is intentional: the library promises runtime-agnosticism, and the examples have to prove that. An example that secretly resolves through a shared pnpm workspace doesn't actually demonstrate it works in Bun or Deno. - -## Prerequisites - -You need [**mise**](https://mise.jdx.dev) (≥ 2025.5.0). It bootstraps everything else — Node, pnpm, bun, deno are all installed and pinned by mise. - -```sh -curl https://mise.run | sh -# or: brew install mise / pacman -S mise / etc. -``` - -> **First time using mise?** It activates by adding a hook to your shell rc. Follow the [mise install docs](https://mise.jdx.dev/getting-started.html) — once activated, every project tool magically resolves to the version pinned in the relevant `mise.toml`. - -You'll also be prompted by mise to "trust" the per-example `mise.toml` files the first time you `cd` into them. Run `mise trust` when it asks. - -## Setup from scratch - -```sh -git clone git@github.com:withvariablelab/env.git -cd env -mise install # node + pnpm -mise run setup # ↓ runs the bootstrap chain -``` - -`mise run setup` runs the following steps in order: - -1. `mise install` — installs the root tools. -2. `for d in examples/*/; do mise install -C "$d"; done` — installs each example's runtime (bun, deno, etc.). `mise install` doesn't accept monorepo globs, so the loop stays. -3. `pnpm install` — workspace deps for `package` + `docsite`. -4. `mise run "//examples/...:setup"` — runs each example's `setup` task in parallel (resolved via `[monorepo].config_roots`). Each `setup` declares `depends = ["//:env:pack"]`, so mise dedupes `env:pack` to a single execution before the example installs. -5. `mise run playwright:install` — fetches Chromium for the SPA / SSR e2e suites. - -End state: every example has its own `node_modules/`, its own lockfile, and a freshly installed copy of `@variablelab/env` extracted from the tarball. - -## Working on the library (`package/`) - -The library uses [tsdown](https://tsdown.dev) for the build and [vitest](https://vitest.dev) for unit tests. - -```sh -mise run lib:test # unit tests -mise run lib:build # emits dist/ -mise run check # JS & TS check across package + docsite -``` - -If you're hacking inside `package/`, the library's own `package.json` exposes the underlying scripts — those are what `mise run lib:*` and `mise run check` ultimately call. - -### Iterating with the examples - -When you change `package/src/`, the examples need to see the new code. The flow: - -```sh -mise run env:pack # rebuild + repack the tarball -mise run examples:bump # reinstall the tarball in every example -``` - -Each example's `setup` task declares `depends = ["//:env:pack"]` (with `sources` / `outputs` declared on `env:pack` itself), so any of these will pull in the latest tarball automatically: - -```sh -mise run //examples/backend-node:test:e2e -mise run //examples/backend-bun:start -``` - -For an active edit loop (rebuild on every save, push to all examples): - -```sh -mise run env:watch -# in another terminal -mise run //examples/backend-node:start -``` - -`env:watch` runs `tsdown --watch` plus a chokidar that pipes into `env:pack` + `examples:bump`. - -## Working on the docs (`docsite/`) - -The docs are a [TanStack Start](https://tanstack.com/start) + [Fumadocs](https://fumadocs.dev) app deployed to Cloudflare Workers. - -```sh -mise run docsite # local dev server -``` - -Anything beyond dev (production build, deploy, preview) lives as `pnpm` scripts inside `docsite/package.json` — `cd docsite && pnpm build` / `pnpm deploy` / `pnpm preview`. Those are docs-specific deployment workflows; we haven't promoted them to mise tasks because they don't cross repo boundaries. - -Content lives in `docsite/content/docs/` as MDX. Read [`docsite/README.md`](../docsite/README.md) for the full layout (routes, llms.txt endpoints, the `*.mdx` content negotiation trick, etc.). - -## Working on the examples (`examples/*`) - -Every example has the same task vocabulary, 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 test:e2e # Playwright -mise run check # JS & TS check -``` - -Or invoke from the repo root with the `//path:task` syntax: - -```sh -mise run //examples/spa-vite-plugin:dev -mise run //examples/worker-cloudflare:test:e2e -``` - -### How `@variablelab/env` is consumed - -Every example declares the dependency as a local file tarball: - -```jsonc -"@variablelab/env": "file:../../package/.local/variablelab-env.tgz" -``` - -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 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 | -| [`spa-vite-dynamic`](../examples/spa-vite-dynamic) | Vite + React | pnpm | Dynamic `import()` pattern | -| [`ssr-react-router`](../examples/ssr-react-router) | React Router 7 | pnpm | `` SSR / hydration | -| [`ssr-tanstack-start`](../examples/ssr-tanstack-start) | TanStack Start | pnpm | `` via TanStack Start | - -The `backend-*`, `worker-*`, and `edge-nextjs` examples drive Playwright in HTTP-only mode (the `request` fixture). The 4 SPA / SSR examples drive a real Chromium browser — they need `mise run playwright:install` first (already part of `setup`). - -### Adding a new example - -The full step-by-step is in [CONTRIBUTING.md](./CONTRIBUTING.md#adding-a-new-example). The shape is: - -1. New dir under `examples//`. -2. `package.json` with `"@variablelab/env": "file:../../package/.local/variablelab-env.tgz"` and the runtime's flavor of devDeps (e.g. `@types/bun` for Bun, `@types/deno` for Deno). -3. `mise.toml` with `[tools]` and the standard tasks. Copy from the closest existing example. -4. `biome.json` extending `@rrlab/biome-config` (skip for Deno-only). -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. `mise run setup` from the new dir — commit the generated lockfile. -9. `mise lock` for every platform from the new dir — commit its `mise.lock` (CI installs tools with `--locked`). - -## mise tasks reference - -Run `mise tasks` from anywhere in the repo to see what's available. The core ones: - -### Root tasks (in [`mise.toml`](../mise.toml)) - -| Task | What it does | -| --------------------------- | ----------------------------------------------------------------------------- | -| `setup` | Bootstrap everything (tools, root deps, tarball, examples, Playwright). | -| `env:pack` | Build `@variablelab/env` and pack it into `package/.local/variablelab-env.tgz`. | -| `env:watch` | Watch `package/src/`, repack on change, reinstall in every example. | -| `playwright:install` | Install Chromium for Playwright (one-time, idempotent). | -| `lib:test` | Unit tests for `@variablelab/env`. | -| `lib:build` | Build `@variablelab/env`. | -| `check` | JS & TS check across the workspace (`package` + `docsite`). | -| `test:e2e` | Run e2e for every example. | -| `examples:check` | JS & TS check across every example. | -| `examples:bump` | `reinstall` the tarball in every example. | - -### Per-example tasks (uniform across every `examples/*/mise.toml`) - -| Task | What it does | -| ------------- | ------------------------------------------------------------------------- | -| `setup` | Pack env + install deps with the example's native PM (depends on `//:env:pack`). | -| `reinstall` | Force-reinstall the env tarball. | -| `start` | Run the server (or `dev` / `build` / `preview` for the Vite-based ones). | -| `test:e2e` | Playwright. | -| `check` | JS & TS check. | - -## Why mise is the only API - -The root `package.json` carries no scripts. Every operation that crosses repo boundaries — running a workspace check, packing the lib, installing every example, running the e2e matrix — is a `mise run `. Two reasons: - -1. **One way to do each thing.** `pnpm setup` and `mise run setup` doing the same thing was just noise — devs had to remember which to type, and the docs had to mention both. -2. **CI and devs share a single contract.** What you type locally is the same thing CI runs (`mise run lib:test`, `mise run test:e2e`, …). No drift between `package.json` scripts and a separate CI command list. - -The `package/` and `docsite/` workspaces still expose plain `pnpm test` / `pnpm dev` / etc. inside their own `package.json` — those scripts are what the mise tasks ultimately delegate to. They're internal to each subproject; you only see them if you `cd` into the subproject and run `pnpm