@jterrazz/typescript 6.1.1 → 6.2.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/bin/commands/check.sh +87 -10
- package/lib/merge-knip-config.js +1 -1
- package/package.json +6 -2
- 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/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,71 @@ 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
|
+
|
|
145
193
|
# Wait and collect statuses
|
|
146
194
|
wait $type_pid; local type_status=$?
|
|
147
195
|
wait $lint_pid; local lint_status=$?
|
|
148
196
|
wait $format_pid; local format_status=$?
|
|
149
197
|
[ -n "$knip_pid" ] && { wait $knip_pid; knip_status=$?; }
|
|
198
|
+
[ -n "$checker_pid" ] && { wait $checker_pid; checker_status=$?; }
|
|
150
199
|
|
|
151
|
-
# Print results
|
|
200
|
+
# Print results — quiet on success, verbose on failure: a tool's captured log
|
|
201
|
+
# is shown only when it failed, so green output stays byte-identical across
|
|
202
|
+
# platforms (some tool builds print success chatter on Linux but not macOS).
|
|
152
203
|
printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} TypeScript Check\n\n"
|
|
153
|
-
[ -
|
|
154
|
-
|
|
204
|
+
if [ $type_status -ne 0 ]; then
|
|
205
|
+
[ -s "$tmp_dir/type.log" ] && cat "$tmp_dir/type.log"
|
|
206
|
+
printf "${RED}✗ Failed with exit code %d${NC}\n" $type_status
|
|
207
|
+
else
|
|
208
|
+
printf "${GREEN}✓ Passed${NC}\n"
|
|
209
|
+
fi
|
|
155
210
|
|
|
156
211
|
local lint_label="Oxlint Check"
|
|
157
212
|
[ "$FIX_MODE" = true ] && lint_label="Oxlint Fix"
|
|
158
213
|
printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} ${lint_label}\n\n"
|
|
159
|
-
[ -
|
|
160
|
-
|
|
214
|
+
if [ $lint_status -ne 0 ]; then
|
|
215
|
+
[ -s "$tmp_dir/lint.log" ] && cat "$tmp_dir/lint.log"
|
|
216
|
+
printf "${RED}✗ Failed with exit code %d${NC}\n" $lint_status
|
|
217
|
+
else
|
|
218
|
+
printf "${GREEN}✓ Passed${NC}\n"
|
|
219
|
+
fi
|
|
161
220
|
|
|
162
221
|
local format_label="Oxfmt Check"
|
|
163
222
|
[ "$FIX_MODE" = true ] && format_label="Oxfmt Format"
|
|
164
223
|
printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} ${format_label}\n\n"
|
|
165
|
-
[ -
|
|
166
|
-
|
|
224
|
+
if [ $format_status -ne 0 ]; then
|
|
225
|
+
[ -s "$tmp_dir/format.log" ] && cat "$tmp_dir/format.log"
|
|
226
|
+
printf "${RED}✗ Failed with exit code %d${NC}\n" $format_status
|
|
227
|
+
else
|
|
228
|
+
printf "${GREEN}✓ Passed${NC}\n"
|
|
229
|
+
fi
|
|
167
230
|
|
|
168
231
|
if [ "$FIX_MODE" = false ]; then
|
|
169
232
|
printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} Knip (unused code)\n\n"
|
|
170
|
-
[ -
|
|
171
|
-
|
|
233
|
+
if [ $knip_status -ne 0 ]; then
|
|
234
|
+
[ -s "$tmp_dir/knip.log" ] && cat "$tmp_dir/knip.log"
|
|
235
|
+
printf "${RED}✗ Failed with exit code %d${NC}\n" $knip_status
|
|
236
|
+
else
|
|
237
|
+
printf "${GREEN}✓ Passed${NC}\n"
|
|
238
|
+
fi
|
|
239
|
+
|
|
240
|
+
if [ -n "$checker_pid" ]; then
|
|
241
|
+
printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} Test Conventions (@jterrazz/test)\n\n"
|
|
242
|
+
if [ $checker_status -ne 0 ]; then
|
|
243
|
+
[ -s "$tmp_dir/checker.log" ] && cat "$tmp_dir/checker.log"
|
|
244
|
+
printf "${RED}✗ Failed with exit code %d${NC}\n" $checker_status
|
|
245
|
+
else
|
|
246
|
+
printf "${GREEN}✓ Passed${NC}\n"
|
|
247
|
+
fi
|
|
248
|
+
fi
|
|
172
249
|
fi
|
|
173
250
|
|
|
174
251
|
# Summary
|
|
@@ -178,7 +255,7 @@ run_checks() {
|
|
|
178
255
|
printf "\n${CYAN_BG}${BRIGHT_WHITE} END ${NC} Finalizing quality checks\n\n"
|
|
179
256
|
fi
|
|
180
257
|
|
|
181
|
-
if [ $type_status -eq 0 ] && [ $lint_status -eq 0 ] && [ $format_status -eq 0 ] && [ $knip_status -eq 0 ]; then
|
|
258
|
+
if [ $type_status -eq 0 ] && [ $lint_status -eq 0 ] && [ $format_status -eq 0 ] && [ $knip_status -eq 0 ] && [ $checker_status -eq 0 ]; then
|
|
182
259
|
printf "${GREEN}✓ All checks passed${NC}\n"
|
|
183
260
|
exit 0
|
|
184
261
|
else
|
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": "6.
|
|
3
|
+
"version": "6.2.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/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
|
+
});
|