@jterrazz/typescript 6.1.1 → 7.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 +17 -133
- package/bin/commands/check.sh +109 -10
- package/bin/commands/docs.sh +123 -68
- package/bin/typescript.sh +13 -4
- package/lib/merge-knip-config.js +1 -1
- package/package.json +6 -2
- package/presets/oxfmt/index.js +3 -0
- package/presets/oxlint/base.js +13 -0
- package/src/oxlint.d.ts +15 -0
- package/src/oxlint.js +56 -0
- package/src/oxlint.test.ts +79 -0
package/README.md
CHANGED
|
@@ -8,150 +8,21 @@ The complete TypeScript toolchain — build, run, check, and document with zero
|
|
|
8
8
|
npm install @jterrazz/typescript --save-dev
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
### 1. Choose a TypeScript configuration
|
|
14
|
-
|
|
15
|
-
```json
|
|
16
|
-
// tsconfig.json - Pick one:
|
|
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
|
-
```
|
|
21
|
-
|
|
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
|
|
11
|
+
## The CLI
|
|
43
12
|
|
|
44
13
|
```bash
|
|
45
14
|
npx typescript build # Build application (ESM + types)
|
|
46
15
|
npx typescript bundle # Bundle library (ESM + CJS + types)
|
|
47
16
|
npx typescript start # Run the built application
|
|
48
17
|
npx typescript dev # Build, run, and rebuild on changes
|
|
49
|
-
npx typescript docs #
|
|
50
|
-
npx typescript check #
|
|
18
|
+
npx typescript docs # Compile the committed docs/reference tree from source
|
|
19
|
+
npx typescript check # Type-check, lint, format-check, and unused-code in parallel
|
|
51
20
|
npx typescript fix # Auto-fix lint and formatting issues
|
|
52
21
|
```
|
|
53
22
|
|
|
54
|
-
## Building
|
|
55
|
-
|
|
56
|
-
- **Blazing fast** — Powered by [tsdown](https://tsdown.dev) / [Rolldown](https://rolldown.rs) (Rust)
|
|
57
|
-
- **Zero configuration** — Works out of the box
|
|
58
|
-
- **Multiple outputs** — ESM + CommonJS + TypeScript declarations
|
|
59
|
-
- **Source maps** — Full debugging support
|
|
60
|
-
|
|
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 |
|
|
71
|
-
|
|
72
|
-
### Project structure
|
|
73
|
-
|
|
74
|
-
```
|
|
75
|
-
your-project/
|
|
76
|
-
├── src/
|
|
77
|
-
│ ├── index.ts # Main entry point
|
|
78
|
-
│ └── instrumentation.ts # Optional instrumentation entry point
|
|
79
|
-
├── dist/ # Generated files
|
|
80
|
-
└── tsconfig.json # Extends this package
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
## Quality checks
|
|
84
|
-
|
|
85
|
-
`typescript check` runs four tools in parallel:
|
|
86
|
-
|
|
87
|
-
| Tool | Purpose |
|
|
88
|
-
| ------ | -------------------- |
|
|
89
|
-
| tsc | Type checking |
|
|
90
|
-
| oxlint | Linting |
|
|
91
|
-
| oxfmt | Formatting |
|
|
92
|
-
| knip | Unused code analysis |
|
|
93
|
-
|
|
94
|
-
`typescript fix` runs tsc, 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:
|
|
107
|
-
|
|
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
23
|
## How it works
|
|
153
24
|
|
|
154
|
-
|
|
25
|
+
Fully compiled — no JavaScript in the hot path:
|
|
155
26
|
|
|
156
27
|
| Step | Tool | Language |
|
|
157
28
|
| ------------ | ---------------------------------------------------------------- | -------- |
|
|
@@ -164,6 +35,19 @@ The toolchain is fully compiled — no JavaScript in the hot path:
|
|
|
164
35
|
| Unused code | [Knip](https://knip.dev) | Node |
|
|
165
36
|
| API docs | [Typedoc](https://typedoc.org) | Node |
|
|
166
37
|
|
|
38
|
+
## Documentation
|
|
39
|
+
|
|
40
|
+
The full corpus lives in [`docs/`](docs/):
|
|
41
|
+
|
|
42
|
+
- [Getting started](docs/01-getting-started.md) — install and configure a project.
|
|
43
|
+
- [Building](docs/02-building.md) — `build`, `bundle`, `start`, `dev`.
|
|
44
|
+
- [Quality checks](docs/03-quality-checks.md) — `check` / `fix` and their passes.
|
|
45
|
+
- [Lint presets](docs/04-lint-presets.md) — oxlint presets, `compose`, architecture, knip.
|
|
46
|
+
- [Docs pipeline](docs/05-docs-pipeline.md) — the `typescript docs` compiler.
|
|
47
|
+
- [Repo structure](docs/06-repo-structure.md) — the corpus / injection / compiler doctrine.
|
|
48
|
+
|
|
49
|
+
For agents: read the chapters and the generated [`docs/reference/`](docs/reference/) tree straight from the repo, plus two Claude Code skills — [`skills/jterrazz-typescript`](skills/jterrazz-typescript/SKILL.md) (the toolchain) and [`skills/jterrazz-repo-structure`](skills/jterrazz-repo-structure/SKILL.md) (the repo doctrine).
|
|
50
|
+
|
|
167
51
|
## License
|
|
168
52
|
|
|
169
53
|
MIT © [Jean-Baptiste Terrazzoni](https://github.com/jterrazz)
|
package/bin/commands/check.sh
CHANGED
|
@@ -66,6 +66,41 @@ TSC=$(find_tsc)
|
|
|
66
66
|
OXLINT=$(find_binary oxlint)
|
|
67
67
|
OXFMT=$(find_binary oxfmt)
|
|
68
68
|
KNIP=$(find_binary knip)
|
|
69
|
+
CHECKER=$(find_binary jterrazz-test-check)
|
|
70
|
+
|
|
71
|
+
# The @jterrazz/test conventions checker (D4 tokens, C8/C9 fixtures) runs only when the
|
|
72
|
+
# consuming project depends on @jterrazz/test — auto-detected from its package.json.
|
|
73
|
+
project_uses_jterrazz_test() {
|
|
74
|
+
[ -f "package.json" ] || return 1
|
|
75
|
+
node -e 'const p=require("./package.json");const d={...p.dependencies,...p.devDependencies,...p.peerDependencies};process.exit(d["@jterrazz/test"]?0:1)' 2>/dev/null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# The @jterrazz/test oxlint plugin is ESM-only. A CommonJS oxlint config silently drops
|
|
79
|
+
# it (oxlint prints a load warning and still exits 0) — none of the jterrazz/* rules run.
|
|
80
|
+
# Warn loudly when that pitfall is detectable.
|
|
81
|
+
warn_cjs_oxlint_config() {
|
|
82
|
+
local cfg=""
|
|
83
|
+
for c in oxlint.config.ts oxlint.config.mjs oxlint.config.cjs oxlint.config.js; do
|
|
84
|
+
[ -f "$c" ] && { cfg="$c"; break; }
|
|
85
|
+
done
|
|
86
|
+
[ -z "$cfg" ] && return 0
|
|
87
|
+
|
|
88
|
+
local is_cjs=false
|
|
89
|
+
case "$cfg" in
|
|
90
|
+
*.cjs) is_cjs=true ;;
|
|
91
|
+
*.js)
|
|
92
|
+
if ! node -e 'process.exit(require("./package.json").type==="module"?0:1)' 2>/dev/null; then
|
|
93
|
+
is_cjs=true
|
|
94
|
+
fi
|
|
95
|
+
;;
|
|
96
|
+
esac
|
|
97
|
+
|
|
98
|
+
if [ "$is_cjs" = true ]; then
|
|
99
|
+
printf "${RED} WARNING ${NC} @jterrazz/test is installed but %s is CommonJS.\n" "$cfg"
|
|
100
|
+
printf " The @jterrazz/test oxlint plugin is ESM-only and will be SILENTLY DROPPED —\n"
|
|
101
|
+
printf " none of the jterrazz/* rules will run. Switch to an ESM config (oxlint.config.ts or .mjs).\n\n"
|
|
102
|
+
fi
|
|
103
|
+
}
|
|
69
104
|
|
|
70
105
|
# Parse command and args
|
|
71
106
|
COMMAND=""
|
|
@@ -109,6 +144,10 @@ run_checks() {
|
|
|
109
144
|
|
|
110
145
|
printf "${CYAN_BG}${BRIGHT_WHITE} START ${NC} ${LABEL}\n"
|
|
111
146
|
|
|
147
|
+
if project_uses_jterrazz_test; then
|
|
148
|
+
warn_cjs_oxlint_config
|
|
149
|
+
fi
|
|
150
|
+
|
|
112
151
|
# Run all tools in parallel
|
|
113
152
|
"$TSC" --noEmit > "$tmp_dir/type.log" 2>&1 &
|
|
114
153
|
local type_pid=$!
|
|
@@ -142,33 +181,93 @@ run_checks() {
|
|
|
142
181
|
knip_pid=$!
|
|
143
182
|
fi
|
|
144
183
|
|
|
184
|
+
# Conventions checker: only in check mode, only when the project uses @jterrazz/test
|
|
185
|
+
# and has a specs/ directory to validate.
|
|
186
|
+
local checker_pid=""
|
|
187
|
+
local checker_status=0
|
|
188
|
+
if [ "$FIX_MODE" = false ] && [ -d "specs" ] && project_uses_jterrazz_test; then
|
|
189
|
+
"$CHECKER" specs > "$tmp_dir/checker.log" 2>&1 &
|
|
190
|
+
checker_pid=$!
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
# Docs (sync): only in check mode, and only once the project has generated
|
|
194
|
+
# its committed docs (docs/reference/ exists — opt-in by first generation).
|
|
195
|
+
# Delegates to docs.sh --check: regenerate into a temp dir, diff the
|
|
196
|
+
# committed projections. Never duplicates the compiler's logic.
|
|
197
|
+
local docs_pid=""
|
|
198
|
+
local docs_status=0
|
|
199
|
+
if [ "$FIX_MODE" = false ] && [ -d "docs/reference" ]; then
|
|
200
|
+
bash "$SCRIPT_DIR/docs.sh" "$(pwd)" "$PACKAGE_ROOT" --check > "$tmp_dir/docs.log" 2>&1 &
|
|
201
|
+
docs_pid=$!
|
|
202
|
+
fi
|
|
203
|
+
|
|
145
204
|
# Wait and collect statuses
|
|
146
205
|
wait $type_pid; local type_status=$?
|
|
147
206
|
wait $lint_pid; local lint_status=$?
|
|
148
207
|
wait $format_pid; local format_status=$?
|
|
149
208
|
[ -n "$knip_pid" ] && { wait $knip_pid; knip_status=$?; }
|
|
209
|
+
[ -n "$checker_pid" ] && { wait $checker_pid; checker_status=$?; }
|
|
210
|
+
[ -n "$docs_pid" ] && { wait $docs_pid; docs_status=$?; }
|
|
150
211
|
|
|
151
|
-
# Print results
|
|
212
|
+
# Print results — quiet on success, verbose on failure: a tool's captured log
|
|
213
|
+
# is shown only when it failed, so green output stays byte-identical across
|
|
214
|
+
# platforms (some tool builds print success chatter on Linux but not macOS).
|
|
152
215
|
printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} TypeScript Check\n\n"
|
|
153
|
-
[ -
|
|
154
|
-
|
|
216
|
+
if [ $type_status -ne 0 ]; then
|
|
217
|
+
[ -s "$tmp_dir/type.log" ] && cat "$tmp_dir/type.log"
|
|
218
|
+
printf "${RED}✗ Failed with exit code %d${NC}\n" $type_status
|
|
219
|
+
else
|
|
220
|
+
printf "${GREEN}✓ Passed${NC}\n"
|
|
221
|
+
fi
|
|
155
222
|
|
|
156
223
|
local lint_label="Oxlint Check"
|
|
157
224
|
[ "$FIX_MODE" = true ] && lint_label="Oxlint Fix"
|
|
158
225
|
printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} ${lint_label}\n\n"
|
|
159
|
-
[ -
|
|
160
|
-
|
|
226
|
+
if [ $lint_status -ne 0 ]; then
|
|
227
|
+
[ -s "$tmp_dir/lint.log" ] && cat "$tmp_dir/lint.log"
|
|
228
|
+
printf "${RED}✗ Failed with exit code %d${NC}\n" $lint_status
|
|
229
|
+
else
|
|
230
|
+
printf "${GREEN}✓ Passed${NC}\n"
|
|
231
|
+
fi
|
|
161
232
|
|
|
162
233
|
local format_label="Oxfmt Check"
|
|
163
234
|
[ "$FIX_MODE" = true ] && format_label="Oxfmt Format"
|
|
164
235
|
printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} ${format_label}\n\n"
|
|
165
|
-
[ -
|
|
166
|
-
|
|
236
|
+
if [ $format_status -ne 0 ]; then
|
|
237
|
+
[ -s "$tmp_dir/format.log" ] && cat "$tmp_dir/format.log"
|
|
238
|
+
printf "${RED}✗ Failed with exit code %d${NC}\n" $format_status
|
|
239
|
+
else
|
|
240
|
+
printf "${GREEN}✓ Passed${NC}\n"
|
|
241
|
+
fi
|
|
167
242
|
|
|
168
243
|
if [ "$FIX_MODE" = false ]; then
|
|
169
244
|
printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} Knip (unused code)\n\n"
|
|
170
|
-
[ -
|
|
171
|
-
|
|
245
|
+
if [ $knip_status -ne 0 ]; then
|
|
246
|
+
[ -s "$tmp_dir/knip.log" ] && cat "$tmp_dir/knip.log"
|
|
247
|
+
printf "${RED}✗ Failed with exit code %d${NC}\n" $knip_status
|
|
248
|
+
else
|
|
249
|
+
printf "${GREEN}✓ Passed${NC}\n"
|
|
250
|
+
fi
|
|
251
|
+
|
|
252
|
+
if [ -n "$checker_pid" ]; then
|
|
253
|
+
printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} Test Conventions (@jterrazz/test)\n\n"
|
|
254
|
+
if [ $checker_status -ne 0 ]; then
|
|
255
|
+
[ -s "$tmp_dir/checker.log" ] && cat "$tmp_dir/checker.log"
|
|
256
|
+
printf "${RED}✗ Failed with exit code %d${NC}\n" $checker_status
|
|
257
|
+
else
|
|
258
|
+
printf "${GREEN}✓ Passed${NC}\n"
|
|
259
|
+
fi
|
|
260
|
+
fi
|
|
261
|
+
|
|
262
|
+
if [ -n "$docs_pid" ]; then
|
|
263
|
+
printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} Docs (sync)\n\n"
|
|
264
|
+
if [ $docs_status -ne 0 ]; then
|
|
265
|
+
[ -s "$tmp_dir/docs.log" ] && cat "$tmp_dir/docs.log"
|
|
266
|
+
printf "${RED}✗ Failed with exit code %d${NC}\n" $docs_status
|
|
267
|
+
else
|
|
268
|
+
printf "${GREEN}✓ Passed${NC}\n"
|
|
269
|
+
fi
|
|
270
|
+
fi
|
|
172
271
|
fi
|
|
173
272
|
|
|
174
273
|
# Summary
|
|
@@ -178,7 +277,7 @@ run_checks() {
|
|
|
178
277
|
printf "\n${CYAN_BG}${BRIGHT_WHITE} END ${NC} Finalizing quality checks\n\n"
|
|
179
278
|
fi
|
|
180
279
|
|
|
181
|
-
if [ $type_status -eq 0 ] && [ $lint_status -eq 0 ] && [ $format_status -eq 0 ] && [ $knip_status -eq 0 ]; then
|
|
280
|
+
if [ $type_status -eq 0 ] && [ $lint_status -eq 0 ] && [ $format_status -eq 0 ] && [ $knip_status -eq 0 ] && [ $checker_status -eq 0 ] && [ $docs_status -eq 0 ]; then
|
|
182
281
|
printf "${GREEN}✓ All checks passed${NC}\n"
|
|
183
282
|
exit 0
|
|
184
283
|
else
|
package/bin/commands/docs.sh
CHANGED
|
@@ -1,15 +1,35 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
#
|
|
3
|
-
# Called by: typescript docs
|
|
2
|
+
# Compile the committed docs projection from TypeScript source.
|
|
3
|
+
# Called by: typescript docs [--check] (via bin/typescript.sh) and the
|
|
4
|
+
# Docs (sync) pass in bin/commands/check.sh.
|
|
4
5
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
6
|
+
# Projection (COMMITTED, sync-checked) — a single cross-layer compilation of
|
|
7
|
+
# source into docs:
|
|
8
|
+
# docs/reference/ typedoc markdown tree (stale members vanish on regen)
|
|
9
|
+
#
|
|
10
|
+
# Every generated file self-identifies with a first-line header. Generation is
|
|
11
|
+
# deterministic: LC_ALL=C sorts, explicit find | sort, no timestamps — two runs
|
|
12
|
+
# on an unchanged repo are byte-identical (E2E-tested). `--check` regenerates
|
|
13
|
+
# into a temp dir and diffs against the committed reference tree (never touching
|
|
14
|
+
# the committed tree), so it is safe to run in parallel from `typescript check`.
|
|
7
15
|
|
|
8
16
|
set -e
|
|
9
17
|
|
|
18
|
+
RED='\033[0;31m'
|
|
19
|
+
GREEN='\033[0;32m'
|
|
20
|
+
NC='\033[0m'
|
|
21
|
+
|
|
10
22
|
PROJECT_ROOT="$1"
|
|
11
23
|
PACKAGE_ROOT="$2"
|
|
12
|
-
|
|
24
|
+
MODE="${3:-generate}" # "generate" | "--check"
|
|
25
|
+
|
|
26
|
+
# Header stamped as the first line of every generated file. It is an HTML
|
|
27
|
+
# comment: invisible when the markdown renders, and uniform across the whole
|
|
28
|
+
# reference tree.
|
|
29
|
+
GEN_HEADER="<!-- GENERATED by \`typescript docs\` — DO NOT EDIT -->"
|
|
30
|
+
|
|
31
|
+
# Byte-identical output regardless of the caller's locale.
|
|
32
|
+
export LC_ALL=C
|
|
13
33
|
|
|
14
34
|
TYPEDOC=$(
|
|
15
35
|
if [ -x "$PACKAGE_ROOT/node_modules/.bin/typedoc" ]; then
|
|
@@ -21,69 +41,104 @@ TYPEDOC=$(
|
|
|
21
41
|
fi
|
|
22
42
|
)
|
|
23
43
|
|
|
24
|
-
#
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
--
|
|
38
|
-
|
|
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 ""
|
|
44
|
+
# Entry barrel: src/index.ts for source projects, or src/index.d.ts for
|
|
45
|
+
# JS-shipped packages that carry hand-written declarations (this toolchain
|
|
46
|
+
# itself is one).
|
|
47
|
+
ENTRY=""
|
|
48
|
+
if [ -f "$PROJECT_ROOT/src/index.ts" ]; then
|
|
49
|
+
ENTRY="$PROJECT_ROOT/src/index.ts"
|
|
50
|
+
elif [ -f "$PROJECT_ROOT/src/index.d.ts" ]; then
|
|
51
|
+
ENTRY="$PROJECT_ROOT/src/index.d.ts"
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Guard: nothing to compile without an entry barrel AND a docs/ corpus. In
|
|
55
|
+
# --check mode a project that owns no docs is trivially in sync (exit 0).
|
|
56
|
+
if [ -z "$ENTRY" ] || [ ! -d "$PROJECT_ROOT/docs" ]; then
|
|
57
|
+
if [ "$MODE" = "--check" ]; then
|
|
58
|
+
exit 0
|
|
59
59
|
fi
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
{
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
60
|
+
printf "${RED}Error: 'typescript docs' needs an entry barrel and a docs/ directory.${NC}\n" >&2
|
|
61
|
+
[ -z "$ENTRY" ] && printf " No src/index.ts or src/index.d.ts found.\n" >&2
|
|
62
|
+
[ ! -d "$PROJECT_ROOT/docs" ] && printf " No docs/ directory found — create it with your hand-written chapters.\n" >&2
|
|
63
|
+
exit 1
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# Prepend the generated-file header as the first line of a file.
|
|
67
|
+
prepend_header() {
|
|
68
|
+
local file="$1"
|
|
69
|
+
local tmp
|
|
70
|
+
tmp="$(mktemp)"
|
|
71
|
+
{
|
|
72
|
+
printf '%s\n' "$GEN_HEADER"
|
|
73
|
+
cat "$file"
|
|
74
|
+
} > "$tmp"
|
|
75
|
+
mv "$tmp" "$file"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Generate the reference projection into <out_base>:
|
|
79
|
+
# <out_base>/reference/ typedoc tree
|
|
80
|
+
generate_docs() {
|
|
81
|
+
local OUT_BASE="$1"
|
|
82
|
+
local REF="$OUT_BASE/reference"
|
|
83
|
+
|
|
84
|
+
mkdir -p "$OUT_BASE"
|
|
85
|
+
|
|
86
|
+
# Step 1 — typedoc markdown tree. cleanOutputDir (default) wipes the target
|
|
87
|
+
# first, so members removed from the source vanish from the committed tree.
|
|
88
|
+
# --gitRevision main pins source links to the branch, not a commit SHA:
|
|
89
|
+
# a SHA link would name the commit BEFORE the one carrying the regenerated
|
|
90
|
+
# docs, so every docs-carrying commit would invalidate its own projection
|
|
91
|
+
# (bootstrap paradox — the sync check could never be green at HEAD).
|
|
92
|
+
"$TYPEDOC" \
|
|
93
|
+
--entryPoints "$ENTRY" \
|
|
94
|
+
--plugin typedoc-plugin-markdown \
|
|
95
|
+
--out "$REF" \
|
|
96
|
+
--gitRevision main \
|
|
97
|
+
--readme none \
|
|
98
|
+
--entryFileName index.md \
|
|
99
|
+
--hideBreadcrumbs true \
|
|
100
|
+
--hidePageHeader true \
|
|
101
|
+
--outputFileStrategy members \
|
|
102
|
+
--useCodeBlocks true \
|
|
103
|
+
--excludeInternal true \
|
|
104
|
+
--excludePrivate true \
|
|
105
|
+
--excludeProtected true \
|
|
106
|
+
--githubPages false \
|
|
107
|
+
--indexFormat table \
|
|
108
|
+
--parametersFormat table \
|
|
109
|
+
--enumMembersFormat table \
|
|
110
|
+
--typeDeclarationFormat table \
|
|
111
|
+
--tsconfig "$PROJECT_ROOT/tsconfig.json"
|
|
112
|
+
|
|
113
|
+
# Step 2 — stamp every reference markdown file with the generated header.
|
|
114
|
+
local f
|
|
115
|
+
while IFS= read -r f; do
|
|
116
|
+
prepend_header "$f"
|
|
117
|
+
done < <(find "$REF" -name '*.md' | LC_ALL=C sort)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# ── --check: regenerate into a temp dir, diff against the committed tree ──
|
|
121
|
+
if [ "$MODE" = "--check" ]; then
|
|
122
|
+
TMP_BASE=$(mktemp -d)
|
|
123
|
+
trap 'rm -rf "$TMP_BASE"' EXIT
|
|
124
|
+
|
|
125
|
+
# Typedoc chatter is noise here — the diff is the signal.
|
|
126
|
+
generate_docs "$TMP_BASE" > /dev/null 2>&1
|
|
127
|
+
|
|
128
|
+
OUT_OF_SYNC=()
|
|
129
|
+
diff -r -q "$TMP_BASE/reference" "$PROJECT_ROOT/docs/reference" > /dev/null 2>&1 || OUT_OF_SYNC+=("docs/reference/")
|
|
130
|
+
|
|
131
|
+
if [ ${#OUT_OF_SYNC[@]} -gt 0 ]; then
|
|
132
|
+
printf "${RED}Docs are out of sync:${NC}\n"
|
|
133
|
+
for p in "${OUT_OF_SYNC[@]}"; do
|
|
134
|
+
printf " ${RED}✗${NC} %s\n" "$p"
|
|
135
|
+
done
|
|
136
|
+
printf "Run 'typescript docs' to regenerate.\n"
|
|
137
|
+
exit 1
|
|
73
138
|
fi
|
|
139
|
+
exit 0
|
|
140
|
+
fi
|
|
74
141
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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"
|
|
142
|
+
# ── generate: write the committed projection in place ──
|
|
143
|
+
generate_docs "$PROJECT_ROOT/docs"
|
|
144
|
+
echo "Generated docs/reference/"
|
package/bin/typescript.sh
CHANGED
|
@@ -88,11 +88,19 @@ case "$COMMAND" in
|
|
|
88
88
|
;;
|
|
89
89
|
|
|
90
90
|
docs)
|
|
91
|
-
|
|
91
|
+
if [ "${1:-}" = "--check" ]; then
|
|
92
|
+
printf "${CYAN_BG}${BRIGHT_WHITE} TYPESCRIPT ${NC} Checking docs are in sync...\n\n"
|
|
92
93
|
|
|
93
|
-
|
|
94
|
+
bash "$SCRIPT_DIR/commands/docs.sh" "$PROJECT_ROOT" "$PACKAGE_ROOT" --check
|
|
94
95
|
|
|
95
|
-
|
|
96
|
+
printf "${GREEN}Docs are in sync${NC}\n"
|
|
97
|
+
else
|
|
98
|
+
printf "${CYAN_BG}${BRIGHT_WHITE} TYPESCRIPT ${NC} Generating API docs...\n\n"
|
|
99
|
+
|
|
100
|
+
bash "$SCRIPT_DIR/commands/docs.sh" "$PROJECT_ROOT" "$PACKAGE_ROOT"
|
|
101
|
+
|
|
102
|
+
printf "\n${GREEN}Docs generated at docs/${NC}\n"
|
|
103
|
+
fi
|
|
96
104
|
;;
|
|
97
105
|
|
|
98
106
|
check|fix)
|
|
@@ -107,7 +115,7 @@ case "$COMMAND" in
|
|
|
107
115
|
printf " bundle Bundle library (ESM + CJS + types)\n"
|
|
108
116
|
printf " start Run the built application\n"
|
|
109
117
|
printf " dev Build, run, and rebuild on changes\n"
|
|
110
|
-
printf " docs Generate
|
|
118
|
+
printf " docs Generate the committed docs/reference tree; --check verifies sync\n"
|
|
111
119
|
printf " check Check types, lint, formatting, and unused code\n"
|
|
112
120
|
printf " fix Auto-fix lint and formatting issues\n\n"
|
|
113
121
|
printf "Examples:\n"
|
|
@@ -116,6 +124,7 @@ case "$COMMAND" in
|
|
|
116
124
|
printf " typescript start\n"
|
|
117
125
|
printf " typescript dev\n"
|
|
118
126
|
printf " typescript docs\n"
|
|
127
|
+
printf " typescript docs --check\n"
|
|
119
128
|
printf " typescript check\n"
|
|
120
129
|
printf " typescript fix\n"
|
|
121
130
|
exit 1
|
package/lib/merge-knip-config.js
CHANGED
|
@@ -75,7 +75,7 @@ if (existsSync('docs')) {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
// Scan for fixtures/** and expected/** directories anywhere in the tree (up to 3 levels)
|
|
78
|
-
const scanDirs = ['tests', 'test', 'src'];
|
|
78
|
+
const scanDirs = ['specs', 'tests', 'test', 'src'];
|
|
79
79
|
for (const root of scanDirs) {
|
|
80
80
|
if (!existsSync(root)) {
|
|
81
81
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jterrazz/typescript",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"author": "Jean-Baptiste Terrazzoni <contact@jterrazz.com>",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
"types": "./src/index.d.ts",
|
|
24
24
|
"default": "./src/index.js"
|
|
25
25
|
},
|
|
26
|
+
"./oxlint": {
|
|
27
|
+
"types": "./src/oxlint.d.ts",
|
|
28
|
+
"default": "./src/oxlint.js"
|
|
29
|
+
},
|
|
26
30
|
"./tsconfig/*": "./presets/tsconfig/*.json",
|
|
27
31
|
"./tsconfig/*.json": "./presets/tsconfig/*.json",
|
|
28
32
|
"./tsdown/*": "./presets/tsdown/*.js",
|
|
@@ -52,7 +56,7 @@
|
|
|
52
56
|
"typescript": "^6.0.0"
|
|
53
57
|
},
|
|
54
58
|
"devDependencies": {
|
|
55
|
-
"@jterrazz/test": "^
|
|
59
|
+
"@jterrazz/test": "^9.0.0",
|
|
56
60
|
"@types/node": "^26.1.1",
|
|
57
61
|
"vitest": "^4.1.10"
|
|
58
62
|
},
|
package/presets/oxfmt/index.js
CHANGED
|
@@ -9,4 +9,7 @@ export default defineConfig({
|
|
|
9
9
|
trailingComma: 'all',
|
|
10
10
|
bracketSpacing: true,
|
|
11
11
|
endOfLine: 'lf',
|
|
12
|
+
/* `typescript docs` writes the byte-for-byte typedoc tree to docs/reference/ —
|
|
13
|
+
* formatting it would fight the Docs (sync) pass, so skip it by default. */
|
|
14
|
+
ignorePatterns: ['docs/reference'],
|
|
12
15
|
});
|
package/presets/oxlint/base.js
CHANGED
|
@@ -4,6 +4,19 @@ import { defineConfig } from 'oxlint';
|
|
|
4
4
|
const require = createRequire(import.meta.url);
|
|
5
5
|
const perfectionistPath = require.resolve('eslint-plugin-perfectionist');
|
|
6
6
|
|
|
7
|
+
/*
|
|
8
|
+
* Base lint preset — tooling rules only. Wiring is EXPLICIT: this preset never
|
|
9
|
+
* auto-detects other jterrazz packages. A project using @jterrazz/test composes
|
|
10
|
+
* its `testing` fragment itself:
|
|
11
|
+
*
|
|
12
|
+
* import { testing } from '@jterrazz/test/oxlint';
|
|
13
|
+
* import { compose, node } from '@jterrazz/typescript/oxlint';
|
|
14
|
+
*
|
|
15
|
+
* export default compose(node, testing);
|
|
16
|
+
*
|
|
17
|
+
* (The @jterrazz/test plugin is ESM-only: a CommonJS oxlint config silently
|
|
18
|
+
* drops it — `typescript check` still warns loudly about that pitfall.)
|
|
19
|
+
*/
|
|
7
20
|
export default defineConfig({
|
|
8
21
|
plugins: ['typescript', 'import', 'oxc', 'unicorn'],
|
|
9
22
|
jsPlugins: [perfectionistPath],
|
package/src/oxlint.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type ConfigObject = Record<string, unknown>;
|
|
2
|
+
|
|
3
|
+
declare const expo: ConfigObject;
|
|
4
|
+
declare const hexagonal: ConfigObject;
|
|
5
|
+
declare const next: ConfigObject;
|
|
6
|
+
declare const node: ConfigObject;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Deterministic merge of oxlint config fragments, left to right:
|
|
10
|
+
* jsPlugins/plugins/ignorePatterns/extends concatenated + deduped,
|
|
11
|
+
* rules/categories shallow-merged (last wins), overrides concatenated.
|
|
12
|
+
*/
|
|
13
|
+
declare function compose(...fragments: ConfigObject[]): ConfigObject;
|
|
14
|
+
|
|
15
|
+
export { compose, expo, hexagonal, next, node };
|
package/src/oxlint.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The tool-facing oxlint entry (`@jterrazz/typescript/oxlint`): the named
|
|
3
|
+
* presets plus the `compose()` helper. Wiring is EXPLICIT — a consumer
|
|
4
|
+
* composes exactly the fragments it wants, nothing is auto-detected:
|
|
5
|
+
*
|
|
6
|
+
* import { testing } from '@jterrazz/test/oxlint';
|
|
7
|
+
* import { compose, node } from '@jterrazz/typescript/oxlint';
|
|
8
|
+
*
|
|
9
|
+
* export default compose(node, testing);
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Config keys concatenated across fragments, duplicates dropped (===). */
|
|
13
|
+
const CONCAT_DEDUPE = new Set(['extends', 'ignorePatterns', 'jsPlugins', 'plugins']);
|
|
14
|
+
/** Config keys concatenated verbatim (order matters, no dedupe). */
|
|
15
|
+
const CONCAT = new Set(['overrides']);
|
|
16
|
+
/** Config keys shallow-merged as objects — the LAST fragment wins per key. */
|
|
17
|
+
const SHALLOW_MERGE = new Set(['categories', 'env', 'globals', 'rules', 'settings']);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Deterministic merge of oxlint config fragments, left to right:
|
|
21
|
+
* `jsPlugins` / `plugins` / `ignorePatterns` / `extends` are concatenated and
|
|
22
|
+
* deduped, `rules` / `categories` (and env/globals/settings) are shallow-merged
|
|
23
|
+
* with last-wins per key, `overrides` are concatenated, and any other key is
|
|
24
|
+
* taken from the last fragment that sets it.
|
|
25
|
+
*/
|
|
26
|
+
export function compose(...fragments) {
|
|
27
|
+
const merged = {};
|
|
28
|
+
for (const fragment of fragments) {
|
|
29
|
+
if (!fragment || typeof fragment !== 'object') {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
for (const [key, value] of Object.entries(fragment)) {
|
|
33
|
+
if (value === undefined) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (CONCAT_DEDUPE.has(key)) {
|
|
37
|
+
const previous = Array.isArray(merged[key]) ? merged[key] : [];
|
|
38
|
+
const combined = [...previous, ...(Array.isArray(value) ? value : [value])];
|
|
39
|
+
merged[key] = combined.filter((entry, index) => combined.indexOf(entry) === index);
|
|
40
|
+
} else if (CONCAT.has(key)) {
|
|
41
|
+
const previous = Array.isArray(merged[key]) ? merged[key] : [];
|
|
42
|
+
merged[key] = [...previous, ...(Array.isArray(value) ? value : [value])];
|
|
43
|
+
} else if (SHALLOW_MERGE.has(key)) {
|
|
44
|
+
merged[key] = { ...merged[key], ...value };
|
|
45
|
+
} else {
|
|
46
|
+
merged[key] = value;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return merged;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { default as hexagonal } from '../presets/oxlint/architectures/hexagonal.js';
|
|
54
|
+
export { default as expo } from '../presets/oxlint/expo.js';
|
|
55
|
+
export { default as next } from '../presets/oxlint/next.js';
|
|
56
|
+
export { default as node } from '../presets/oxlint/node.js';
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { expect, test } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { compose, expo, hexagonal, next, node } from './oxlint.js';
|
|
4
|
+
|
|
5
|
+
test('concatenates and dedupes plugin lists', () => {
|
|
6
|
+
// Given - two fragments sharing one jsPlugin
|
|
7
|
+
const merged = compose(
|
|
8
|
+
{ jsPlugins: ['a', 'b'], plugins: ['typescript'] },
|
|
9
|
+
{ jsPlugins: ['b', 'c'], plugins: ['typescript', 'import'] },
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
// Then - order preserved, duplicates dropped
|
|
13
|
+
expect(merged.jsPlugins).toEqual(['a', 'b', 'c']);
|
|
14
|
+
expect(merged.plugins).toEqual(['typescript', 'import']);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('shallow-merges rules with last fragment winning', () => {
|
|
18
|
+
// Given - two fragments disagreeing on one rule
|
|
19
|
+
const merged = compose(
|
|
20
|
+
{ rules: { curly: 'error', 'no-ternary': 'off' } },
|
|
21
|
+
{ rules: { curly: 'off', 'jterrazz/b4-given-then': 'error' } },
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// Then - the later fragment wins per key, others survive
|
|
25
|
+
expect(merged.rules).toEqual({
|
|
26
|
+
curly: 'off',
|
|
27
|
+
'jterrazz/b4-given-then': 'error',
|
|
28
|
+
'no-ternary': 'off',
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('concatenates overrides in order without deduping', () => {
|
|
33
|
+
// Given - two fragments each shipping an override
|
|
34
|
+
const first = { files: ['**/*.specification.ts'], rules: {} };
|
|
35
|
+
const second = { files: ['src/**'], rules: {} };
|
|
36
|
+
const merged = compose({ overrides: [first] }, { overrides: [second] });
|
|
37
|
+
|
|
38
|
+
// Then - both overrides survive, in composition order
|
|
39
|
+
expect(merged.overrides).toEqual([first, second]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('concatenates and dedupes ignorePatterns', () => {
|
|
43
|
+
// Given - overlapping ignore lists
|
|
44
|
+
const merged = compose(
|
|
45
|
+
{ ignorePatterns: ['dist/**', 'node_modules/**'] },
|
|
46
|
+
{ ignorePatterns: ['node_modules/**', '**/fixtures/**'] },
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Then - one entry each
|
|
50
|
+
expect(merged.ignorePatterns).toEqual(['dist/**', 'node_modules/**', '**/fixtures/**']);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('takes unknown scalar keys from the last fragment', () => {
|
|
54
|
+
// Given - fragments disagreeing on a scalar key
|
|
55
|
+
const merged = compose({ somethingElse: 1 }, { somethingElse: 2 });
|
|
56
|
+
|
|
57
|
+
// Then - last wins
|
|
58
|
+
expect(merged.somethingElse).toBe(2);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('ignores null and undefined fragments', () => {
|
|
62
|
+
// Given - a composition with holes (conditional fragments)
|
|
63
|
+
const merged = compose(
|
|
64
|
+
undefined as unknown as Record<string, unknown>,
|
|
65
|
+
{ rules: { curly: 'error' } },
|
|
66
|
+
null as unknown as Record<string, unknown>,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Then - the holes contribute nothing
|
|
70
|
+
expect(merged.rules).toEqual({ curly: 'error' });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('exports the named presets', () => {
|
|
74
|
+
// Given - the tool-facing entry
|
|
75
|
+
// Then - every preset is a config object
|
|
76
|
+
for (const preset of [node, expo, next, hexagonal]) {
|
|
77
|
+
expect(typeof preset).toBe('object');
|
|
78
|
+
}
|
|
79
|
+
});
|