@jterrazz/typescript 5.3.0 → 6.0.1
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/README.md +123 -27
- package/bin/commands/check.sh +164 -0
- package/bin/merge-knip-config.js +240 -0
- package/bin/typescript.sh +11 -3
- package/package.json +24 -7
- package/presets/knip/base.json +5 -0
- package/presets/oxfmt/index.js +12 -0
- package/presets/oxlint/architectures/hexagonal-rules.js +39 -0
- package/presets/oxlint/architectures/hexagonal.js +13 -0
- package/presets/oxlint/base.js +128 -0
- package/presets/oxlint/expo.js +33 -0
- package/presets/oxlint/next.js +23 -0
- package/presets/oxlint/node.js +14 -0
- package/presets/oxlint/plugins/codestyle.js +194 -0
- package/src/index.js +16 -0
- /package/bin/{generate-docs.sh → commands/docs.sh} +0 -0
package/README.md
CHANGED
|
@@ -1,53 +1,75 @@
|
|
|
1
1
|
# @jterrazz/typescript
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The complete TypeScript toolchain — build, run, check, and document with zero configuration. Powered by tsdown, Oxlint, Oxfmt, tsgo, and Knip.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install @jterrazz/typescript
|
|
8
|
+
npm install @jterrazz/typescript --save-dev
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Setup
|
|
12
12
|
|
|
13
13
|
### 1. Choose a TypeScript configuration
|
|
14
14
|
|
|
15
15
|
```json
|
|
16
16
|
// tsconfig.json - Pick one:
|
|
17
|
-
{ "extends": "@jterrazz/typescript/
|
|
18
|
-
{ "extends": "@jterrazz/typescript/
|
|
19
|
-
{ "extends": "@jterrazz/typescript/
|
|
17
|
+
{ "extends": "@jterrazz/typescript/tsconfig/node" } // Node.js projects
|
|
18
|
+
{ "extends": "@jterrazz/typescript/tsconfig/next" } // Next.js projects
|
|
19
|
+
{ "extends": "@jterrazz/typescript/tsconfig/expo" } // Expo/React Native
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
### 2.
|
|
22
|
+
### 2. Create the lint and format configs
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
// oxlint.config.ts
|
|
26
|
+
import { oxlint } from '@jterrazz/typescript';
|
|
27
|
+
import { defineConfig } from 'oxlint';
|
|
28
|
+
|
|
29
|
+
export default defineConfig({
|
|
30
|
+
extends: [oxlint.node],
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// oxfmt.config.ts
|
|
36
|
+
import { oxfmt } from '@jterrazz/typescript';
|
|
37
|
+
import { defineConfig } from 'oxfmt';
|
|
38
|
+
|
|
39
|
+
export default defineConfig(oxfmt);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 3. Use the CLI
|
|
23
43
|
|
|
24
44
|
```bash
|
|
25
45
|
npx typescript build # Build application (ESM + types)
|
|
26
46
|
npx typescript bundle # Bundle library (ESM + CJS + types)
|
|
27
47
|
npx typescript start # Run the built application
|
|
28
48
|
npx typescript dev # Build, run, and rebuild on changes
|
|
49
|
+
npx typescript docs # Generate API reference + llms.txt
|
|
50
|
+
npx typescript check # Check types, lint, formatting, and unused code
|
|
51
|
+
npx typescript fix # Auto-fix lint and formatting issues
|
|
29
52
|
```
|
|
30
53
|
|
|
31
|
-
##
|
|
54
|
+
## Building
|
|
32
55
|
|
|
33
56
|
- **Blazing fast** — Powered by [tsdown](https://tsdown.dev) / [Rolldown](https://rolldown.rs) (Rust)
|
|
34
57
|
- **Zero configuration** — Works out of the box
|
|
35
58
|
- **Multiple outputs** — ESM + CommonJS + TypeScript declarations
|
|
36
59
|
- **Source maps** — Full debugging support
|
|
37
60
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
| `typescript
|
|
43
|
-
| | `dist/index.
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
| `typescript
|
|
48
|
-
| `typescript dev` | `dist/index.js` | Watch + rebuild + run |
|
|
61
|
+
| Command | Output | Description |
|
|
62
|
+
| ------------------- | ----------------- | ------------------------ |
|
|
63
|
+
| `typescript build` | `dist/index.js` | ESM bundle |
|
|
64
|
+
| | `dist/index.d.ts` | TypeScript declarations |
|
|
65
|
+
| `typescript bundle` | `dist/index.js` | ESM bundle |
|
|
66
|
+
| | `dist/index.cjs` | CommonJS bundle |
|
|
67
|
+
| | `dist/index.d.ts` | TypeScript declarations |
|
|
68
|
+
| `typescript start` | — | Runs `dist/index.js` |
|
|
69
|
+
| `typescript dev` | `dist/index.js` | Watch + rebuild + run |
|
|
70
|
+
| `typescript docs` | `.docs/` | API reference + llms.txt |
|
|
49
71
|
|
|
50
|
-
|
|
72
|
+
### Project structure
|
|
51
73
|
|
|
52
74
|
```
|
|
53
75
|
your-project/
|
|
@@ -58,15 +80,89 @@ your-project/
|
|
|
58
80
|
└── tsconfig.json # Extends this package
|
|
59
81
|
```
|
|
60
82
|
|
|
61
|
-
##
|
|
83
|
+
## Quality checks
|
|
84
|
+
|
|
85
|
+
`typescript check` runs four tools in parallel:
|
|
86
|
+
|
|
87
|
+
| Tool | Purpose |
|
|
88
|
+
| ------ | -------------------- |
|
|
89
|
+
| tsgo | Type checking |
|
|
90
|
+
| oxlint | Linting |
|
|
91
|
+
| oxfmt | Formatting |
|
|
92
|
+
| knip | Unused code analysis |
|
|
93
|
+
|
|
94
|
+
`typescript fix` runs tsgo, oxlint (with `--fix`), and oxfmt in parallel (knip excluded).
|
|
95
|
+
|
|
96
|
+
### Lint presets
|
|
97
|
+
|
|
98
|
+
| Preset | Use Case |
|
|
99
|
+
| ------------- | --------------------------------- |
|
|
100
|
+
| `oxlint.node` | Node.js (requires .js extensions) |
|
|
101
|
+
| `oxlint.expo` | Expo / React Native |
|
|
102
|
+
| `oxlint.next` | Next.js |
|
|
103
|
+
|
|
104
|
+
### Architecture enforcement
|
|
105
|
+
|
|
106
|
+
Enforce hexagonal architecture boundaries with the additive `oxlint.hexagonal` preset:
|
|
62
107
|
|
|
63
|
-
|
|
108
|
+
```ts
|
|
109
|
+
import { oxlint } from '@jterrazz/typescript';
|
|
110
|
+
import { defineConfig } from 'oxlint';
|
|
111
|
+
|
|
112
|
+
export default defineConfig({
|
|
113
|
+
extends: [oxlint.node, oxlint.hexagonal],
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Rules enforced:
|
|
118
|
+
|
|
119
|
+
- `domain/` cannot import from other layers
|
|
120
|
+
- `application/` cannot import infrastructure
|
|
121
|
+
- `presentation/ui/` cannot import navigation
|
|
122
|
+
- `features/` cannot import other features
|
|
123
|
+
|
|
124
|
+
### Unused code detection
|
|
125
|
+
|
|
126
|
+
`typescript check` runs [Knip](https://knip.dev/) to detect unused files, exports, and dependencies. A base config is automatically merged with any project-local `knip.json`, handling common ecosystem patterns:
|
|
127
|
+
|
|
128
|
+
- `@jterrazz/*` packages auto-ignored
|
|
129
|
+
- Published libraries: `exports`/`types`/`files` rules auto-disabled
|
|
130
|
+
- Convention paths (`fixtures/`, `expected/`, `docs/`) auto-ignored
|
|
131
|
+
- Plugin dependencies (`*-plugin-*`, `@scope/*`) auto-ignored
|
|
132
|
+
|
|
133
|
+
For fine-tuning, create a `knip.json` with only project-specific overrides.
|
|
134
|
+
|
|
135
|
+
## API docs generation
|
|
136
|
+
|
|
137
|
+
`typescript docs` reads TSDoc from `src/index.ts` and generates:
|
|
138
|
+
|
|
139
|
+
- **Typedoc markdown** — Full API reference under `.docs/`
|
|
140
|
+
- **`llms.txt`** — Structured index following the [llms.txt standard](https://llmstxt.org/)
|
|
141
|
+
- **`llms-full.txt`** — Complete reference in one file for LLM context windows
|
|
142
|
+
|
|
143
|
+
No `typedoc.json` needed. Pair with the shared CI workflow to auto-deploy:
|
|
144
|
+
|
|
145
|
+
```yaml
|
|
146
|
+
# .github/workflows/docs.yaml
|
|
147
|
+
jobs:
|
|
148
|
+
docs:
|
|
149
|
+
uses: jterrazz/jterrazz-actions/.github/workflows/docs.yaml@main
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## How it works
|
|
64
153
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
|
68
|
-
|
|
|
69
|
-
|
|
|
154
|
+
The toolchain is fully compiled — no JavaScript in the hot path:
|
|
155
|
+
|
|
156
|
+
| Step | Tool | Language |
|
|
157
|
+
| ------------ | -------------------------------------------------- | -------- |
|
|
158
|
+
| Transpile | [Oxc](https://oxc.rs) (via tsdown) | Rust |
|
|
159
|
+
| Bundle | [Rolldown](https://rolldown.rs) | Rust |
|
|
160
|
+
| Declarations | [tsdown](https://tsdown.dev) built-in | Rust |
|
|
161
|
+
| Type check | [tsgo](https://github.com/microsoft/typescript-go) | Go |
|
|
162
|
+
| Lint | [Oxlint](https://oxc.rs/docs/guide/usage/linter) | Rust |
|
|
163
|
+
| Format | [Oxfmt](https://oxc.rs/docs/guide/usage/formatter) | Rust |
|
|
164
|
+
| Unused code | [Knip](https://knip.dev) | Node |
|
|
165
|
+
| API docs | [Typedoc](https://typedoc.org) | Node |
|
|
70
166
|
|
|
71
167
|
## License
|
|
72
168
|
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Quality checks: runs tsgo, oxlint, oxfmt, and knip in parallel.
|
|
3
|
+
# Called by: typescript check | typescript fix
|
|
4
|
+
|
|
5
|
+
# Colors for output
|
|
6
|
+
RED='\033[0;31m'
|
|
7
|
+
GREEN='\033[0;32m'
|
|
8
|
+
CYAN_BG='\033[46m'
|
|
9
|
+
BRIGHT_WHITE='\033[1;30m'
|
|
10
|
+
NC='\033[0m'
|
|
11
|
+
|
|
12
|
+
# Resolve symlinks to get the real script location
|
|
13
|
+
SOURCE="${BASH_SOURCE[0]}"
|
|
14
|
+
while [ -L "$SOURCE" ]; do
|
|
15
|
+
DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
|
|
16
|
+
SOURCE="$(readlink "$SOURCE")"
|
|
17
|
+
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE"
|
|
18
|
+
done
|
|
19
|
+
SCRIPT_DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
|
|
20
|
+
PACKAGE_ROOT="$SCRIPT_DIR/../.."
|
|
21
|
+
|
|
22
|
+
# Find bin directory
|
|
23
|
+
if [ -x "$PACKAGE_ROOT/../../.bin/oxlint" ]; then
|
|
24
|
+
BIN_DIR="$PACKAGE_ROOT/../../.bin"
|
|
25
|
+
elif [ -x "$PACKAGE_ROOT/node_modules/.bin/oxlint" ]; then
|
|
26
|
+
BIN_DIR="$PACKAGE_ROOT/node_modules/.bin"
|
|
27
|
+
else
|
|
28
|
+
BIN_DIR="$(npm bin 2>/dev/null)"
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Parse command and args
|
|
32
|
+
COMMAND=""
|
|
33
|
+
LINT_ARGS=()
|
|
34
|
+
|
|
35
|
+
if [[ "${1:-}" == -* ]] || [ -z "${1:-}" ]; then
|
|
36
|
+
# No command, everything is args
|
|
37
|
+
true
|
|
38
|
+
else
|
|
39
|
+
COMMAND="$1"
|
|
40
|
+
shift
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# Parse remaining args
|
|
44
|
+
while [[ $# -gt 0 ]]; do
|
|
45
|
+
case $1 in
|
|
46
|
+
--ignore-pattern)
|
|
47
|
+
LINT_ARGS+=("$1" "$2")
|
|
48
|
+
shift 2
|
|
49
|
+
;;
|
|
50
|
+
*)
|
|
51
|
+
shift
|
|
52
|
+
;;
|
|
53
|
+
esac
|
|
54
|
+
done
|
|
55
|
+
|
|
56
|
+
# Create a temporary directory for log files
|
|
57
|
+
tmp_dir=$(mktemp -d)
|
|
58
|
+
cleanup() { rm -rf "$tmp_dir"; }
|
|
59
|
+
trap cleanup EXIT
|
|
60
|
+
|
|
61
|
+
run_checks() {
|
|
62
|
+
local FIX_MODE="$1"
|
|
63
|
+
local LABEL
|
|
64
|
+
|
|
65
|
+
if [ "$FIX_MODE" = true ]; then
|
|
66
|
+
LABEL="Running quality fixes"
|
|
67
|
+
else
|
|
68
|
+
LABEL="Running quality checks"
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
printf "${CYAN_BG}${BRIGHT_WHITE} START ${NC} ${LABEL}\n"
|
|
72
|
+
|
|
73
|
+
# Run all tools in parallel
|
|
74
|
+
"$BIN_DIR/tsgo" --noEmit > "$tmp_dir/type.log" 2>&1 &
|
|
75
|
+
local type_pid=$!
|
|
76
|
+
|
|
77
|
+
if [ "$FIX_MODE" = true ]; then
|
|
78
|
+
"$BIN_DIR/oxlint" --fix "${LINT_ARGS[@]}" > "$tmp_dir/lint.log" 2>&1 &
|
|
79
|
+
else
|
|
80
|
+
"$BIN_DIR/oxlint" "${LINT_ARGS[@]}" > "$tmp_dir/lint.log" 2>&1 &
|
|
81
|
+
fi
|
|
82
|
+
local lint_pid=$!
|
|
83
|
+
|
|
84
|
+
if [ "$FIX_MODE" = true ]; then
|
|
85
|
+
"$BIN_DIR/oxfmt" > "$tmp_dir/format.log" 2>&1 &
|
|
86
|
+
else
|
|
87
|
+
"$BIN_DIR/oxfmt" --check > "$tmp_dir/format.log" 2>&1 &
|
|
88
|
+
fi
|
|
89
|
+
local format_pid=$!
|
|
90
|
+
|
|
91
|
+
# Knip: only run in check mode (fix mode is destructive)
|
|
92
|
+
# Merge base config (from this package) with optional project-local knip.json
|
|
93
|
+
local knip_pid=""
|
|
94
|
+
local knip_status=0
|
|
95
|
+
if [ "$FIX_MODE" = false ]; then
|
|
96
|
+
local knip_base="$PACKAGE_ROOT/presets/knip/base.json"
|
|
97
|
+
local knip_project=""
|
|
98
|
+
[ -f "knip.json" ] && knip_project="knip.json"
|
|
99
|
+
[ -f "knip.jsonc" ] && knip_project="knip.jsonc"
|
|
100
|
+
|
|
101
|
+
node "$PACKAGE_ROOT/bin/merge-knip-config.js" "$knip_base" $knip_project > "$tmp_dir/knip-merged.json"
|
|
102
|
+
"$BIN_DIR/knip" --no-progress --no-config-hints --config "$tmp_dir/knip-merged.json" > "$tmp_dir/knip.log" 2>&1 &
|
|
103
|
+
knip_pid=$!
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# Wait and collect statuses
|
|
107
|
+
wait $type_pid; local type_status=$?
|
|
108
|
+
wait $lint_pid; local lint_status=$?
|
|
109
|
+
wait $format_pid; local format_status=$?
|
|
110
|
+
[ -n "$knip_pid" ] && { wait $knip_pid; knip_status=$?; }
|
|
111
|
+
|
|
112
|
+
# Print results
|
|
113
|
+
printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} TypeScript Check\n\n"
|
|
114
|
+
[ -s "$tmp_dir/type.log" ] && cat "$tmp_dir/type.log"
|
|
115
|
+
[ $type_status -ne 0 ] && printf "${RED}✗ Failed with exit code %d${NC}\n" $type_status || printf "${GREEN}✓ Passed${NC}\n"
|
|
116
|
+
|
|
117
|
+
local lint_label="Oxlint Check"
|
|
118
|
+
[ "$FIX_MODE" = true ] && lint_label="Oxlint Fix"
|
|
119
|
+
printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} ${lint_label}\n\n"
|
|
120
|
+
[ -s "$tmp_dir/lint.log" ] && cat "$tmp_dir/lint.log"
|
|
121
|
+
[ $lint_status -ne 0 ] && printf "${RED}✗ Failed with exit code %d${NC}\n" $lint_status || printf "${GREEN}✓ Passed${NC}\n"
|
|
122
|
+
|
|
123
|
+
local format_label="Oxfmt Check"
|
|
124
|
+
[ "$FIX_MODE" = true ] && format_label="Oxfmt Format"
|
|
125
|
+
printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} ${format_label}\n\n"
|
|
126
|
+
[ -s "$tmp_dir/format.log" ] && cat "$tmp_dir/format.log"
|
|
127
|
+
[ $format_status -ne 0 ] && printf "${RED}✗ Failed with exit code %d${NC}\n" $format_status || printf "${GREEN}✓ Passed${NC}\n"
|
|
128
|
+
|
|
129
|
+
if [ "$FIX_MODE" = false ]; then
|
|
130
|
+
printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} Knip (unused code)\n\n"
|
|
131
|
+
[ -s "$tmp_dir/knip.log" ] && cat "$tmp_dir/knip.log"
|
|
132
|
+
[ $knip_status -ne 0 ] && printf "${RED}✗ Failed with exit code %d${NC}\n" $knip_status || printf "${GREEN}✓ Passed${NC}\n"
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
# Summary
|
|
136
|
+
if [ "$FIX_MODE" = true ]; then
|
|
137
|
+
printf "\n${CYAN_BG}${BRIGHT_WHITE} END ${NC} Finalizing quality fixes\n\n"
|
|
138
|
+
else
|
|
139
|
+
printf "\n${CYAN_BG}${BRIGHT_WHITE} END ${NC} Finalizing quality checks\n\n"
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
if [ $type_status -eq 0 ] && [ $lint_status -eq 0 ] && [ $format_status -eq 0 ] && [ $knip_status -eq 0 ]; then
|
|
143
|
+
printf "${GREEN}✓ All checks passed${NC}\n"
|
|
144
|
+
exit 0
|
|
145
|
+
else
|
|
146
|
+
printf "${RED}✗ Some checks failed${NC}\n"
|
|
147
|
+
exit 1
|
|
148
|
+
fi
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
case "$COMMAND" in
|
|
152
|
+
check)
|
|
153
|
+
run_checks false
|
|
154
|
+
;;
|
|
155
|
+
|
|
156
|
+
fix)
|
|
157
|
+
run_checks true
|
|
158
|
+
;;
|
|
159
|
+
|
|
160
|
+
*)
|
|
161
|
+
printf "Usage: check.sh <check|fix> [--ignore-pattern <pattern>]\n"
|
|
162
|
+
exit 1
|
|
163
|
+
;;
|
|
164
|
+
esac
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Merges the base knip config with an optional project-local knip.json,
|
|
5
|
+
* then applies dynamic detection rules based on the project's package.json and file structure.
|
|
6
|
+
*
|
|
7
|
+
* Usage: node merge-knip-config.js <base.json> [project-knip.json]
|
|
8
|
+
*
|
|
9
|
+
* Merge rules:
|
|
10
|
+
* - Arrays (ignoreDependencies, ignoreBinaries, ignore): concatenated and deduplicated
|
|
11
|
+
* - Objects (rules, vitest, etc.): project values override base
|
|
12
|
+
* - Scalars (project, entry): project value wins
|
|
13
|
+
* - $schema is always stripped from output
|
|
14
|
+
*
|
|
15
|
+
* Dynamic detection:
|
|
16
|
+
* - Published library (main/exports/publishConfig) → disable exports/types/files rules
|
|
17
|
+
* - Auto-ignore docs/**, fixtures/**, expected/** convention paths
|
|
18
|
+
* - Vitest workspace workaround → explicit vitest config when vitest is a dependency
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
22
|
+
|
|
23
|
+
const basePath = process.argv[2];
|
|
24
|
+
const projectPath = process.argv[3];
|
|
25
|
+
|
|
26
|
+
const base = JSON.parse(readFileSync(basePath, 'utf8'));
|
|
27
|
+
|
|
28
|
+
let project = {};
|
|
29
|
+
if (projectPath) {
|
|
30
|
+
project = JSON.parse(readFileSync(projectPath, 'utf8'));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ARRAY_KEYS = new Set(['ignore', 'ignoreBinaries', 'ignoreDependencies', 'entry', 'project']);
|
|
34
|
+
|
|
35
|
+
const merged = {};
|
|
36
|
+
const allKeys = new Set([...Object.keys(base), ...Object.keys(project)]);
|
|
37
|
+
|
|
38
|
+
for (const key of allKeys) {
|
|
39
|
+
if (key === '$schema') {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const bVal = base[key];
|
|
44
|
+
const pVal = project[key];
|
|
45
|
+
|
|
46
|
+
if (pVal === undefined) {
|
|
47
|
+
merged[key] = bVal;
|
|
48
|
+
} else if (bVal === undefined) {
|
|
49
|
+
merged[key] = pVal;
|
|
50
|
+
} else if (ARRAY_KEYS.has(key) && Array.isArray(bVal) && Array.isArray(pVal)) {
|
|
51
|
+
merged[key] = [...new Set([...bVal, ...pVal])];
|
|
52
|
+
} else if (typeof bVal === 'object' && typeof pVal === 'object' && !Array.isArray(bVal)) {
|
|
53
|
+
merged[key] = { ...bVal, ...pVal };
|
|
54
|
+
} else {
|
|
55
|
+
merged[key] = pVal;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- Dynamic detection ---
|
|
60
|
+
|
|
61
|
+
let pkg = {};
|
|
62
|
+
if (existsSync('package.json')) {
|
|
63
|
+
pkg = JSON.parse(readFileSync('package.json', 'utf8'));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 1. Published library → disable exports/types/files rules
|
|
67
|
+
if (pkg.main || pkg.exports || pkg.publishConfig) {
|
|
68
|
+
merged.rules = { exports: 'off', files: 'off', types: 'off', ...merged.rules };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 2. Auto-ignore convention paths that exist on disk
|
|
72
|
+
const conventionIgnores = [];
|
|
73
|
+
if (existsSync('docs')) {
|
|
74
|
+
conventionIgnores.push('docs/**');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Scan for fixtures/** and expected/** directories anywhere in the tree (up to 3 levels)
|
|
78
|
+
const scanDirs = ['tests', 'test', 'src'];
|
|
79
|
+
for (const root of scanDirs) {
|
|
80
|
+
if (!existsSync(root)) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
findConventionDirs(root, 0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function findConventionDirs(dir, depth) {
|
|
87
|
+
if (depth > 3) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
let entries;
|
|
91
|
+
try {
|
|
92
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
93
|
+
} catch {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
for (const entry of entries) {
|
|
97
|
+
if (!entry.isDirectory()) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (entry.name === 'fixtures' || entry.name === 'expected') {
|
|
101
|
+
conventionIgnores.push(`${dir}/${entry.name}/**`);
|
|
102
|
+
} else if (entry.name !== 'node_modules') {
|
|
103
|
+
findConventionDirs(`${dir}/${entry.name}`, depth + 1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (conventionIgnores.length > 0) {
|
|
109
|
+
const existing = merged.ignore || [];
|
|
110
|
+
merged.ignore = [...new Set([...existing, ...conventionIgnores])];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 3. Vitest workspace workaround — knip crashes on vitest.workspace.ts
|
|
114
|
+
// If vitest is a dependency, explicitly set the vitest config to avoid the workspace file
|
|
115
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies };
|
|
116
|
+
if (allDeps.vitest && !merged.vitest) {
|
|
117
|
+
if (existsSync('vitest.config.ts')) {
|
|
118
|
+
merged.vitest = { config: ['vitest.config.ts'] };
|
|
119
|
+
} else if (existsSync('vitest.config.js')) {
|
|
120
|
+
merged.vitest = { config: ['vitest.config.js'] };
|
|
121
|
+
} else if (existsSync('vitest.workspace.ts')) {
|
|
122
|
+
merged.vitest = { config: ['vitest.workspace.ts'] };
|
|
123
|
+
} else if (existsSync('vitest.workspace.js')) {
|
|
124
|
+
merged.vitest = { config: ['vitest.workspace.js'] };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Also ignore vitest.workspace.ts if it exists (knip can't parse it)
|
|
129
|
+
if (existsSync('vitest.workspace.ts') || existsSync('vitest.workspace.js')) {
|
|
130
|
+
const existing = merged.ignore || [];
|
|
131
|
+
const wsFile = existsSync('vitest.workspace.ts')
|
|
132
|
+
? 'vitest.workspace.ts'
|
|
133
|
+
: 'vitest.workspace.js';
|
|
134
|
+
merged.ignore = [...new Set([...existing, wsFile])];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 4. Playwright at non-standard paths + string-referenced global hooks.
|
|
138
|
+
// Knip auto-discovers `playwright.config.ts` at the repo root, but
|
|
139
|
+
// Projects that keep their browser tests under a dedicated subdir
|
|
140
|
+
// (e.g. `web/playwright.config.ts`, `e2e/playwright.config.ts`,
|
|
141
|
+
// `tests/playwright.config.ts`) need an explicit entry. The global
|
|
142
|
+
// Setup/teardown files next to that config are referenced as plain
|
|
143
|
+
// Strings from `playwright.config.ts`, so knip can't trace them —
|
|
144
|
+
// Add them as entries when they exist.
|
|
145
|
+
if (allDeps['@playwright/test'] || allDeps.playwright) {
|
|
146
|
+
const playwrightSearchPaths = [
|
|
147
|
+
'web/playwright.config.ts',
|
|
148
|
+
'web/playwright.config.js',
|
|
149
|
+
'e2e/playwright.config.ts',
|
|
150
|
+
'e2e/playwright.config.js',
|
|
151
|
+
'tests/playwright.config.ts',
|
|
152
|
+
'tests/playwright.config.js',
|
|
153
|
+
];
|
|
154
|
+
const playwrightEntries = [];
|
|
155
|
+
for (const configPath of playwrightSearchPaths) {
|
|
156
|
+
if (!existsSync(configPath)) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
playwrightEntries.push(configPath);
|
|
160
|
+
const configDir = configPath.slice(0, configPath.lastIndexOf('/'));
|
|
161
|
+
for (const hook of [
|
|
162
|
+
`${configDir}/setup/global-setup.ts`,
|
|
163
|
+
`${configDir}/setup/global-teardown.ts`,
|
|
164
|
+
`${configDir}/setup/global-setup.js`,
|
|
165
|
+
`${configDir}/setup/global-teardown.js`,
|
|
166
|
+
]) {
|
|
167
|
+
if (existsSync(hook)) {
|
|
168
|
+
playwrightEntries.push(hook);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (playwrightEntries.length > 0) {
|
|
173
|
+
const existing = merged.entry || [];
|
|
174
|
+
merged.entry = [...new Set([...existing, ...playwrightEntries])];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 5. Auto-ignore plugin/addon dependencies for known parent packages
|
|
179
|
+
const prodDeps = Object.keys(pkg.dependencies || {});
|
|
180
|
+
const devDeps = Object.keys(pkg.devDependencies || {});
|
|
181
|
+
const peerDeps = Object.keys(pkg.peerDependencies || {});
|
|
182
|
+
const allDepNames = Object.keys(allDeps);
|
|
183
|
+
const autoIgnoreDeps = [];
|
|
184
|
+
|
|
185
|
+
// DevDependencies that are also peerDependencies (installed locally for testing)
|
|
186
|
+
for (const dep of devDeps) {
|
|
187
|
+
if (peerDeps.includes(dep)) {
|
|
188
|
+
autoIgnoreDeps.push(dep);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for (const dep of [...prodDeps, ...devDeps]) {
|
|
193
|
+
// @scope/sub-package when the unscoped root is a peer or dependency
|
|
194
|
+
// E.g. @hono/node-server → hono
|
|
195
|
+
const scopeMatch = dep.match(/^@(?<scope>[^/]+)\//);
|
|
196
|
+
if (
|
|
197
|
+
scopeMatch &&
|
|
198
|
+
(peerDeps.includes(scopeMatch.groups.scope) ||
|
|
199
|
+
allDepNames.includes(scopeMatch.groups.scope))
|
|
200
|
+
) {
|
|
201
|
+
autoIgnoreDeps.push(dep);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// <parent>-plugin-* or <parent>-preset-* when <parent> is installed
|
|
206
|
+
// E.g. vitepress-plugin-llms → vitepress
|
|
207
|
+
const pluginMatch = dep.match(/^(?<parent>.+?)-(?:plugin|preset|transformer|loader)-/);
|
|
208
|
+
if (pluginMatch && allDepNames.includes(pluginMatch.groups.parent)) {
|
|
209
|
+
autoIgnoreDeps.push(dep);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// eslint-plugin-* — always a runtime plugin loaded by config, never imported
|
|
214
|
+
if (dep.startsWith('eslint-plugin-')) {
|
|
215
|
+
autoIgnoreDeps.push(dep);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 5. Auto-ignore production dependencies that provide binaries
|
|
220
|
+
// When a binary-only tool is in "dependencies", it's shipped for consumers (e.g. @jterrazz/typescript ships oxlint)
|
|
221
|
+
for (const dep of prodDeps) {
|
|
222
|
+
if (autoIgnoreDeps.includes(dep)) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const depPkg = JSON.parse(readFileSync(`node_modules/${dep}/package.json`, 'utf8'));
|
|
227
|
+
if (depPkg.bin && Object.keys(depPkg.bin).length > 0) {
|
|
228
|
+
autoIgnoreDeps.push(dep);
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
// Dep not installed locally — skip
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (autoIgnoreDeps.length > 0) {
|
|
236
|
+
const existing = merged.ignoreDependencies || [];
|
|
237
|
+
merged.ignoreDependencies = [...new Set([...existing, ...autoIgnoreDeps])];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
process.stdout.write(JSON.stringify(merged));
|
package/bin/typescript.sh
CHANGED
|
@@ -90,26 +90,34 @@ case "$COMMAND" in
|
|
|
90
90
|
docs)
|
|
91
91
|
printf "${CYAN_BG}${BRIGHT_WHITE} TYPESCRIPT ${NC} Generating API docs...\n\n"
|
|
92
92
|
|
|
93
|
-
bash "$SCRIPT_DIR/
|
|
93
|
+
bash "$SCRIPT_DIR/commands/docs.sh" "$PROJECT_ROOT" "$PACKAGE_ROOT"
|
|
94
94
|
|
|
95
95
|
printf "\n${GREEN}Docs generated at .docs/${NC}\n"
|
|
96
96
|
;;
|
|
97
97
|
|
|
98
|
+
check|fix)
|
|
99
|
+
exec bash "$SCRIPT_DIR/commands/check.sh" "$COMMAND" "$@"
|
|
100
|
+
;;
|
|
101
|
+
|
|
98
102
|
*)
|
|
99
|
-
printf "${CYAN_BG}${BRIGHT_WHITE} TYPESCRIPT ${NC} TypeScript
|
|
103
|
+
printf "${CYAN_BG}${BRIGHT_WHITE} TYPESCRIPT ${NC} TypeScript toolchain\n\n"
|
|
100
104
|
printf "Usage: typescript <command>\n\n"
|
|
101
105
|
printf "Commands:\n"
|
|
102
106
|
printf " build Build application (ESM + types)\n"
|
|
103
107
|
printf " bundle Bundle library (ESM + CJS + types)\n"
|
|
104
108
|
printf " start Run the built application\n"
|
|
105
109
|
printf " dev Build, run, and rebuild on changes\n"
|
|
106
|
-
printf " docs Generate API reference + llms.txt from TSDoc\n
|
|
110
|
+
printf " docs Generate API reference + llms.txt from TSDoc\n"
|
|
111
|
+
printf " check Check types, lint, formatting, and unused code\n"
|
|
112
|
+
printf " fix Auto-fix lint and formatting issues\n\n"
|
|
107
113
|
printf "Examples:\n"
|
|
108
114
|
printf " typescript build\n"
|
|
109
115
|
printf " typescript bundle\n"
|
|
110
116
|
printf " typescript start\n"
|
|
111
117
|
printf " typescript dev\n"
|
|
112
118
|
printf " typescript docs\n"
|
|
119
|
+
printf " typescript check\n"
|
|
120
|
+
printf " typescript fix\n"
|
|
113
121
|
exit 1
|
|
114
122
|
;;
|
|
115
123
|
esac
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jterrazz/typescript",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.1",
|
|
4
4
|
"author": "Jean-Baptiste Terrazzoni <contact@jterrazz.com>",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -11,25 +11,42 @@
|
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"bin",
|
|
14
|
-
"presets"
|
|
14
|
+
"presets",
|
|
15
|
+
"src"
|
|
15
16
|
],
|
|
16
17
|
"type": "module",
|
|
18
|
+
"main": "src/index.js",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./src/index.js",
|
|
21
|
+
"./tsconfig/*": "./presets/tsconfig/*.json",
|
|
22
|
+
"./tsconfig/*.json": "./presets/tsconfig/*.json",
|
|
23
|
+
"./tsdown/*": "./presets/tsdown/*.js",
|
|
24
|
+
"./tsdown/*.js": "./presets/tsdown/*.js",
|
|
25
|
+
"./presets/tsconfig/*": "./presets/tsconfig/*.json",
|
|
26
|
+
"./presets/tsconfig/*.json": "./presets/tsconfig/*.json",
|
|
27
|
+
"./presets/tsdown/*": "./presets/tsdown/*.js",
|
|
28
|
+
"./presets/tsdown/*.js": "./presets/tsdown/*.js"
|
|
29
|
+
},
|
|
17
30
|
"publishConfig": {
|
|
18
31
|
"registry": "https://registry.npmjs.org/"
|
|
19
32
|
},
|
|
20
33
|
"scripts": {
|
|
21
|
-
"
|
|
22
|
-
"lint
|
|
23
|
-
"
|
|
24
|
-
"
|
|
34
|
+
"build": "# no build script",
|
|
35
|
+
"lint": "./bin/typescript.sh check --ignore-pattern '**/fixtures/**'",
|
|
36
|
+
"lint:fix": "./bin/typescript.sh fix --ignore-pattern '**/fixtures/**'",
|
|
37
|
+
"test": "vitest --run"
|
|
25
38
|
},
|
|
26
39
|
"dependencies": {
|
|
40
|
+
"@typescript/native-preview": "^7.0.0-dev.20260327.2",
|
|
41
|
+
"eslint-plugin-perfectionist": "^5.7.0",
|
|
42
|
+
"knip": "^6.2.0",
|
|
43
|
+
"oxfmt": "^0.42.0",
|
|
44
|
+
"oxlint": "^1.57.0",
|
|
27
45
|
"tsdown": "^0.21.5",
|
|
28
46
|
"typedoc": "^0.28.19",
|
|
29
47
|
"typedoc-plugin-markdown": "^4.11.0"
|
|
30
48
|
},
|
|
31
49
|
"devDependencies": {
|
|
32
|
-
"@jterrazz/codestyle": "^3.0.9",
|
|
33
50
|
"@jterrazz/test": "^5.3.2",
|
|
34
51
|
"@types/node": "^25.5.0",
|
|
35
52
|
"vitest": "^4.1.2"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export default [
|
|
2
|
+
{
|
|
3
|
+
from: '/domain/',
|
|
4
|
+
disallow: [
|
|
5
|
+
'/application/',
|
|
6
|
+
'/infrastructure/',
|
|
7
|
+
'/presentation/',
|
|
8
|
+
'/di/',
|
|
9
|
+
'/config/',
|
|
10
|
+
'/generated/',
|
|
11
|
+
],
|
|
12
|
+
message: 'Domain layer must be pure - cannot import from other layers',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
from: '/application/use-cases/',
|
|
16
|
+
disallow: ['/infrastructure/', '/presentation/', '/di/'],
|
|
17
|
+
message: 'Use cases can only depend on domain and ports',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
from: '/application/ports/',
|
|
21
|
+
disallow: ['/infrastructure/', '/presentation/', '/di/'],
|
|
22
|
+
message: 'Ports are interfaces - cannot depend on implementations',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
from: '/infrastructure/inbound/',
|
|
26
|
+
disallow: ['/infrastructure/outbound/'],
|
|
27
|
+
message: 'Inbound adapters should not import outbound adapters - use DI',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
from: '/presentation/ui/(atoms|molecules)/',
|
|
31
|
+
disallow: ['/navigation/'],
|
|
32
|
+
message: 'Atoms and molecules must be pure - no navigation imports',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
from: '/presentation/features/',
|
|
36
|
+
disallow: ['/presentation/features/(?!common)'],
|
|
37
|
+
message: 'Features should be independent - use shared code in features/common',
|
|
38
|
+
},
|
|
39
|
+
];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { defineConfig } from 'oxlint';
|
|
3
|
+
|
|
4
|
+
import defaultRules from './hexagonal-rules.js';
|
|
5
|
+
|
|
6
|
+
const pluginPath = resolve(import.meta.dirname, '../plugins/codestyle.js');
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
jsPlugins: [pluginPath],
|
|
10
|
+
rules: {
|
|
11
|
+
'codestyle/arch-hexagonal': ['error', { rules: defaultRules }],
|
|
12
|
+
},
|
|
13
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { defineConfig } from 'oxlint';
|
|
3
|
+
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const perfectionistPath = require.resolve('eslint-plugin-perfectionist');
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
plugins: ['typescript', 'import', 'oxc', 'unicorn'],
|
|
9
|
+
jsPlugins: [perfectionistPath],
|
|
10
|
+
categories: {
|
|
11
|
+
correctness: 'error',
|
|
12
|
+
suspicious: 'error',
|
|
13
|
+
perf: 'error',
|
|
14
|
+
pedantic: 'off',
|
|
15
|
+
style: 'error',
|
|
16
|
+
nursery: 'off',
|
|
17
|
+
},
|
|
18
|
+
rules: {
|
|
19
|
+
// ============================================
|
|
20
|
+
// ENABLED RULES
|
|
21
|
+
// ============================================
|
|
22
|
+
|
|
23
|
+
'no-unused-vars': 'error',
|
|
24
|
+
|
|
25
|
+
// -- TypeScript --
|
|
26
|
+
'typescript/consistent-type-imports': [
|
|
27
|
+
'error',
|
|
28
|
+
{
|
|
29
|
+
disallowTypeAnnotations: true,
|
|
30
|
+
fixStyle: 'inline-type-imports',
|
|
31
|
+
prefer: 'type-imports',
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
'typescript/no-unused-expressions': [
|
|
35
|
+
'error',
|
|
36
|
+
{
|
|
37
|
+
allowShortCircuit: true,
|
|
38
|
+
allowTernary: true,
|
|
39
|
+
allowTaggedTemplates: true,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
|
|
43
|
+
// -- Perfectionist (sorting/ordering) --
|
|
44
|
+
'perfectionist/sort-heritage-clauses': ['error', { type: 'natural' }],
|
|
45
|
+
'perfectionist/sort-intersection-types': ['error', { type: 'natural' }],
|
|
46
|
+
'perfectionist/sort-jsx-props': ['error', { type: 'natural' }],
|
|
47
|
+
'perfectionist/sort-named-exports': ['error', { type: 'natural' }],
|
|
48
|
+
'perfectionist/sort-named-imports': ['error', { type: 'natural' }],
|
|
49
|
+
'perfectionist/sort-union-types': ['error', { type: 'natural' }],
|
|
50
|
+
'perfectionist/sort-imports': [
|
|
51
|
+
'error',
|
|
52
|
+
{
|
|
53
|
+
type: 'alphabetical',
|
|
54
|
+
order: 'asc',
|
|
55
|
+
ignoreCase: true,
|
|
56
|
+
newlinesBetween: 1,
|
|
57
|
+
groups: [
|
|
58
|
+
['builtin', 'external'],
|
|
59
|
+
'internal',
|
|
60
|
+
['parent', 'sibling', 'index'],
|
|
61
|
+
'style',
|
|
62
|
+
'unknown',
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
|
|
67
|
+
// ============================================
|
|
68
|
+
// DISABLED - Conflicts with perfectionist
|
|
69
|
+
// ============================================
|
|
70
|
+
|
|
71
|
+
'sort-keys': 'off',
|
|
72
|
+
'sort-imports': 'off',
|
|
73
|
+
|
|
74
|
+
// ============================================
|
|
75
|
+
// DISABLED - Too strict / opinionated
|
|
76
|
+
// ============================================
|
|
77
|
+
|
|
78
|
+
'func-style': 'off',
|
|
79
|
+
'no-magic-numbers': 'off',
|
|
80
|
+
'no-ternary': 'off',
|
|
81
|
+
'init-declarations': 'off',
|
|
82
|
+
'id-length': 'off',
|
|
83
|
+
'max-statements': 'off',
|
|
84
|
+
'max-params': 'off',
|
|
85
|
+
'no-continue': 'off',
|
|
86
|
+
'arrow-body-style': 'off',
|
|
87
|
+
'prefer-destructuring': 'off',
|
|
88
|
+
'new-cap': 'off',
|
|
89
|
+
'no-await-in-loop': 'off',
|
|
90
|
+
'typescript/consistent-type-definitions': 'off',
|
|
91
|
+
'typescript/array-type': 'off',
|
|
92
|
+
'typescript/no-inferrable-types': 'off',
|
|
93
|
+
'unicorn/no-null': 'off',
|
|
94
|
+
'unicorn/no-array-sort': 'off',
|
|
95
|
+
'import/no-unassigned-import': 'off',
|
|
96
|
+
'import/no-named-default': 'off',
|
|
97
|
+
'import/no-nodejs-modules': 'off',
|
|
98
|
+
|
|
99
|
+
// ============================================
|
|
100
|
+
// ENABLED - Code style enforcement
|
|
101
|
+
// ============================================
|
|
102
|
+
|
|
103
|
+
'capitalized-comments': 'error',
|
|
104
|
+
curly: 'error',
|
|
105
|
+
'no-nested-ternary': 'error',
|
|
106
|
+
'unicorn/catch-error-name': 'error',
|
|
107
|
+
'unicorn/numeric-separators-style': 'error',
|
|
108
|
+
|
|
109
|
+
// ============================================
|
|
110
|
+
// DISABLED - Conflicts with other rules
|
|
111
|
+
// ============================================
|
|
112
|
+
|
|
113
|
+
'import/no-named-export': 'off',
|
|
114
|
+
'import/consistent-type-specifier-style': 'off',
|
|
115
|
+
'import/prefer-default-export': 'off',
|
|
116
|
+
'import/no-default-export': 'off',
|
|
117
|
+
'import/group-exports': 'off',
|
|
118
|
+
'import/no-anonymous-default-export': 'off',
|
|
119
|
+
|
|
120
|
+
// ============================================
|
|
121
|
+
// ENABLED - Clean imports
|
|
122
|
+
// ============================================
|
|
123
|
+
|
|
124
|
+
'import/first': 'error',
|
|
125
|
+
'import/no-namespace': 'error',
|
|
126
|
+
},
|
|
127
|
+
ignorePatterns: ['dist/**', 'node_modules/**'],
|
|
128
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { defineConfig } from 'oxlint';
|
|
3
|
+
|
|
4
|
+
import base from './base.js';
|
|
5
|
+
|
|
6
|
+
const pluginPath = resolve(import.meta.dirname, 'plugins/codestyle.js');
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
extends: [base],
|
|
10
|
+
plugins: ['typescript', 'import', 'react'],
|
|
11
|
+
jsPlugins: [pluginPath],
|
|
12
|
+
rules: {
|
|
13
|
+
'typescript/no-require-imports': [
|
|
14
|
+
'error',
|
|
15
|
+
{
|
|
16
|
+
allow: [
|
|
17
|
+
String.raw`\.png$`,
|
|
18
|
+
String.raw`\.jpg$`,
|
|
19
|
+
String.raw`\.jpeg$`,
|
|
20
|
+
String.raw`\.gif$`,
|
|
21
|
+
String.raw`\.webp$`,
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
'codestyle/imports-without-ext': 'error',
|
|
26
|
+
'react/react-in-jsx-scope': 'off',
|
|
27
|
+
'react/jsx-props-no-spreading': 'off',
|
|
28
|
+
'react/jsx-boolean-value': 'off',
|
|
29
|
+
'react/jsx-handler-names': 'off',
|
|
30
|
+
'react/jsx-curly-brace-presence': 'off',
|
|
31
|
+
'unicorn/no-nested-ternary': 'off',
|
|
32
|
+
},
|
|
33
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { defineConfig } from 'oxlint';
|
|
3
|
+
|
|
4
|
+
import base from './base.js';
|
|
5
|
+
|
|
6
|
+
const pluginPath = resolve(import.meta.dirname, 'plugins/codestyle.js');
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
extends: [base],
|
|
10
|
+
plugins: ['typescript', 'import', 'react', 'nextjs'],
|
|
11
|
+
jsPlugins: [pluginPath],
|
|
12
|
+
ignorePatterns: ['dist/**', 'node_modules/**', '.next/**', 'next-env.d.ts'],
|
|
13
|
+
rules: {
|
|
14
|
+
'codestyle/imports-without-ext': 'error',
|
|
15
|
+
'react/react-in-jsx-scope': 'off',
|
|
16
|
+
'react/jsx-props-no-spreading': 'off',
|
|
17
|
+
'react/jsx-boolean-value': 'off',
|
|
18
|
+
'react/jsx-handler-names': 'off',
|
|
19
|
+
'react/jsx-curly-brace-presence': 'off',
|
|
20
|
+
'react/jsx-max-depth': 'off',
|
|
21
|
+
'unicorn/no-nested-ternary': 'off',
|
|
22
|
+
},
|
|
23
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { defineConfig } from 'oxlint';
|
|
3
|
+
|
|
4
|
+
import base from './base.js';
|
|
5
|
+
|
|
6
|
+
const pluginPath = resolve(import.meta.dirname, 'plugins/codestyle.js');
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
extends: [base],
|
|
10
|
+
jsPlugins: [pluginPath],
|
|
11
|
+
rules: {
|
|
12
|
+
'codestyle/imports-with-ext': 'error',
|
|
13
|
+
},
|
|
14
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// Codestyle plugin - custom rules for code quality
|
|
2
|
+
// Contains: architecture boundaries, import extensions
|
|
3
|
+
|
|
4
|
+
import defaultHexagonalRules from '../architectures/hexagonal-rules.js';
|
|
5
|
+
|
|
6
|
+
// ============================================
|
|
7
|
+
// Hexagonal - Architecture boundary rules
|
|
8
|
+
// ============================================
|
|
9
|
+
|
|
10
|
+
function createHexagonalRule() {
|
|
11
|
+
return {
|
|
12
|
+
meta: {
|
|
13
|
+
type: 'problem',
|
|
14
|
+
docs: {
|
|
15
|
+
description: 'Enforce hexagonal architecture layer boundaries',
|
|
16
|
+
category: 'Best Practices',
|
|
17
|
+
},
|
|
18
|
+
schema: [
|
|
19
|
+
{
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
rules: {
|
|
23
|
+
type: 'array',
|
|
24
|
+
items: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
from: { type: 'string' },
|
|
28
|
+
disallow: { type: 'array', items: { type: 'string' } },
|
|
29
|
+
message: { type: 'string' },
|
|
30
|
+
},
|
|
31
|
+
required: ['from', 'disallow'],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
create(context) {
|
|
39
|
+
const options = context.options[0];
|
|
40
|
+
const rules = options && options.rules ? options.rules : defaultHexagonalRules;
|
|
41
|
+
const filename = context.getFilename();
|
|
42
|
+
|
|
43
|
+
function checkNode(node) {
|
|
44
|
+
if (!node.source || !node.source.value) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const importPath = node.source.value;
|
|
49
|
+
|
|
50
|
+
for (const rule of rules) {
|
|
51
|
+
const fromPattern = new RegExp(rule.from);
|
|
52
|
+
if (!fromPattern.test(filename)) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const disallowPattern of rule.disallow) {
|
|
57
|
+
if (new RegExp(disallowPattern).test(importPath)) {
|
|
58
|
+
context.report({
|
|
59
|
+
node: node.source,
|
|
60
|
+
message: rule.message || 'Import violates architecture boundaries',
|
|
61
|
+
});
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
ImportDeclaration: checkNode,
|
|
70
|
+
ExportNamedDeclaration: checkNode,
|
|
71
|
+
ExportAllDeclaration: checkNode,
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ============================================
|
|
78
|
+
// Imports-with-ext - Require .js extensions
|
|
79
|
+
// ============================================
|
|
80
|
+
|
|
81
|
+
const importsWithExtRule = {
|
|
82
|
+
meta: {
|
|
83
|
+
type: 'problem',
|
|
84
|
+
docs: {
|
|
85
|
+
description: 'Require .js extension in imports for Node.js ESM compatibility',
|
|
86
|
+
category: 'Best Practices',
|
|
87
|
+
},
|
|
88
|
+
fixable: 'code',
|
|
89
|
+
schema: [],
|
|
90
|
+
},
|
|
91
|
+
create(context) {
|
|
92
|
+
const hasExtension = /\.[a-zA-Z0-9]+$/;
|
|
93
|
+
|
|
94
|
+
function checkNode(node) {
|
|
95
|
+
if (!node.source || !node.source.value) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const importPath = node.source.value;
|
|
100
|
+
|
|
101
|
+
// Only check relative imports
|
|
102
|
+
if (!importPath.startsWith('.')) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Skip if it already has an extension
|
|
107
|
+
if (hasExtension.test(importPath)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Skip type-only imports (they are erased at runtime)
|
|
112
|
+
if (node.importKind === 'type') {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
context.report({
|
|
117
|
+
node: node.source,
|
|
118
|
+
message: 'Missing .js extension in import (required for Node.js ESM)',
|
|
119
|
+
fix(fixer) {
|
|
120
|
+
const newPath = `${importPath}.js`;
|
|
121
|
+
return fixer.replaceText(node.source, `'${newPath}'`);
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
ImportDeclaration: checkNode,
|
|
128
|
+
ExportNamedDeclaration: checkNode,
|
|
129
|
+
ExportAllDeclaration: checkNode,
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// ============================================
|
|
135
|
+
// Imports-without-ext - Remove extensions
|
|
136
|
+
// ============================================
|
|
137
|
+
|
|
138
|
+
const importsWithoutExtRule = {
|
|
139
|
+
meta: {
|
|
140
|
+
type: 'problem',
|
|
141
|
+
docs: {
|
|
142
|
+
description: 'Remove .js, .jsx, .ts, .tsx extensions from imports',
|
|
143
|
+
category: 'Best Practices',
|
|
144
|
+
},
|
|
145
|
+
fixable: 'code',
|
|
146
|
+
schema: [],
|
|
147
|
+
},
|
|
148
|
+
create(context) {
|
|
149
|
+
const extensionsToRemove = /\.(?:js|jsx|ts|tsx)$/;
|
|
150
|
+
|
|
151
|
+
function checkNode(node) {
|
|
152
|
+
if (!node.source || !node.source.value) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const importPath = node.source.value;
|
|
157
|
+
|
|
158
|
+
// Check both relative imports and path alias imports
|
|
159
|
+
if (!importPath.startsWith('.') && !importPath.startsWith('@/')) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (extensionsToRemove.test(importPath)) {
|
|
164
|
+
const match = importPath.match(extensionsToRemove);
|
|
165
|
+
context.report({
|
|
166
|
+
node: node.source,
|
|
167
|
+
message: `Remove "${match[0]}" extension from import`,
|
|
168
|
+
fix(fixer) {
|
|
169
|
+
const newPath = importPath.replace(extensionsToRemove, '');
|
|
170
|
+
return fixer.replaceText(node.source, `'${newPath}'`);
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
ImportDeclaration: checkNode,
|
|
178
|
+
ExportNamedDeclaration: checkNode,
|
|
179
|
+
ExportAllDeclaration: checkNode,
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export default {
|
|
185
|
+
meta: {
|
|
186
|
+
name: 'codestyle',
|
|
187
|
+
version: '1.0.0',
|
|
188
|
+
},
|
|
189
|
+
rules: {
|
|
190
|
+
'arch-hexagonal': createHexagonalRule(),
|
|
191
|
+
'imports-with-ext': importsWithExtRule,
|
|
192
|
+
'imports-without-ext': importsWithoutExtRule,
|
|
193
|
+
},
|
|
194
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import oxfmtConfig from '../presets/oxfmt/index.js';
|
|
2
|
+
import hexagonalConfig from '../presets/oxlint/architectures/hexagonal.js';
|
|
3
|
+
import expoConfig from '../presets/oxlint/expo.js';
|
|
4
|
+
import nextConfig from '../presets/oxlint/next.js';
|
|
5
|
+
import nodeConfig from '../presets/oxlint/node.js';
|
|
6
|
+
|
|
7
|
+
export const oxfmt = oxfmtConfig;
|
|
8
|
+
|
|
9
|
+
export const oxlint = {
|
|
10
|
+
expo: expoConfig,
|
|
11
|
+
hexagonal: hexagonalConfig,
|
|
12
|
+
next: nextConfig,
|
|
13
|
+
node: nodeConfig,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default { oxfmt, oxlint };
|
|
File without changes
|