diff --git a/.agents/skills/documentation/SKILL.md b/.agents/skills/documentation/SKILL.md new file mode 100644 index 0000000..dae5144 --- /dev/null +++ b/.agents/skills/documentation/SKILL.md @@ -0,0 +1,109 @@ +--- +name: documentation +description: "Creates, structures, and reviews technical documentation following the Diátaxis framework (tutorials, how-to guides, reference, and explanation pages). Use when a user needs to write or reorganize docs, structure a tutorial vs. a how-to guide, build reference docs or API documentation, create explanation pages, choose between Diátaxis documentation types, or improve existing documentation structure. Trigger terms include: documentation structure, Diátaxis, tutorials vs how-to guides, organize docs, user guide, reference docs, technical writing." +metadata: + tags: documentation, technical-writing, tutorials, guides, reference, diataxis +--- + +## When to use + +Use this skill when you need to create, review, or improve technical documentation following the Diátaxis framework. Examples include: +- Creating user guides +- API documentation +- Tutorial content +- Restructuring existing documentation to better serve different user needs and contexts + +## Instructions + +Organize documentation into four distinct types — tutorials, how-to guides, reference material, and explanations — each serving different user needs and contexts. + +Always ask clarifying questions about the user's context, audience, and goals **before** creating documentation. + +--- + +### Step 1 — Identify the documentation type + +Use the following decision checklist based on user signals: + +| User signal | Documentation type | +|---|---| +| "I'm new to X and want to learn it" / "walk me through" | **Tutorial** | +| "How do I…?" / "I need to accomplish X" | **How-to guide** | +| "What are the parameters/options/syntax for X?" | **Reference** | +| "Why does X work this way?" / "Help me understand X" | **Explanation** | + +Quick decision tree: +- Is the user **learning by doing** for the first time? → Tutorial +- Do they need to **solve a specific problem** they already understand? → How-to guide +- Do they need **technical facts** to look up? → Reference +- Do they want **conceptual background**? → Explanation + +--- + +### Step 2 — Apply type-specific patterns + +#### Tutorials (learning-oriented) +- **Title pattern:** Start with a verb — *"Build your first X"*, *"Create a Y from scratch"* +- Structure: Goal → Prerequisites → Numbered steps → Immediate verifiable result at each step → Final outcome +- Minimise explanation; maximise doing +- Every step must produce a visible, testable result +- **Validation:** A beginner must be able to complete the tutorial without external help + +**Example intro:** +> *"In this tutorial, you will build a simple REST API using Express. By the end, you will have a running server that responds to GET requests. No prior Express experience is needed."* + +--- + +#### How-to guides (problem-oriented) +- **Title pattern:** Frame as a task — *"How to configure X"*, *"How to deploy Y to Z"* +- Structure: Goal statement → Assumptions/prerequisites → Numbered steps → Expected result +- Assume baseline knowledge; skip conceptual explanations +- Allow for variation; note alternatives where they exist +- **Validation:** An experienced user can complete the task without confusion or backtracking + +**Example intro:** +> *"This guide shows how to add JWT authentication to an existing Express app. It assumes you have a working Express server and basic familiarity with middleware."* + +--- + +#### Reference (information-oriented) +- **Title pattern:** Name the thing — *"Configuration options"*, *"API endpoints"*, *"CLI flags"* +- Structure: Consistent repeatable format per entry (name → type → default → description → example) +- State facts; avoid instruction beyond minimal usage examples +- Keep current; version-stamp if needed +- **Validation:** A user can look up a specific fact in under 30 seconds without reading surrounding content + +**Example entry:** +> **`timeout`** *(integer, default: `5000`)* +> Maximum time in milliseconds to wait for a response before the request fails. +> *Example:* `{ timeout: 3000 }` + +--- + +#### Explanations (understanding-oriented) +- **Title pattern:** Frame as a concept — *"How X works"*, *"Understanding Y"*, *"Why Z is designed this way"* +- Structure: Context → Core concept → Alternatives/trade-offs → Higher-level perspective +- Avoid step-by-step instruction or technical specification +- **Validation:** After reading, the user can explain the concept in their own words and understands the rationale behind design decisions + +**Example intro:** +> *"Authentication and authorisation are often confused. This page explains the distinction, why both matter, and how common patterns (sessions, tokens, OAuth) approach each concern differently."* + +--- + +### Step 3 — Maintain separation and integration + +- Keep each document a single type — don't mix tutorial steps with reference tables or conceptual digressions +- Cross-link between types: a tutorial can link to the relevant reference page; a how-to guide can link to an explanation for background +- Use consistent headings and terminology across all types so users can navigate the full documentation system + +--- + +### Step 4 — Validate before delivering + +| Type | Validation check | +|---|---| +| Tutorial | Can a beginner complete it end-to-end without external help? | +| How-to guide | Does it solve the stated problem for an experienced user? | +| Reference | Can the user find a specific fact in under 30 seconds? | +| Explanation | Does the user understand the *why*, not just the *what*? | diff --git a/.agents/skills/documentation/tile.json b/.agents/skills/documentation/tile.json new file mode 100644 index 0000000..f389cef --- /dev/null +++ b/.agents/skills/documentation/tile.json @@ -0,0 +1,11 @@ +{ + "name": "mcollina/documentation", + "version": "0.1.0", + "private": false, + "summary": "Creates, structures, and reviews technical documentation following the Diátaxis framework (tutorials, how-to guides, reference, and explanation pages). Use when a user needs to write or reorganize docs, structure a tutorial vs. a how-to guide, build reference docs or API documentation, create explanation pages, choose between Diátaxis documentation types, or improve existing documentation structure. Trigger terms include: documentation structure, Diátaxis, tutorials vs how-to guides, organize docs, user guide, reference docs, technical writing.", + "skills": { + "documentation": { + "path": "SKILL.md" + } + } +} diff --git a/.changeset/docs-readme-localhost.md b/.changeset/docs-readme-localhost.md new file mode 100644 index 0000000..fe37a55 --- /dev/null +++ b/.changeset/docs-readme-localhost.md @@ -0,0 +1,5 @@ +--- +"@vlandoss/env": patch +--- + +Slim down package README and move full documentation to the new Fumadocs site diff --git a/README.md b/README.md index f62035d..c62f115 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ # env +[![npm version](https://img.shields.io/npm/v/@vlandoss/env?label=%40vlandoss%2Fenv&color=blue)](https://www.npmjs.com/package/@vlandoss/env) +[![CI](https://github.com/variableland/env/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/variableland/env/actions/workflows/ci.yml) +[![changesets](https://img.shields.io/badge/maintained%20with-changesets-176de3.svg)](https://github.com/changesets/changesets) + 🌱 Contract-first environment configuration with typed schemas and per-runtime entrypoints. -> Monorepo home for [`@vlandoss/env`](./package) and its examples. +> Monorepo home for [`@vlandoss/env`](./package), its [examples](./examples), and the [documentation site](./docs). ```bash pnpm add @vlandoss/env @@ -32,6 +36,7 @@ This monorepo uses [Turborepo](https://turbo.build/repo/docs): - `pnpm test` — run unit tests - `pnpm test:e2e` — run end-to-end tests (Playwright) - `pnpm test:types` — js & ts checks +- `pnpm docs` — start the documentation site locally It also uses [run-run](https://github.com/variableland/dx/blob/main/packages/run-run/README.md) 🦊: diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..58af77e --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,5 @@ +.source +.tanstack +.wrangler +dist +node_modules diff --git a/docs/content/docs/api-reference/index.mdx b/docs/content/docs/api-reference/index.mdx new file mode 100644 index 0000000..7ba5dba --- /dev/null +++ b/docs/content/docs/api-reference/index.mdx @@ -0,0 +1,24 @@ +--- +title: Overview +description: Every export of @vlandoss/env, organized by the entrypoint you import it 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. + +## Entrypoints + +| Page | Import | Lives in | +| ----------------------------------------------------- | ------------------------------- | ----------------------------------------- | +| [Core](/api-reference/core) | `@vlandoss/env` | Any runtime — schema, defineEnv, envName | +| [Node](/api-reference/node) | `@vlandoss/env/node` | Node — `loadConfig` | +| [Vite](/api-reference/vite) | `@vlandoss/env/vite` | Vite config — `envConfig()` plugin | +| [React](/api-reference/react) | `@vlandoss/env/react` | SSR / SSG — `` | +| [Zod primitives](/api-reference/zod) | `@vlandoss/env/zod` | Any — opinionated single-purpose schemas | +| [Types](/api-reference/types) | `@vlandoss/env` | All exported type aliases | + +## Conventions used in this section + +- Tables list options in declaration order. +- Sync vs. async behavior is marked explicitly on overloaded functions. +- Each export page ends with a "See also" linking to the relevant guide and concept page. diff --git a/docs/content/docs/api-reference/meta.json b/docs/content/docs/api-reference/meta.json new file mode 100644 index 0000000..2e45984 --- /dev/null +++ b/docs/content/docs/api-reference/meta.json @@ -0,0 +1,5 @@ +{ + "title": "API reference", + "icon": "Code", + "pages": ["index"] +} diff --git a/docs/content/docs/concepts/env-name.mdx b/docs/content/docs/concepts/env-name.mdx new file mode 100644 index 0000000..b2dfce2 --- /dev/null +++ b/docs/content/docs/concepts/env-name.mdx @@ -0,0 +1,37 @@ +--- +title: envName() +description: How @vlandoss/env detects the current environment name across Node, browsers, and Vite builds — including the subtlety with custom modes. +icon: Hash +--- + +`envName()` returns the current environment name. It's the value that drives config-file discovery (`config/.ts`), and it's exposed as `env.$name` on the resolved env object. + +## Precedence + +First defined wins: + +1. **`env.ENV`** — explicit runtime override (always wins). Use this in CI, tests, or scripts. +2. **`__ENV_NAME__`** — build-time literal injected by the `envConfig()` Vite plugin. Beats `NODE_ENV` because Vite forces `NODE_ENV="production"` regardless of `--mode`, so this is the only way `envName()` can return custom modes like `staging` / `qa` in the browser. +3. **`env.NODE_ENV`** +4. **`env.VITE_ENV`** +5. **`"development"`** — fallback. + +## Reading from a specific source + +By default, `envName()` reads from `readEnv()` (`process.env` on server, `window.__env` in browser). You can pass an explicit record: + +```ts +import { envName } from "@vlandoss/env"; + +envName({ NODE_ENV: "test", ENV: "qa" }); // -> "qa" +``` + +## The Vite gotcha + +If you run `vite build --mode staging`, Vite **still sets `NODE_ENV="production"`** at build time. Without the `envConfig()` plugin, `envName()` in browser code would return `"production"` — not `"staging"`. + +The plugin solves this by injecting `__ENV_NAME__ = "staging"` as a build-time constant. That constant wins over `NODE_ENV` in the precedence chain, so the browser sees the mode you actually built. + +This is why **the plugin is required for custom Vite modes**, even if you load config via dynamic import (Pattern 1) and don't actually use the `#config` alias. + +See [Guides → SPA dynamic import](/guides/spa-dynamic-import) and [Guides → Custom modes](/guides/custom-modes) for the wiring. diff --git a/docs/content/docs/concepts/env-var-naming.mdx b/docs/content/docs/concepts/env-var-naming.mdx new file mode 100644 index 0000000..c224654 --- /dev/null +++ b/docs/content/docs/concepts/env-var-naming.mdx @@ -0,0 +1,98 @@ +--- +title: Env-var naming +description: How schema paths become environment variable names, and how to override them with the `vars` option. +icon: Tag +--- + +You don't map every leaf to an env-var name. By convention, each leaf binds to a name derived from its dot-path. + +## The convention + +camelCase keys are converted to `SCREAMING_SNAKE_CASE` and joined with `_`: + +| Schema path | Env var | +| ----------------------- | ------------------------ | +| `server.PORT` | `SERVER_PORT` | +| `db.URL` | `DB_URL` | +| `sessionCookie.PREFIX` | `SESSION_COOKIE_PREFIX` | +| `db.kit.LOGGING` | `DB_KIT_LOGGING` | +| `PORT` (top-level leaf) | `PORT` | + +## Per-leaf override + +When a leaf doesn't follow the convention, declare just the exception in `vars`: + +```ts +defineEnv({ + schema: Env, + vars: { + db: { URL: "DATABASE_URL" }, // DB_URL -> DATABASE_URL + }, +}); +``` + +## Branch prefix shorthand + +A string at a branch level becomes the prefix for **every** leaf under it: + +```ts +defineEnv({ + schema: Env, + vars: { + db: "DATABASE", // db.URL -> DATABASE_URL, db.LOGGING -> DATABASE_LOGGING + }, +}); +``` + +## Flat branches with `null` + +`null` declares a flat branch — leaves under it use their own name with no prefix. Useful when composing schemas where the inner contract already uses fully-qualified names: + +```ts +defineEnv({ + schema: Env, + vars: { + server: null, // server.PORT -> PORT, server.HOST -> HOST + }, +}); +``` + +The empty string `""` is **not** accepted — `null` is the only way to declare a flat branch. This avoids the readability confusion of `""` (which looks like a missing value). + +## Mixed mode: branch prefix + per-leaf override + +Use `$` to set the branch prefix and override individual leaves at the same time: + +```ts +defineEnv({ + schema: Env, + vars: { + db: { + $: "DATABASE", // branch prefix + LOGGING: "POSTGRES_LOGGING", // override for this one leaf + }, + public: { + $: null, // flat branch + leaf override + APP_NAME: "PUBLIC_APP_NAME", + }, + }, +}); +``` + +## Type safety + +`vars` is fully type-checked against the schema. Unknown branches, unknown leaves, non-uppercase literals, or non-string values fail at compile time: + +```ts +defineEnv({ + schema: Env, + vars: { + // @ts-expect-error 'cache' is not in the schema + cache: { TTL: "CACHE_TTL" }, + server: { + // @ts-expect-error must be uppercase + PORT: "port", + }, + }, +}); +``` diff --git a/docs/content/docs/concepts/meta.json b/docs/content/docs/concepts/meta.json new file mode 100644 index 0000000..45b8ab0 --- /dev/null +++ b/docs/content/docs/concepts/meta.json @@ -0,0 +1,5 @@ +{ + "title": "Concepts", + "icon": "Lightbulb", + "pages": ["overview", "resolution", "env-var-naming", "env-name"] +} diff --git a/docs/content/docs/concepts/overview.mdx b/docs/content/docs/concepts/overview.mdx new file mode 100644 index 0000000..1ec8a3b --- /dev/null +++ b/docs/content/docs/concepts/overview.mdx @@ -0,0 +1,66 @@ +--- +title: Overview +description: The mental model behind @vlandoss/env — one contract, three runtime adapters, and a single resolved env object. +icon: Map +--- + +`@vlandoss/env` is built around four ideas. Once they click, the rest of the API is small. + +## 1. One contract per app + +A **schema** declares every variable your app expects, grouped into branches. It's the only source of truth — types, validation, and env-var names all derive from it. + +```ts +import { schema } from "@vlandoss/env"; + +const Env = schema({ + server: { PORT: z.coerce.number(), HOST: z.string() }, + db: { URL: z.string() }, +}); +``` + +Leaves are Standard Schema validators (Zod, Valibot, ArkType…). Branches are nested objects. Other `schema()` results can be inlined to share contracts across files. + +## 2. Per-environment config files, not `.env` per environment + +Each environment has a file (`development.ts`, `production.ts`, …) that returns a typed object matching the schema. These are **versioned defaults**, not secrets. Secrets still come from `process.env`. + +```ts title="src/config/production.ts" +export default { + server: { PORT: 8080, HOST: "0.0.0.0" }, +} satisfies EnvConfig; +``` + +## 3. Env vars override config, with predictable naming + +At boot, `defineEnv` merges three layers — see [Resolution order](/concepts/resolution). Each leaf maps to an env-var name by convention (`server.PORT` → `SERVER_PORT`); when the convention doesn't fit, override it in `vars` — see [Env-var naming](/concepts/env-var-naming). + +## 4. Runtime-agnostic core, opt-in adapters + +The core (`@vlandoss/env`) doesn't touch the file system or the bundler. Adapters do: + +| Entrypoint | Runtime | Responsibility | +| ----------------------- | ----------------- | ------------------------------------------------------------- | +| `@vlandoss/env` | Any | Schema, resolution, validation, types | +| `@vlandoss/env/node` | Node | Load config files from disk (`loadConfig`) | +| `@vlandoss/env/vite` | Build time | Alias `#config`, inject `__ENV_NAME__` for the browser | +| `@vlandoss/env/react` | SSR / SSG | Serialize public env into `