|
| 1 | +### Formatter settings (from `biome.jsonc`) |
| 2 | + |
| 3 | +- **Indent style:** Tabs (not spaces) for code files. |
| 4 | +- **Indent width:** 2. |
| 5 | +- **Line width:** 100 characters. |
| 6 | +- **Trailing commas:** `all` — always include trailing commas. |
| 7 | +- **Quote style:** Single quotes (`'`). |
| 8 | +- **Semicolons:** Always. |
| 9 | +- **Expand:** `auto` — Biome auto-decides when to expand arrays/objects. |
| 10 | + |
| 11 | +--- |
| 12 | + |
| 13 | +## 2. Import Conventions |
| 14 | + |
| 15 | +### Always use the `node:` protocol for Node.js builtins |
| 16 | + |
| 17 | +Enforced as an **error** via Biome's `useNodejsImportProtocol` rule. Every import of a Node built-in must use the `node:` prefix: |
| 18 | + |
| 19 | +```ts |
| 20 | +// ✅ Correct |
| 21 | +import fs from "node:fs"; |
| 22 | +import path from "node:path"; |
| 23 | + |
| 24 | +// ❌ Wrong — will fail lint |
| 25 | +import fs from "fs"; |
| 26 | +import path from "path"; |
| 27 | +``` |
| 28 | + |
| 29 | +### Enforce `import type` for type-only imports |
| 30 | + |
| 31 | +Enforced as an **error** via Biome's `useImportType` rule. If an import is only used as a type (not at runtime), it must use `import type`: |
| 32 | + |
| 33 | +```ts |
| 34 | +// ✅ Correct |
| 35 | +import type { AstroConfig } from "../types/public/config.js"; |
| 36 | +import { getFileInfo } from "../vite-plugin-utils/index.js"; |
| 37 | + |
| 38 | +// ❌ Wrong — pulls runtime code for a type-only import |
| 39 | +import { AstroConfig } from "../types/public/config.js"; |
| 40 | +``` |
| 41 | + |
| 42 | +### Import organization |
| 43 | + |
| 44 | +Biome's `organizeImports` assist is set to `"on"`, which auto-sorts and groups imports. |
| 45 | + |
| 46 | +--- |
| 47 | + |
| 48 | +## 3. Restricted Use of Node.js APIs |
| 49 | + |
| 50 | +This is a foundational architectural constraint. Astro code may run in non-Node environments like **Bun** or **Cloudflare Workers**, so Node.js API usage is strictly limited to specific areas of the codebase. |
| 51 | + |
| 52 | +### Rules |
| 53 | + |
| 54 | +- **Runtime-agnostic code** (code that shouldn't use Node.js APIs) should be placed inside folders or files called `runtime` (`runtime/` or `runtime.ts`). |
| 55 | +- **Vite plugin implementations** may use Node.js APIs, but if a Vite plugin returns a **virtual module**, that virtual module cannot use Node.js APIs. |
| 56 | +- Prefer **`URL` and file URL APIs** (`new URL(...)`, `import.meta.url`) over `node:path` string manipulation where possible. The codebase favors URL-based path handling for cross-runtime compatibility. |
| 57 | + |
| 58 | +--- |
| 59 | + |
| 60 | +## 4. Function Signature Conventions |
| 61 | + |
| 62 | +### Options bag pattern |
| 63 | + |
| 64 | +Functions with more than two parameters are collapsed into an **options object**. This is a pervasive pattern throughout the codebase: |
| 65 | + |
| 66 | +```ts |
| 67 | +// ✅ Correct — options bag with a typed interface |
| 68 | +interface CompileAstroOption { |
| 69 | + compileProps: CompileProps; |
| 70 | + astroFileToCompileMetadata: Map<string, CompileMetadata>; |
| 71 | + logger: Logger; |
| 72 | +} |
| 73 | + |
| 74 | +export async function compileAstro({ |
| 75 | + compileProps, |
| 76 | + astroFileToCompileMetadata, |
| 77 | + logger, |
| 78 | +}: CompileAstroOption): Promise<CompileAstroResult> { ... } |
| 79 | +``` |
| 80 | + |
| 81 | +```ts |
| 82 | +// ✅ Also correct — inline destructuring for simpler cases |
| 83 | +export async function generateHydrateScript( |
| 84 | + scriptOptions: HydrateScriptOptions, |
| 85 | + metadata: Required<AstroComponentMetadata>, |
| 86 | +): Promise<SSRElement> { ... } |
| 87 | +``` |
| 88 | + |
| 89 | +### Public APIs should default to `async` |
| 90 | + |
| 91 | +Public-facing functions default to being `async`, even if they don't strictly need to be today. This future-proofs the API against needing to introduce async operations later without a breaking change. |
| 92 | + |
| 93 | +--- |
| 94 | + |
| 95 | +## 5. Error Handling Convention |
| 96 | + |
| 97 | +Astro has a **unified, structured error system**. All errors go through a central `AstroError` class and are defined in a single data file. |
| 98 | + |
| 99 | +### Never throw generic `Error` — use `AstroError` |
| 100 | + |
| 101 | +```ts |
| 102 | +// ❌ Wrong |
| 103 | +throw new Error("Something went wrong"); |
| 104 | + |
| 105 | +// ✅ Correct |
| 106 | +throw new AstroError({ |
| 107 | + ...AstroErrorData.NoMatchingImport, |
| 108 | + message: AstroErrorData.NoMatchingImport.message(metadata.displayName), |
| 109 | +}); |
| 110 | +``` |
| 111 | + |
| 112 | +### Error definitions live in `errors-data.ts` |
| 113 | + |
| 114 | +All error data is centralized in `packages/astro/src/core/errors/errors-data.ts`. Each error follows a strict shape: |
| 115 | + |
| 116 | +- **`name`** — A permanent, unique PascalCase identifier (never changed once published). |
| 117 | +- **`title`** — A brief user-facing summary. |
| 118 | +- **`message`** — Can be a static string or a function for dynamic context. Describes what happened and what action to take. |
| 119 | +- **`hint`** — Optional. Additional context, links to docs, or common causes. |
| 120 | + |
| 121 | +### Error writing guidelines (from `errors/README.md`) |
| 122 | + |
| 123 | +- Write from the **user's perspective**, not Astro internals ("You are missing..." not "Astro cannot find..."). |
| 124 | +- **Avoid cutesy language** — no "Oops!" The developer may be frustrated. |
| 125 | +- Keep messages **skimmable** — short, clear sentences. |
| 126 | +- Don't prefix with "Error:" or "Hint:" — the UI already adds labels. |
| 127 | +- **Names are permanent** — never rename, so users can always search for them. |
| 128 | +- Dynamic messages use a function shape: `message: (param) => \`...\``. |
| 129 | +- Avoid complex logic inside error definitions. |
| 130 | +- Errors are **grouped by domain** (Content Collections, Images, Routing, etc.) and display on the error reference page in definition order. |
| 131 | + |
| 132 | +--- |
| 133 | + |
| 134 | +## 6. Code Architecture Principles |
| 135 | + |
| 136 | +### Decouple business logic from infrastructure |
| 137 | + |
| 138 | +The CONTRIBUTING guide explicitly calls this out as a core principle: |
| 139 | + |
| 140 | +- **Infrastructure:** Code that depends on external systems (DB calls, file system, randomness, etc.) or requires a special environment to run. |
| 141 | +- **Business logic (domain/core):** Pure logic that's easy to run from anywhere. |
| 142 | + |
| 143 | +In practice this means: avoid side effects, make external dependencies explicit, and **pass more things as arguments** (dependency injection style). |
| 144 | + |
| 145 | +### Test all implementations |
| 146 | + |
| 147 | +If an infrastructure implementation is just a thin wrapper around an npm package, you may skip testing it and trust the package's own tests. But all business logic should be tested. |
| 148 | + |
| 149 | +--- |
| 150 | + |
| 151 | +## 8. Code Cleanliness Rules |
| 152 | + |
| 153 | +All enforced at the **error** level: |
| 154 | + |
| 155 | +| Rule | Effect | |
| 156 | +| ---------------------------- | ----------------------------------------------------- | |
| 157 | +| `noUnusedVariables` | No unused variables (with `ignoreRestSiblings: true`) | |
| 158 | +| `noUnusedFunctionParameters` | No unused function parameters | |
| 159 | +| `noUnusedImports` | No unused imports | |
| 160 | +| `useNodejsImportProtocol` | Must use `node:` prefix for Node builtins | |
| 161 | +| `useImportType` | Must use `import type` for type-only imports | |
| 162 | + |
| 163 | +- Node.js engine requirement: `>=22.12.0`. |
| 164 | +- Package manager: `pnpm` (enforced via `only-allow`). |
| 165 | + |
| 166 | +--- |
| 167 | + |
| 168 | +## 12. Code Style |
| 169 | + |
| 170 | +The observed patterns from the source code include: |
| 171 | + |
| 172 | +- **Explicit return types** on exported functions. |
| 173 | +- **Interface-first design** — define an interface for options bags, then destructure in the function signature. |
| 174 | +- **JSDoc comments** on exported functions and complex internal functions, using `/** */` style. Avoid excessive or self-documenting comments. |
| 175 | +- **Explicit `.ts` extensions in import paths** — even for TypeScript files, imports use literal extensions. |
| 176 | +- **Avoid barrel files via `index.ts`** — packages organize re-exports through index files. |
| 177 | +- **`const` by default** — `let` only when reassignment is needed. |
| 178 | + |
| 179 | +--- |
| 180 | + |
| 181 | +## Summary of Key Rules |
| 182 | + |
| 183 | +| Convention | Details | |
| 184 | +| --------------- | ------------------------------------------------------------------------------------------ | |
| 185 | +| Formatter | Biome: tabs, single quotes, semicolons always, trailing commas always, 100-char line width | |
| 186 | +| Node builtins | Always `node:` prefix (error-level lint rule) | |
| 187 | +| Type imports | Always `import type` for type-only (error-level lint rule) | |
| 188 | +| Node.js APIs | Restricted to non-`runtime/` code; virtual modules must be runtime-agnostic | |
| 189 | +| Function params | >2 params → options bag with a typed interface | |
| 190 | +| Public APIs | Default to `async` | |
| 191 | +| Error handling | Always `AstroError` with centralized error data; never generic `Error` | |
| 192 | +| `console.log` | Warned; use `console.info`/`warn`/`error`/`debug` instead | |
| 193 | +| Unused code | Unused vars, params, and imports are errors | |
| 194 | +| Path handling | Prefer `URL` / file URLs over `node:path` string manipulation | |
| 195 | +| Testing | Vitest via `astro-scripts`; `bgproc` for dev servers; `agent-browser` for HMR | |
| 196 | +| Scripting | `node -e`, not Python | |
| 197 | +| Business logic | Decouple from infrastructure; make dependencies explicit via arguments | |
0 commit comments