@dirstack/kodeks 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Piotr Kulpinski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # Kodeks
2
+
3
+ [![npm version](https://img.shields.io/npm/v/%40dirstack%2Fkodeks.svg)](https://www.npmjs.com/package/@dirstack/kodeks)
4
+ [![license](https://img.shields.io/npm/l/%40dirstack%2Fkodeks.svg)](https://github.com/dirstack/kodeks/blob/main/LICENSE)
5
+
6
+ Shared linter configurations for TypeScript projects.
7
+
8
+ Kodeks provides reusable, opinionated configurations for maintaining consistent code quality across TypeScript projects.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ bun add -d @dirstack/kodeks
14
+ ```
15
+
16
+ This will automatically install all required dependencies.
17
+
18
+ > [!NOTE]
19
+ > All tools are installed together for simplicity, even if you only use some of the configurations. This ensures all CLIs and configs work correctly and avoids resolution issues.
20
+
21
+ ## Usage
22
+
23
+ ### oxlint Configuration
24
+
25
+ Create a `.oxlintrc.json` file in your project root:
26
+
27
+ ```json
28
+ {
29
+ "extends": ["./node_modules/@dirstack/kodeks/configs/oxlint-base.json"]
30
+ }
31
+ ```
32
+
33
+ For Next.js projects, use the Next.js preset:
34
+
35
+ ```json
36
+ {
37
+ "extends": ["./node_modules/@dirstack/kodeks/configs/oxlint-next.json"]
38
+ }
39
+ ```
40
+
41
+ For TanStack projects, use the TanStack preset:
42
+
43
+ ```json
44
+ {
45
+ "extends": ["./node_modules/@dirstack/kodeks/configs/oxlint-tanstack.json"]
46
+ }
47
+ ```
48
+
49
+ ### oxfmt Configuration
50
+
51
+ Since oxfmt does not support configuration inheritance, you have two options:
52
+
53
+ **Option 1: Reference via CLI flag**
54
+
55
+ Add to your `package.json` scripts:
56
+
57
+ ```json
58
+ {
59
+ "scripts": {
60
+ "format": "oxfmt --config ./node_modules/@dirstack/kodeks/configs/oxfmt.json --write ."
61
+ }
62
+ }
63
+ ```
64
+
65
+ **Option 2: Copy configuration locally**
66
+
67
+ Copy the configuration file to your project root and reference it directly:
68
+
69
+ ```bash
70
+ cp ./node_modules/@dirstack/kodeks/configs/oxfmt.json .
71
+ ```
72
+
73
+ Then use:
74
+
75
+ ```bash
76
+ oxfmt --config ./oxfmt.json --write .
77
+ ```
78
+
79
+ ### Commitlint Configuration
80
+
81
+ Create a `commitlint.json` file in your project root:
82
+
83
+ ```json
84
+ {
85
+ "extends": ["./node_modules/@dirstack/kodeks/configs/commitlint.json"]
86
+ }
87
+ ```
88
+
89
+ ### Lefthook Configuration
90
+
91
+ Create a `lefthook.json` file in your project root and extend the hooks you need:
92
+
93
+ ```json
94
+ {
95
+ "extends": [
96
+ "node_modules/@dirstack/kodeks/configs/lefthook-oxlint.json",
97
+ "node_modules/@dirstack/kodeks/configs/lefthook-oxfmt.json",
98
+ "node_modules/@dirstack/kodeks/configs/lefthook-commitlint.json"
99
+ ]
100
+ }
101
+ ```
102
+
103
+ **Available hooks:**
104
+ - `lefthook-oxlint.json` - Lints and fixes staged files with oxlint (pre-commit)
105
+ - `lefthook-oxfmt.json` - Formats staged files with oxfmt (pre-commit)
106
+ - `lefthook-commitlint.json` - Validates commit messages (commit-msg)
107
+
108
+ ## Agentic Coding
109
+
110
+ ### Skills
111
+
112
+ This package includes agent skills for code formatting, testing, and commit conventions.
113
+
114
+ **Available skills:**
115
+ - `formatting` - Code formatting rules not handled by auto-formatters (arrow functions, exports, types, naming, comments, JSX)
116
+ - `testing` - Best practices, structure, coverage categories, and formatting conventions for writing tests
117
+ - `commits` - Conventional commit types, scopes, and message formatting guidelines
118
+
119
+ Install all skills using [skills.sh](https://skills.sh):
120
+
121
+ ```bash
122
+ npx skills add dirstack/kodeks
123
+ ```
124
+
125
+ Or install individual skills:
126
+
127
+ ```bash
128
+ npx skills add dirstack/kodeks --skill formatting
129
+ npx skills add dirstack/kodeks --skill testing
130
+ npx skills add dirstack/kodeks --skill commits
131
+ ```
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/commitlintrc.json",
3
+ "extends": ["@commitlint/config-conventional"],
4
+ "rules": {
5
+ "subject-case": [2, "always", "lower-case"]
6
+ }
7
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/lefthook.json",
3
+ "commit-msg": {
4
+ "commands": {
5
+ "lint:commitlint": {
6
+ "run": "bunx commitlint --config ./commitlint.json --edit {1}"
7
+ }
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/lefthook.json",
3
+ "pre-commit": {
4
+ "parallel": true,
5
+ "commands": {
6
+ "format:oxfmt": {
7
+ "glob": "*.{ts,tsx,js,jsx,json,jsonc,css}",
8
+ "run": "bunx oxfmt --write {staged_files}",
9
+ "stage_fixed": true
10
+ }
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/lefthook.json",
3
+ "pre-commit": {
4
+ "parallel": true,
5
+ "commands": {
6
+ "lint:oxlint": {
7
+ "glob": "*.{ts,tsx,js,jsx}",
8
+ "run": "bunx oxlint --fix --no-errors-on-unmatched --files-ignore-unknown=true {staged_files}",
9
+ "stage_fixed": true
10
+ }
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/lefthook.json",
3
+ "pre-commit": {
4
+ "commands": {
5
+ "typecheck": {
6
+ "glob": "*.{ts,tsx}",
7
+ "run": "bunx tsc --noEmit"
8
+ }
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/refs/heads/main/npm/oxfmt/configuration_schema.json",
3
+ "printWidth": 100,
4
+ "tabWidth": 2,
5
+ "useTabs": false,
6
+ "semi": false,
7
+ "singleQuote": false,
8
+ "trailingComma": "all",
9
+ "bracketSpacing": true,
10
+ "arrowParens": "avoid",
11
+ "experimentalSortImports": {
12
+ "enabled": true,
13
+ "groups": [
14
+ ["side_effect"],
15
+ ["value-builtin", "type-builtin"],
16
+ ["value-external", "type-external"],
17
+ ["value-internal", "type-internal"]
18
+ ],
19
+ "newlinesBetween": false
20
+ }
21
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/refs/heads/main/npm/oxlint/configuration_schema.json",
3
+ "plugins": ["react", "typescript", "jsx-a11y", "oxc"],
4
+ "rules": {
5
+ "react-hooks/exhaustive-deps": "off",
6
+ "jsx-a11y/anchor-has-content": "off",
7
+ "jsx-a11y/heading-has-content": "off"
8
+ }
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/refs/heads/main/npm/oxlint/configuration_schema.json",
3
+ "extends": ["./oxlint-base.json"],
4
+ "plugins": ["nextjs"],
5
+ "rules": {
6
+ "nextjs/no-img-element": "off",
7
+ "nextjs/no-html-link-for-pages": "off"
8
+ }
9
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/refs/heads/main/npm/oxlint/configuration_schema.json",
3
+ "extends": ["./oxlint-base.json"]
4
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/semantic-release.json",
3
+ "branches": [
4
+ "main",
5
+ { "name": "beta", "channel": "beta", "prerelease": "beta" },
6
+ { "name": "alpha", "channel": "alpha", "prerelease": "alpha" },
7
+ { "name": "next", "channel": "next", "prerelease": "next" }
8
+ ],
9
+ "plugins": [
10
+ [
11
+ "@semantic-release/commit-analyzer",
12
+ {
13
+ "preset": "conventionalcommits",
14
+ "releaseRules": [{ "breaking": true, "release": "major" }]
15
+ }
16
+ ]
17
+ ]
18
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/semantic-release.json",
3
+ "branches": [
4
+ "main",
5
+ { "name": "beta", "channel": "beta", "prerelease": "beta" },
6
+ { "name": "alpha", "channel": "alpha", "prerelease": "alpha" },
7
+ { "name": "next", "channel": "next", "prerelease": "next" }
8
+ ],
9
+ "plugins": [
10
+ [
11
+ "@semantic-release/commit-analyzer",
12
+ {
13
+ "preset": "conventionalcommits",
14
+ "releaseRules": [{ "breaking": true, "release": "major" }]
15
+ }
16
+ ],
17
+ ["@semantic-release/release-notes-generator", { "preset": "conventionalcommits" }],
18
+ ["@semantic-release/npm", { "provenance": true }],
19
+ "@semantic-release/github"
20
+ ]
21
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@dirstack/kodeks",
3
+ "description": "Shared linter configurations for TypeScript projects.",
4
+ "homepage": "https://github.com/dirstack/kodeks#readme",
5
+ "bugs": {
6
+ "url": "https://github.com/dirstack/kodeks/issues"
7
+ },
8
+ "license": "MIT",
9
+ "author": "Piotr Kulpinski",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/dirstack/kodeks"
13
+ },
14
+ "files": [
15
+ "configs",
16
+ "skills"
17
+ ],
18
+ "type": "module",
19
+ "exports": {
20
+ "./oxlint": "./configs/oxlint-base.json",
21
+ "./oxlint-next": "./configs/oxlint-next.json",
22
+ "./oxlint-tanstack": "./configs/oxlint-tanstack.json",
23
+ "./commitlint": "./configs/commitlint.json",
24
+ "./semantic-release": "./configs/semantic-release.json",
25
+ "./semantic-release-dry-run": "./configs/semantic-release-dry-run.json"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "scripts": {
31
+ "prepare": "lefthook install"
32
+ },
33
+ "devDependencies": {
34
+ "@commitlint/cli": "^20.4.2",
35
+ "@commitlint/config-conventional": "^20.4.2",
36
+ "conventional-changelog-conventionalcommits": "^9.1.0",
37
+ "lefthook": "^2.1.1",
38
+ "oxfmt": "^0.35.0",
39
+ "oxlint": "^1.50.0",
40
+ "semantic-release": "^25.0.3",
41
+ "typescript": "^5.9.3"
42
+ },
43
+ "peerDependencies": {
44
+ "@commitlint/cli": ">=20.4.2",
45
+ "@commitlint/config-conventional": ">=20.4.2",
46
+ "conventional-changelog-conventionalcommits": ">=9.1.0",
47
+ "lefthook": ">=2.1.1",
48
+ "oxfmt": ">=0.35.0",
49
+ "oxlint": ">=1.50.0",
50
+ "semantic-release": ">=25.0.3",
51
+ "typescript": ">=5.9.3"
52
+ },
53
+ "engines": {
54
+ "bun": ">=1.0.0"
55
+ },
56
+ "packageManager": "bun@1.3.9",
57
+ "version": "1.0.0"
58
+ }
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: commits
3
+ description: Conventional commit message guidelines. Use when writing commit messages, reviewing commits, or when asked to commit changes. Covers commit types, subject rules (lowercase, imperative), atomic commits, and examples of good vs bad messages.
4
+ ---
5
+
6
+ # Commit Convention
7
+
8
+ Guidelines for writing consistent and clear commit messages.
9
+
10
+ ## Format
11
+
12
+ ```
13
+ <type>: <description>
14
+ ```
15
+
16
+ - No scope required.
17
+ - No body or footer for simple changes.
18
+ - Max 72 characters for the subject line.
19
+
20
+ ## Types
21
+
22
+ - `feat` — new feature or capability
23
+ - `fix` — bug fix
24
+ - `docs` — documentation only changes
25
+ - `style` — code style/formatting (no logic changes)
26
+ - `refactor` — code restructuring (no feature, no fix)
27
+ - `perf` — performance improvements
28
+ - `test` — adding or updating tests
29
+ - `build` — build system or dependency changes
30
+ - `ci` — CI/CD configuration changes
31
+ - `chore` — maintenance tasks, configs
32
+ - `revert` — reverting a previous commit
33
+
34
+ ## Subject Rules
35
+
36
+ - **lowercase**: description starts with a lowercase letter.
37
+ - **imperative mood**: use "add", not "added" or "adds".
38
+ - **no period**: do not end the subject with a period.
39
+ - **max 72 chars**: keep it concise.
40
+
41
+ ## Atomic Commits
42
+
43
+ One logical change per commit. If the description requires "and", consider splitting the changes into separate commits.
44
+
45
+ ## Breaking Changes
46
+
47
+ Use the `!` suffix after the type for breaking changes:
48
+
49
+ ```
50
+ feat!: remove deprecated API endpoint
51
+ ```
52
+
53
+ ## Good vs Bad Examples
54
+
55
+ ### Good
56
+ - `feat: add user authentication flow`
57
+ - `fix: resolve login redirect loop`
58
+ - `refactor: extract validation logic into separate module`
59
+ - `docs: update API documentation for v2 endpoints`
60
+ - `chore: update dependencies to latest versions`
61
+
62
+ ### Bad
63
+ - `feat: Added user authentication` (past tense)
64
+ - `fix: Bug fix` (vague)
65
+ - `feat: add user auth and update profile page` (multiple logical changes)
66
+ - `FEAT: Add user auth` (uppercase type)
67
+ - `feat: Add user auth` (uppercase description)
@@ -0,0 +1,327 @@
1
+ ---
2
+ name: formatting
3
+ description: Code formatting and style rules not handled by auto-formatters (oxfmt). Use when writing or modifying TypeScript/React code. Covers function declarations, exports, types, comments, JSX, naming conventions, and import patterns.
4
+ ---
5
+
6
+ # Code Formatting Rules
7
+
8
+ Style guide for code formatting decisions not handled by auto-formatters (oxfmt).
9
+
10
+ ## 1. Function Declaration Style
11
+
12
+ Always use arrow functions with explicit types. Avoid the `function` keyword for components and utilities.
13
+
14
+ ```typescript
15
+ // Correct
16
+ export const processData = (input: string): string => {
17
+ return `Processed: ${input}`
18
+ }
19
+
20
+ // Avoid
21
+ export function processData(input: string): string {
22
+ return "Processed: " + input
23
+ }
24
+ ```
25
+
26
+ ---
27
+
28
+ ## 2. Export Patterns
29
+
30
+ Use named exports only. Never use default exports.
31
+
32
+ ```typescript
33
+ // Correct
34
+ export const Button = () => {
35
+ return <button>Click me</button>
36
+ }
37
+
38
+ // Avoid
39
+ const Button = () => {
40
+ return <button>Click me</button>
41
+ }
42
+ export default Button
43
+ ```
44
+
45
+ ---
46
+
47
+ ## 3. Early Returns
48
+
49
+ Use early returns for validation and edge cases to keep the main logic flat.
50
+
51
+ ```typescript
52
+ export const processUser = (user: User | null) => {
53
+ if (!user) {
54
+ return
55
+ }
56
+
57
+ if (!user.isActive) {
58
+ return
59
+ }
60
+
61
+ return doSomething(user)
62
+ }
63
+ ```
64
+
65
+ ---
66
+
67
+ ## 4. Type Definitions
68
+
69
+ Prefer `type` over `interface`. Use namespaces for related types and generics for reusable structures.
70
+
71
+ ```typescript
72
+ // Correct
73
+ export type User = {
74
+ id: string
75
+ name: string
76
+ }
77
+
78
+ // Avoid
79
+ export interface User {
80
+ id: string
81
+ name: string
82
+ }
83
+
84
+ // Namespaces for related types
85
+ export namespace UserApi {
86
+ export type Request = {
87
+ userId: string
88
+ }
89
+ export type Response = {
90
+ user: User
91
+ }
92
+ }
93
+ ```
94
+
95
+ ---
96
+
97
+ ## 5. File Structure
98
+
99
+ Follow a standard file order to maintain consistency across the codebase:
100
+ 1. Imports (grouped: builtin, external, internal)
101
+ 2. Type definitions
102
+ 3. Constants and Helpers
103
+ 4. Main exports (Components/Functions)
104
+
105
+ ---
106
+
107
+ ## 6. Curly Braces
108
+
109
+ Always use curly braces for arrow functions, even for single-line returns.
110
+
111
+ ```typescript
112
+ // Correct
113
+ export const getName = (user: User) => {
114
+ return user.name
115
+ }
116
+
117
+ // Avoid
118
+ export const getName = (user: User) => user.name
119
+ ```
120
+
121
+ ---
122
+
123
+ ## 7. Variable Naming
124
+
125
+ Use full, descriptive words. Avoid abbreviations like `err`, `req`, `res`, or `btn`.
126
+
127
+ ```typescript
128
+ // Correct
129
+ const error = new Error("Failed")
130
+ const request = await fetch(url)
131
+ const button = document.querySelector("button")
132
+
133
+ // Avoid
134
+ const err = new Error("Failed")
135
+ const req = await fetch(url)
136
+ const btn = document.querySelector("button")
137
+ ```
138
+
139
+ **Function name prefixes:**
140
+ - `parse*` (e.g., `parseInput`)
141
+ - `generate*` (e.g., `generateId`)
142
+ - `get*` / `fetch*` (e.g., `getUser`)
143
+ - `is*` / `has*` (e.g., `isValid`, `hasPermission`)
144
+ - `validate*` (e.g., `validateEmail`)
145
+
146
+ ---
147
+
148
+ ## 8. Comments
149
+
150
+ Write comments as full sentences with proper capitalization and punctuation. Place them above the subject.
151
+
152
+ ```typescript
153
+ // Correct
154
+ // This calculates the final price including tax and discounts.
155
+ const finalPrice = calculatePrice(items)
156
+
157
+ // Avoid
158
+ const finalPrice = calculatePrice(items) // calculate price
159
+ ```
160
+
161
+ **Inline comments** are allowed for arrays, object properties, and complex types:
162
+ ```typescript
163
+ export type Config = {
164
+ timeout: number // Timeout in milliseconds.
165
+ retries: number // Number of retry attempts.
166
+ }
167
+ ```
168
+
169
+ ---
170
+
171
+ ## 9. Array Type Syntax
172
+
173
+ Use the generic `Array<T>` syntax instead of the `T[]` shorthand.
174
+
175
+ ```typescript
176
+ // Correct
177
+ const tags: Array<string> = ["react", "typescript"]
178
+
179
+ // Avoid
180
+ const tags: string[] = ["react", "typescript"]
181
+ ```
182
+
183
+ ---
184
+
185
+ ## 10. Nullish Coalescing
186
+
187
+ Use the nullish coalescing operator `??` instead of logical OR `||` for default values.
188
+
189
+ ```typescript
190
+ // Correct
191
+ const port = process.env.PORT ?? 3000
192
+ const username = user.name ?? "Anonymous"
193
+
194
+ // Avoid
195
+ const port = process.env.PORT || 3000
196
+ ```
197
+
198
+ ---
199
+
200
+ ## 11. Template Literals
201
+
202
+ Always use template literals for string composition. Never use `+` for string concatenation.
203
+
204
+ ```typescript
205
+ // Correct
206
+ const message = `Hello, ${user.name}!`
207
+
208
+ // Avoid
209
+ const message = "Hello, " + user.name + "!"
210
+ ```
211
+
212
+ ---
213
+
214
+ ## 12. Bare Return
215
+
216
+ When a function returns `| undefined`, use a bare `return` instead of `return undefined`.
217
+
218
+ ```typescript
219
+ // Correct
220
+ export const findItem = (items: Array<Item>, id: string): Item | undefined => {
221
+ const item = items.find((i) => { return i.id === id })
222
+ if (!item) {
223
+ return
224
+ }
225
+ return item
226
+ }
227
+
228
+ // Avoid
229
+ if (!item) {
230
+ return undefined
231
+ }
232
+ ```
233
+
234
+ ---
235
+
236
+ ## 13. Void Prefix
237
+
238
+ Use the `void` operator for intentionally non-awaited async calls (fire-and-forget).
239
+
240
+ ```typescript
241
+ // Correct
242
+ void trackEvent("page_view")
243
+
244
+ // Avoid
245
+ trackEvent("page_view")
246
+ ```
247
+
248
+ ---
249
+
250
+ ## 14. Empty Catch
251
+
252
+ Use an empty `catch {}` block for expected or ignorable failures, and return a safe default.
253
+
254
+ ```typescript
255
+ export const safeParseJson = (json: string): unknown => {
256
+ try {
257
+ return JSON.parse(json)
258
+ } catch {}
259
+ }
260
+ ```
261
+
262
+ ---
263
+
264
+ ## 15. Config via Constants
265
+
266
+ Isolate all `process.env` access in dedicated constants or environment modules. Business logic should never touch `process.env`.
267
+
268
+ ```typescript
269
+ // Correct (in ~/lib/env.ts)
270
+ export const DATABASE_URL = process.env.DATABASE_URL
271
+
272
+ // Avoid (in business logic)
273
+ const client = new Client(process.env.DATABASE_URL)
274
+ ```
275
+
276
+ ---
277
+
278
+ ## 16. JSX Attributes
279
+
280
+ Order attributes logically: `id` first, then standard props, then `className`, and finally event handlers (`on*`).
281
+
282
+ ```tsx
283
+ // Correct
284
+ <Button
285
+ id="submit-btn"
286
+ type="submit"
287
+ disabled={isLoading}
288
+ className="w-full bg-blue-500"
289
+ onClick={handleSubmit}
290
+ >
291
+ Submit
292
+ </Button>
293
+ ```
294
+
295
+ ---
296
+
297
+ ## 17. Path Aliases
298
+
299
+ Always use `~/*` path aliases for imports. Never use relative paths like `./` or `../`.
300
+
301
+ ```typescript
302
+ // Correct
303
+ import { Button } from "~/components/ui/button"
304
+
305
+ // Avoid
306
+ import { Button } from "../../components/ui/button"
307
+ ```
308
+
309
+ ---
310
+
311
+ ## 18. TypeScript Strict
312
+
313
+ Maintain strict TypeScript usage. Never use `any`. Use `unknown` if the type is truly unknown, or define a specific type/interface.
314
+
315
+ ```typescript
316
+ // Correct
317
+ export const handleData = (data: unknown) => {
318
+ if (typeof data === "string") {
319
+ return data.toUpperCase()
320
+ }
321
+ }
322
+
323
+ // Avoid
324
+ export const handleData = (data: any) => {
325
+ return data.toUpperCase()
326
+ }
327
+ ```
@@ -0,0 +1,159 @@
1
+ ---
2
+ name: testing
3
+ description: Guide for writing comprehensive, well-structured tests with full coverage. Use when creating test files, adding tests to existing files, reviewing test coverage, or when asked to write, fix, or expand tests. Covers test organization (flat vs nested), coverage categories (happy/sad/edge), .todo patterns for planned tests, and formatting conventions.
4
+ ---
5
+
6
+ # Test Writing Guide
7
+
8
+ Detect the test library from project (bun:test, vitest, jest, etc.) and use its idioms. Never assume a specific framework.
9
+
10
+ ## File Conventions
11
+
12
+ - Test files use `*.test.ts` naming
13
+ - Co-locate tests next to their source file (not in `__tests__/` directories)
14
+ - Example: `src/lib/utils/validators.ts` → `src/lib/utils/validators.test.ts`
15
+
16
+ ## Structure: Flat vs Nested
17
+
18
+ **Flat** — for pure/simple functions (parsers, validators, formatters):
19
+
20
+ ```typescript
21
+ import { describe, it, expect } from "bun:test"
22
+
23
+ describe("functionName", () => {
24
+ it("should return X with valid input", () => {
25
+ // ...
26
+ })
27
+
28
+ it("should return undefined for empty string", () => {
29
+ // ...
30
+ })
31
+ })
32
+ ```
33
+
34
+ **Nested 3-category** — for complex functions (DB operations, API handlers, side effects):
35
+
36
+ ```typescript
37
+ import { describe, it, expect } from "bun:test"
38
+
39
+ describe("functionName", () => {
40
+ describe("happy paths", () => {
41
+ it("should create record with all fields", async () => {
42
+ // ...
43
+ })
44
+
45
+ it("should create record with minimal fields", async () => {
46
+ // ...
47
+ })
48
+ })
49
+
50
+ describe("sad paths", () => {
51
+ it("should throw NotFoundError when record missing", async () => {
52
+ // ...
53
+ })
54
+ })
55
+
56
+ describe("edge cases", () => {
57
+ it("should handle duplicate operation idempotently", async () => {
58
+ // ...
59
+ })
60
+ })
61
+ })
62
+ ```
63
+
64
+ **When to use which:**
65
+ - Side effects, external deps, DB, API calls → nested
66
+ - Pure transformation, parser, validator → flat
67
+ - Flat file growing beyond ~15 tests → consider switching to nested
68
+
69
+ ## Coverage Categories
70
+
71
+ **Happy paths** — normal successful operations:
72
+ - Success with full/complete input
73
+ - Success with minimal/partial input
74
+ - Expected return values and side effects
75
+
76
+ **Sad paths** — expected failures:
77
+ - Thrown exceptions (NotFoundError, ValidationError, etc.)
78
+ - Validation failures on invalid input
79
+ - Unauthorized/wrong-user access
80
+ - Missing required data or dependencies
81
+
82
+ **Edge cases** — boundary conditions:
83
+ - Idempotency (repeating the same operation)
84
+ - Pagination and cursor handling
85
+ - Null, empty, or zero values
86
+ - Concurrent or conflicting operations
87
+ - Already-existing data (upsert behavior)
88
+ - Computed/derived fields and timestamps
89
+
90
+ ## Coverage Checklist
91
+
92
+ For every function, consider:
93
+ 1. What does success look like with full input?
94
+ 2. What does success look like with minimal input?
95
+ 3. What errors can this function throw?
96
+ 4. What happens with unauthorized/wrong-user access?
97
+ 5. What are the boundary conditions?
98
+ 6. What if the operation is repeated (idempotent)?
99
+ 7. What if related data is missing or empty?
100
+
101
+ Implement the most critical tests first (happy paths). Use `.todo` for the rest.
102
+
103
+ ## Principles
104
+
105
+ - **Test behavior, not implementation** — test through public API. If behavior doesn't change, tests shouldn't break. If private logic is complex, extract it into its own module and test that.
106
+ - **Mock at boundaries only** — mock external services (HTTP, email, payment), never your own DB or internal modules. Prefer stubs/spies over mocks (verify state, not calls).
107
+ - **Each test is self-contained** — never rely on test execution order. No shared mutable state. Reset mocks in `beforeEach`.
108
+ - **Hard-code expected values** — never compute them with string concat, loops, or conditionals in test code.
109
+ - **Use realistic data** — not "foo"/"bar". Include only data relevant to the specific test.
110
+ - **One behavior per test** — no piggyback assertions testing unrelated things.
111
+ - **Convert production bugs to regression tests** — every bug gets a test before fixing.
112
+
113
+ For detailed guidance on mocking, isolation, data patterns, and common anti-patterns, see [references/best-practices.md](references/best-practices.md).
114
+
115
+ ## The .todo Pattern
116
+
117
+ Use `it.todo` for planned but unimplemented tests. Every `.todo` MUST include a comment inside the callback body explaining the scenario:
118
+
119
+ ```typescript
120
+ import { it } from "bun:test"
121
+
122
+ // Simple scenario — single sentence.
123
+ it.todo("should preserve existing title when feed title is null", () => {
124
+ // Feed has null title — channel should keep its existing title.
125
+ })
126
+
127
+ // Complex scenario — multi-line with setup and expected behavior.
128
+ it.todo("should timeout when headers delayed beyond maxTimeout", () => {
129
+ // Server delays sending headers for 16+ seconds (> maxTimeout: 15s).
130
+ // Expected: ETIMEDOUT error after ~15 seconds, retry triggered,
131
+ // after 3 retries throw UnreachableUrlError.
132
+ })
133
+ ```
134
+
135
+ Use `describe.todo` for entire groups of planned tests:
136
+
137
+ ```typescript
138
+ import { describe, it } from "bun:test"
139
+
140
+ describe.todo("error handling", () => {
141
+ it.todo("should throw on invalid URL", () => {
142
+ // Pass malformed URL like "not-a-url" or "ht!tp://bad".
143
+ // Expected: URL parse error before safety check runs.
144
+ })
145
+ })
146
+ ```
147
+
148
+ **Guidelines:**
149
+ - Never leave a `.todo` without a comment
150
+ - Comments go inside the callback, not above `it.todo`
151
+ - Implement happy paths first, `.todo` sad paths and edge cases when not immediately critical
152
+
153
+ ## Formatting Reference
154
+
155
+ Code examples should follow these conventions:
156
+ - Use double quotes for strings
157
+ - No semicolons
158
+ - Arrow functions for test callbacks
159
+ - `bun:test` as primary reference for `describe`, `it`, `expect`
@@ -0,0 +1,59 @@
1
+ # Testing Best Practices
2
+
3
+ Detailed guidance for writing high-quality tests with `bun:test`.
4
+
5
+ ## Mocking Strategies
6
+
7
+ - **Mock at boundaries**: Focus on mocking external systems like third-party APIs (Stripe, Resend, IPinfo), file system access, or network requests.
8
+ - **Avoid mocking internals**: Don't mock your own database (use a test database or Prisma's client features), local utility functions, or internal business logic modules.
9
+ - **`mock()` vs `mock.module()`**:
10
+ - Use `mock()` for simple function mocks or spies.
11
+ - Use `mock.module()` for entire module replacements.
12
+ - **CRITICAL**: `mock.module()` leaks across files in the same Bun process. Avoid using it for service modules in orchestrator-level tests where parallel execution might occur.
13
+
14
+ ```typescript
15
+ import { mock, it, expect } from "bun:test"
16
+
17
+ const myMock = mock(() => "result")
18
+ expect(myMock()).toBe("result")
19
+ expect(myMock).toHaveBeenCalled()
20
+ ```
21
+
22
+ ## Test Isolation
23
+
24
+ - **No shared state**: Every test should be able to run in any order. Avoid global variables that change during tests.
25
+ - **`beforeEach` reset**: Always clear mocks and reset database state before each test.
26
+ - **Independent data**: Create the data you need within the test or in a `beforeEach` block rather than relying on existing state.
27
+
28
+ ```typescript
29
+ import { beforeEach, mock } from "bun:test"
30
+ import { myService } from "~/lib/services/myService"
31
+
32
+ mock.module("~/lib/services/externalApi", () => ({
33
+ fetchData: mock(() => Promise.resolve({ success: true }))
34
+ }))
35
+
36
+ beforeEach(() => {
37
+ // Clear mock history between tests
38
+ })
39
+ ```
40
+
41
+ ## Data Patterns
42
+
43
+ - **Realistic data**: Use data that resembles production values. Instead of "test", "foo", or "bar", use "user_123", "example.com", or "192.168.1.1".
44
+ - **Minimal relevance**: Only include fields in your test objects that are relevant to the behavior being tested.
45
+ - **Fixed expectations**: Hard-code your expected results. If you calculate them in the test, you might just be duplicating the bug in the production code.
46
+
47
+ ## Common Anti-patterns
48
+
49
+ - **Testing implementation**: If you're checking private methods or specific internal variables, your tests will break when you refactor, even if the feature still works.
50
+ - **Over-mocking**: Mocking everything makes your tests pass even if the real integration is broken.
51
+ - **Piggyback assertions**: Adding unrelated checks to a single test makes it harder to diagnose failures. One test, one behavior.
52
+
53
+ ## framework-specific notes for bun:test
54
+
55
+ - **`mock()`**: Creates a mock function.
56
+ - **`spyOn()`**: Wraps an existing method to track calls while preserving original behavior.
57
+ - **`mock.module()`**: Replaces a module for the entire process.
58
+ - **Snapshots**: Use `expect(value).toMatchSnapshot()` for complex object comparisons.
59
+ - **Cleanup**: Bun automatically handles some cleanup, but manual mock restoration is often safer in complex suites.