@jterrazz/typescript 5.2.0 → 6.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/README.md +123 -27
- package/bin/commands/check.sh +164 -0
- package/bin/commands/docs.sh +89 -0
- package/bin/merge-knip-config.js +240 -0
- package/bin/typescript.sh +20 -2
- package/package.json +48 -33
- 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/presets/tsconfig/expo.json +18 -18
- package/presets/tsconfig/next.json +22 -22
- package/presets/tsconfig/node.json +14 -14
- package/presets/tsdown/build.js +10 -10
- package/presets/tsdown/bundle.js +10 -10
- package/src/index.js +16 -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,89 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Generate API reference + llms.txt from TypeScript source.
|
|
3
|
+
# Called by: typescript docs
|
|
4
|
+
#
|
|
5
|
+
# Runs typedoc with sensible defaults (no typedoc.json needed),
|
|
6
|
+
# then generates llms.txt (index) and llms-full.txt (full reference).
|
|
7
|
+
|
|
8
|
+
set -e
|
|
9
|
+
|
|
10
|
+
PROJECT_ROOT="$1"
|
|
11
|
+
PACKAGE_ROOT="$2"
|
|
12
|
+
OUT_DIR="$PROJECT_ROOT/.docs"
|
|
13
|
+
|
|
14
|
+
TYPEDOC=$(
|
|
15
|
+
if [ -x "$PACKAGE_ROOT/node_modules/.bin/typedoc" ]; then
|
|
16
|
+
echo "$PACKAGE_ROOT/node_modules/.bin/typedoc"
|
|
17
|
+
elif [ -x "$PROJECT_ROOT/node_modules/.bin/typedoc" ]; then
|
|
18
|
+
echo "$PROJECT_ROOT/node_modules/.bin/typedoc"
|
|
19
|
+
else
|
|
20
|
+
echo "typedoc"
|
|
21
|
+
fi
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# ── Step 1: Run typedoc ──
|
|
25
|
+
|
|
26
|
+
"$TYPEDOC" \
|
|
27
|
+
--entryPoints "$PROJECT_ROOT/src/index.ts" \
|
|
28
|
+
--plugin typedoc-plugin-markdown \
|
|
29
|
+
--out "$OUT_DIR" \
|
|
30
|
+
--readme none \
|
|
31
|
+
--entryFileName index.md \
|
|
32
|
+
--hideBreadcrumbs true \
|
|
33
|
+
--hidePageHeader true \
|
|
34
|
+
--outputFileStrategy members \
|
|
35
|
+
--useCodeBlocks true \
|
|
36
|
+
--excludeInternal true \
|
|
37
|
+
--excludePrivate true \
|
|
38
|
+
--excludeProtected true \
|
|
39
|
+
--githubPages false \
|
|
40
|
+
--indexFormat table \
|
|
41
|
+
--parametersFormat table \
|
|
42
|
+
--enumMembersFormat table \
|
|
43
|
+
--typeDeclarationFormat table \
|
|
44
|
+
--tsconfig "$PROJECT_ROOT/tsconfig.json"
|
|
45
|
+
|
|
46
|
+
# ── Step 2: Read package name ──
|
|
47
|
+
|
|
48
|
+
PKG_NAME=$(node -e "console.log(require('$PROJECT_ROOT/package.json').name)")
|
|
49
|
+
PKG_DESC=$(node -e "console.log(require('$PROJECT_ROOT/package.json').description || '')")
|
|
50
|
+
|
|
51
|
+
# ── Step 3: Generate llms.txt ──
|
|
52
|
+
|
|
53
|
+
{
|
|
54
|
+
echo "# $PKG_NAME"
|
|
55
|
+
echo ""
|
|
56
|
+
if [ -n "$PKG_DESC" ]; then
|
|
57
|
+
echo "> $PKG_DESC"
|
|
58
|
+
echo ""
|
|
59
|
+
fi
|
|
60
|
+
echo "## API Reference"
|
|
61
|
+
echo ""
|
|
62
|
+
sed -n '/^## /,$ p' "$OUT_DIR/index.md"
|
|
63
|
+
} > "$OUT_DIR/llms.txt"
|
|
64
|
+
|
|
65
|
+
# ── Step 4: Generate llms-full.txt ──
|
|
66
|
+
|
|
67
|
+
{
|
|
68
|
+
echo "# $PKG_NAME — Full API Reference"
|
|
69
|
+
echo ""
|
|
70
|
+
if [ -n "$PKG_DESC" ]; then
|
|
71
|
+
echo "> $PKG_DESC"
|
|
72
|
+
echo ""
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
for f in "$OUT_DIR"/index.md \
|
|
76
|
+
"$OUT_DIR"/functions/*.md \
|
|
77
|
+
"$OUT_DIR"/classes/*.md \
|
|
78
|
+
"$OUT_DIR"/interfaces/*.md \
|
|
79
|
+
"$OUT_DIR"/type-aliases/*.md \
|
|
80
|
+
"$OUT_DIR"/variables/*.md; do
|
|
81
|
+
[ -f "$f" ] || continue
|
|
82
|
+
echo "---"
|
|
83
|
+
echo ""
|
|
84
|
+
cat "$f"
|
|
85
|
+
echo ""
|
|
86
|
+
done
|
|
87
|
+
} > "$OUT_DIR/llms-full.txt"
|
|
88
|
+
|
|
89
|
+
echo "Generated $OUT_DIR/llms.txt and $OUT_DIR/llms-full.txt"
|
|
@@ -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));
|