@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 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
- ## Setup
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 # Generate API reference + llms.txt
50
- npx typescript check # Check types, lint, formatting, and unused code
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
- The toolchain is fully compiled — no JavaScript in the hot path:
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)
@@ -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
- [ -s "$tmp_dir/type.log" ] && cat "$tmp_dir/type.log"
154
- [ $type_status -ne 0 ] && printf "${RED}✗ Failed with exit code %d${NC}\n" $type_status || printf "${GREEN}✓ Passed${NC}\n"
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
- [ -s "$tmp_dir/lint.log" ] && cat "$tmp_dir/lint.log"
160
- [ $lint_status -ne 0 ] && printf "${RED}✗ Failed with exit code %d${NC}\n" $lint_status || printf "${GREEN}✓ Passed${NC}\n"
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
- [ -s "$tmp_dir/format.log" ] && cat "$tmp_dir/format.log"
166
- [ $format_status -ne 0 ] && printf "${RED}✗ Failed with exit code %d${NC}\n" $format_status || printf "${GREEN}✓ Passed${NC}\n"
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
- [ -s "$tmp_dir/knip.log" ] && cat "$tmp_dir/knip.log"
171
- [ $knip_status -ne 0 ] && printf "${RED}✗ Failed with exit code %d${NC}\n" $knip_status || printf "${GREEN}✓ Passed${NC}\n"
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
@@ -1,15 +1,35 @@
1
1
  #!/bin/bash
2
- # Generate API reference + llms.txt from TypeScript source.
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
- # Runs typedoc with sensible defaults (no typedoc.json needed),
6
- # then generates llms.txt (index) and llms-full.txt (full reference).
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
- OUT_DIR="$PROJECT_ROOT/.docs"
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
- # ── 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 ""
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
- 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 ""
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
- 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"
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
- printf "${CYAN_BG}${BRIGHT_WHITE} TYPESCRIPT ${NC} Generating API docs...\n\n"
91
+ if [ "${1:-}" = "--check" ]; then
92
+ printf "${CYAN_BG}${BRIGHT_WHITE} TYPESCRIPT ${NC} Checking docs are in sync...\n\n"
92
93
 
93
- bash "$SCRIPT_DIR/commands/docs.sh" "$PROJECT_ROOT" "$PACKAGE_ROOT"
94
+ bash "$SCRIPT_DIR/commands/docs.sh" "$PROJECT_ROOT" "$PACKAGE_ROOT" --check
94
95
 
95
- printf "\n${GREEN}Docs generated at .docs/${NC}\n"
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 API reference + llms.txt from TSDoc\n"
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
@@ -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": "6.1.1",
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": "^8.0.0",
59
+ "@jterrazz/test": "^9.0.0",
56
60
  "@types/node": "^26.1.1",
57
61
  "vitest": "^4.1.10"
58
62
  },
@@ -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
  });
@@ -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],
@@ -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
+ });