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