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
109 changes: 109 additions & 0 deletions .agents/skills/documentation/SKILL.md
Original file line number Diff line number Diff line change
@@ -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*? |
11 changes: 11 additions & 0 deletions .agents/skills/documentation/tile.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
5 changes: 5 additions & 0 deletions .changeset/docs-readme-localhost.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@vlandoss/env": patch
---

Slim down package README and move full documentation to the new Fumadocs site
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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) 🦊:

Expand Down
5 changes: 5 additions & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.source
.tanstack
.wrangler
dist
node_modules
24 changes: 24 additions & 0 deletions docs/content/docs/api-reference/index.mdx
Original file line number Diff line number Diff line change
@@ -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 — `<ClientEnv />` |
| [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.
5 changes: 5 additions & 0 deletions docs/content/docs/api-reference/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"title": "API reference",
"icon": "Code",
"pages": ["index"]
}
37 changes: 37 additions & 0 deletions docs/content/docs/concepts/env-name.mdx
Original file line number Diff line number Diff line change
@@ -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/<envName>.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.
98 changes: 98 additions & 0 deletions docs/content/docs/concepts/env-var-naming.mdx
Original file line number Diff line number Diff line change
@@ -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",
},
},
});
```
5 changes: 5 additions & 0 deletions docs/content/docs/concepts/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"title": "Concepts",
"icon": "Lightbulb",
"pages": ["overview", "resolution", "env-var-naming", "env-name"]
}
66 changes: 66 additions & 0 deletions docs/content/docs/concepts/overview.mdx
Original file line number Diff line number Diff line change
@@ -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 `<script id="env">` for hydration |
| `@vlandoss/env/zod` | Any | Opinionated Zod primitives (`port`, `host`, `bool`, `secret`) |

You import the adapter that matches where the code runs. Pure SPA? No `/node`. No SSR? No `/react`. The core stays small.

## How the pieces fit

```text
schema() ─┐
config/* ─┼─► defineEnv() ─► env (typed, validated)
process.env or window.__env ─┘
```

Read on:

- [Resolution order](/concepts/resolution) — precedence of the three sources.
- [Env-var naming](/concepts/env-var-naming) — convention and overrides.
- [`envName()`](/concepts/env-name) — how the current environment is detected.
Loading
Loading