@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 +21 -0
- package/README.md +131 -0
- package/configs/commitlint.json +7 -0
- package/configs/lefthook-commitlint.json +10 -0
- package/configs/lefthook-oxfmt.json +13 -0
- package/configs/lefthook-oxlint.json +13 -0
- package/configs/lefthook-typescript.json +11 -0
- package/configs/oxfmt.json +21 -0
- package/configs/oxlint-base.json +9 -0
- package/configs/oxlint-next.json +9 -0
- package/configs/oxlint-tanstack.json +4 -0
- package/configs/semantic-release-dry-run.json +18 -0
- package/configs/semantic-release.json +21 -0
- package/package.json +58 -0
- package/skills/commits/SKILL.md +67 -0
- package/skills/formatting/SKILL.md +327 -0
- package/skills/testing/SKILL.md +159 -0
- package/skills/testing/references/best-practices.md +59 -0
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
|
+
[](https://www.npmjs.com/package/@dirstack/kodeks)
|
|
4
|
+
[](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,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,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,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.
|