@jterrazz/typescript 10.0.0 → 10.1.1
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 +2 -2
- package/bin/commands/check.sh +49 -36
- package/bin/find-tsc.sh +30 -0
- package/bin/typescript.sh +24 -1
- package/lib/check-baseline.js +12 -1
- package/lib/entry-points.js +91 -0
- package/lib/unsafe-fixers.js +25 -0
- package/package.json +18 -6
- package/presets/oxlint/profiles/library.js +2 -2
- package/presets/oxlint/profiles/react.js +7 -0
- package/presets/tsconfig/astro.json +25 -0
- package/presets/tsconfig/library.json +1 -2
- package/presets/tsconfig/react.json +33 -0
- package/presets/tsdown/build.d.ts +13 -0
- package/presets/tsdown/bundle.d.ts +13 -0
- package/presets/tsdown/bundle.js +10 -1
- package/rules/README.md +11 -11
- package/rules/_contract.js +16 -0
- package/rules/astro.js +9 -2
- package/rules/bundler.js +19 -0
- package/rules/catalog.js +32 -0
- package/rules/catalog.test.ts +21 -7
- package/rules/core/import.js +15 -5
- package/rules/core/typescript.js +31 -12
- package/rules/core/unicorn.js +13 -3
- package/rules/profiles.js +6 -0
- package/rules/react.js +7 -0
- package/rules/vitest.js +38 -13
- package/src/docs.test.ts +2 -1
- package/src/index.d.ts +3 -2
- package/src/index.js +2 -0
- package/src/oxfmt.d.ts +1 -1
- package/src/oxfmt.test.ts +1 -1
- package/src/oxlint.d.ts +4 -2
- package/src/oxlint.js +3 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @jterrazz/typescript
|
|
2
2
|
|
|
3
|
-
The complete TypeScript toolchain — build, run, check, and document with zero configuration. Powered by tsdown, Oxlint, Oxfmt, TypeScript 7, and Knip.
|
|
3
|
+
The complete TypeScript toolchain — build, run, check, and document with zero configuration. Powered by tsdown, Oxlint, Oxfmt, TypeScript 7, and Knip. Seven profiles, one rulebook: every rule of every plugin it loads is decided by name.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -51,7 +51,7 @@ The full corpus lives in [`docs/`](docs/):
|
|
|
51
51
|
- [Operating](docs/04-operating.md) — what publishes it, and which number moves.
|
|
52
52
|
- [Building](docs/05-building.md) — `build`, `bundle`, `start`, `dev`.
|
|
53
53
|
- [Quality checks](docs/06-quality-checks.md) — `check` / `fix` and their fifteen passes.
|
|
54
|
-
- [Lint presets](docs/07-lint-presets.md) — the rulebook, the
|
|
54
|
+
- [Lint presets](docs/07-lint-presets.md) — the rulebook, the seven profiles, `compose`, architecture, knip.
|
|
55
55
|
- [Docs pipeline](docs/08-docs-pipeline.md) — the `typescript docs` compiler.
|
|
56
56
|
- [Repo structure](docs/09-repo-structure.md) — pointer to the shared doctrine; what's TypeScript-specific here.
|
|
57
57
|
|
package/bin/commands/check.sh
CHANGED
|
@@ -37,30 +37,10 @@ find_binary() {
|
|
|
37
37
|
# the per-platform @typescript/typescript-* packages instead of a second
|
|
38
38
|
# package named "typescript": typedoc and eslint-plugin-perfectionist load
|
|
39
39
|
# the JS API from the "typescript" name (v6 here), and any typescript@7 in
|
|
40
|
-
# the tree can hijack that lookup under pnpm's hoist fallback.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
Darwin) os="darwin" ;;
|
|
45
|
-
Linux) os="linux" ;;
|
|
46
|
-
MINGW*|MSYS*|CYGWIN*) os="win32" ;;
|
|
47
|
-
*) os="linux" ;;
|
|
48
|
-
esac
|
|
49
|
-
case "$(uname -m)" in
|
|
50
|
-
arm64|aarch64) arch="arm64" ;;
|
|
51
|
-
armv7l) arch="arm" ;;
|
|
52
|
-
*) arch="x64" ;;
|
|
53
|
-
esac
|
|
54
|
-
|
|
55
|
-
local pkg="@typescript/typescript-$os-$arch"
|
|
56
|
-
if [ -x "$PACKAGE_ROOT/node_modules/$pkg/lib/tsc" ]; then
|
|
57
|
-
echo "$PACKAGE_ROOT/node_modules/$pkg/lib/tsc"
|
|
58
|
-
elif [ -x "$PACKAGE_ROOT/../../$pkg/lib/tsc" ]; then
|
|
59
|
-
echo "$PACKAGE_ROOT/../../$pkg/lib/tsc"
|
|
60
|
-
else
|
|
61
|
-
find_binary tsc
|
|
62
|
-
fi
|
|
63
|
-
}
|
|
40
|
+
# the tree can hijack that lookup under pnpm's hoist fallback. The lookup is
|
|
41
|
+
# shared with the CLI's `tsc` passthrough, so it lives in one file.
|
|
42
|
+
# shellcheck source=../find-tsc.sh
|
|
43
|
+
. "$SCRIPT_DIR/../find-tsc.sh"
|
|
64
44
|
|
|
65
45
|
# oxlint's type-aware rules run in `tsgolint`, a separate binary it looks up on
|
|
66
46
|
# PATH — and a consumer's PATH has no reason to carry this package's bin dir. It
|
|
@@ -369,16 +349,38 @@ run_checks() {
|
|
|
369
349
|
# ones no syntactic linter can express, and a flag that is only sometimes
|
|
370
350
|
# passed is a rule set that is only sometimes enforced. `oxlint-tsgolint` is
|
|
371
351
|
# a dependency of this package, so it is there for every consumer.
|
|
352
|
+
#
|
|
353
|
+
# In FIX mode the two rewriters run one after the other, and they are the
|
|
354
|
+
# only pair that does: `oxlint --fix` and `oxfmt` write the same files, and
|
|
355
|
+
# in parallel the second writer lands its copy over the first's — after
|
|
356
|
+
# which `fix` then `check` fails on formatting the fix had just settled.
|
|
357
|
+
local lint_pid=""
|
|
358
|
+
local lint_status=0
|
|
359
|
+
local format_status=0
|
|
372
360
|
if [ "$FIX_MODE" = true ]; then
|
|
373
|
-
|
|
361
|
+
# A fixer that changes MEANING is never applied unattended: the rules
|
|
362
|
+
# marked `unsafe` in the manifest are allowed for THIS run only, so
|
|
363
|
+
# `fix` leaves them alone and `check` still reports them for a human.
|
|
364
|
+
local unsafe_fixers=()
|
|
365
|
+
while IFS= read -r flag; do
|
|
366
|
+
[ -n "$flag" ] && unsafe_fixers+=("$flag")
|
|
367
|
+
done < <(node "$PACKAGE_ROOT/lib/unsafe-fixers.js")
|
|
368
|
+
|
|
369
|
+
"$OXLINT" --type-aware --fix "${unsafe_fixers[@]}" "${LINT_ARGS[@]}" \
|
|
370
|
+
> "$tmp_dir/lint.log" 2>&1 ||
|
|
371
|
+
lint_status=$?
|
|
372
|
+
"$OXFMT" > "$tmp_dir/format.log" 2>&1 || format_status=$?
|
|
374
373
|
else
|
|
375
374
|
"$OXLINT" --type-aware "${LINT_ARGS[@]}" > "$tmp_dir/lint.log" 2>&1 &
|
|
375
|
+
lint_pid=$!
|
|
376
376
|
fi
|
|
377
|
-
local lint_pid=$!
|
|
378
377
|
|
|
379
378
|
# The same run, machine-readable, so the ratchet can be judged rule by rule.
|
|
380
379
|
# A second invocation rather than a reformat of the first: the human log is
|
|
381
380
|
# what a failing pass prints, and neither form can be derived from the other.
|
|
381
|
+
# In check mode it runs BESIDE the human one, on the same bytes. In fix mode
|
|
382
|
+
# it cannot: the bytes change under the fixer, so the machine-readable run
|
|
383
|
+
# is made after it, below.
|
|
382
384
|
local lint_json_pid=""
|
|
383
385
|
if [ "$FIX_MODE" = false ] && [ -f "$BASELINE_FILE" ]; then
|
|
384
386
|
"$OXLINT" --type-aware --format json "${LINT_ARGS[@]}" \
|
|
@@ -386,12 +388,11 @@ run_checks() {
|
|
|
386
388
|
lint_json_pid=$!
|
|
387
389
|
fi
|
|
388
390
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
else
|
|
391
|
+
local format_pid=""
|
|
392
|
+
if [ "$FIX_MODE" = false ]; then
|
|
392
393
|
"$OXFMT" --check > "$tmp_dir/format.log" 2>&1 &
|
|
394
|
+
format_pid=$!
|
|
393
395
|
fi
|
|
394
|
-
local format_pid=$!
|
|
395
396
|
|
|
396
397
|
# Knip: only run in check mode (fix mode is destructive)
|
|
397
398
|
# Merge base config (from this package) with optional project-local knip.json.
|
|
@@ -605,19 +606,31 @@ run_checks() {
|
|
|
605
606
|
|
|
606
607
|
# Wait and collect statuses
|
|
607
608
|
wait $type_pid; local type_status=$?
|
|
608
|
-
wait $lint_pid;
|
|
609
|
+
[ -n "$lint_pid" ] && { wait $lint_pid; lint_status=$?; }
|
|
609
610
|
|
|
610
611
|
# The ratchet, where the project keeps one: the pass is judged by what the
|
|
611
|
-
# baseline tolerates, not by oxlint's exit code
|
|
612
|
-
#
|
|
613
|
-
|
|
614
|
-
|
|
612
|
+
# baseline tolerates, not by oxlint's exit code — in FIX mode as much as in
|
|
613
|
+
# check mode, or `fix` then `check` reads red then green on the same tree.
|
|
614
|
+
# Bash decides whether the file is there; the script decides what it says.
|
|
615
|
+
#
|
|
616
|
+
# What fix mode judges is what SURVIVED the rewrite, so its machine-readable
|
|
617
|
+
# run is made here, after the fixer, and in the foreground — with the unsafe
|
|
618
|
+
# fixers armed again, so a directive that names one of them is used, not
|
|
619
|
+
# reported unused. The fixer's own exit code is never the verdict: it ran
|
|
620
|
+
# with those rules allowed. Without a baseline the judge wants zero.
|
|
621
|
+
if [ -f "$BASELINE_FILE" ] || [ "$FIX_MODE" = true ]; then
|
|
622
|
+
if [ -n "$lint_json_pid" ]; then
|
|
623
|
+
wait $lint_json_pid
|
|
624
|
+
else
|
|
625
|
+
"$OXLINT" --type-aware --format json "${LINT_ARGS[@]}" \
|
|
626
|
+
> "$tmp_dir/lint.json" 2>/dev/null
|
|
627
|
+
fi
|
|
615
628
|
node "$PACKAGE_ROOT/lib/check-baseline.js" "$tmp_dir/lint.json" . \
|
|
616
629
|
>> "$tmp_dir/lint.log" 2>&1
|
|
617
630
|
lint_status=$?
|
|
618
631
|
fi
|
|
619
632
|
|
|
620
|
-
wait $format_pid;
|
|
633
|
+
[ -n "$format_pid" ] && { wait $format_pid; format_status=$?; }
|
|
621
634
|
[ -n "$knip_pid" ] && { wait $knip_pid; knip_status=$?; }
|
|
622
635
|
[ -n "$gitignore_pid" ] && { wait $gitignore_pid; gitignore_status=$?; }
|
|
623
636
|
[ -n "$docs_layout_pid" ] && { wait $docs_layout_pid; docs_layout_status=$?; }
|
package/bin/find-tsc.sh
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# The TS7 Go compiler, by platform — sourced, never run. Both the CLI's `tsc`
|
|
2
|
+
# passthrough and the TypeScript pass of `check` resolve the same binary, and it
|
|
3
|
+
# has to be THIS one: `tsc` on PATH is whatever the tree hoisted, and under
|
|
4
|
+
# pnpm's hoist fallback that is another package's TypeScript 5.
|
|
5
|
+
#
|
|
6
|
+
# `find_binary` is the caller's — typescript.sh and commands/check.sh each
|
|
7
|
+
# define it against their own roots, and this function is resolved at call time.
|
|
8
|
+
find_tsc() {
|
|
9
|
+
local os arch
|
|
10
|
+
case "$(uname -s)" in
|
|
11
|
+
Darwin) os="darwin" ;;
|
|
12
|
+
Linux) os="linux" ;;
|
|
13
|
+
MINGW*|MSYS*|CYGWIN*) os="win32" ;;
|
|
14
|
+
*) os="linux" ;;
|
|
15
|
+
esac
|
|
16
|
+
case "$(uname -m)" in
|
|
17
|
+
arm64|aarch64) arch="arm64" ;;
|
|
18
|
+
armv7l) arch="arm" ;;
|
|
19
|
+
*) arch="x64" ;;
|
|
20
|
+
esac
|
|
21
|
+
|
|
22
|
+
local pkg="@typescript/typescript-$os-$arch"
|
|
23
|
+
if [ -x "$PACKAGE_ROOT/node_modules/$pkg/lib/tsc" ]; then
|
|
24
|
+
echo "$PACKAGE_ROOT/node_modules/$pkg/lib/tsc"
|
|
25
|
+
elif [ -x "$PACKAGE_ROOT/../../$pkg/lib/tsc" ]; then
|
|
26
|
+
echo "$PACKAGE_ROOT/../../$pkg/lib/tsc"
|
|
27
|
+
else
|
|
28
|
+
find_binary tsc
|
|
29
|
+
fi
|
|
30
|
+
}
|
package/bin/typescript.sh
CHANGED
|
@@ -39,6 +39,9 @@ find_binary() {
|
|
|
39
39
|
fi
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
# shellcheck source=find-tsc.sh
|
|
43
|
+
. "$SCRIPT_DIR/find-tsc.sh"
|
|
44
|
+
|
|
42
45
|
TSDOWN=$(find_binary tsdown)
|
|
43
46
|
|
|
44
47
|
# oxlint's type-aware rules run in `tsgolint`, a separate binary it looks up on
|
|
@@ -68,7 +71,15 @@ run_tsdown() {
|
|
|
68
71
|
|
|
69
72
|
cd "$PROJECT_ROOT"
|
|
70
73
|
|
|
71
|
-
|
|
74
|
+
# What the package publishes is its `exports` map, so the build reads its
|
|
75
|
+
# entries there rather than assuming one. A package with no map, or whose
|
|
76
|
+
# map names nothing under `dist/`, keeps the preset's `src/index.ts`.
|
|
77
|
+
local entries=()
|
|
78
|
+
while IFS= read -r entry; do
|
|
79
|
+
[ -n "$entry" ] && entries+=("$entry")
|
|
80
|
+
done < <(node "$PACKAGE_ROOT/lib/entry-points.js" "$PROJECT_ROOT")
|
|
81
|
+
|
|
82
|
+
if ! "$TSDOWN" --config "$CONFIG_PATH" --cwd "$PROJECT_ROOT" "${entries[@]}"; then
|
|
72
83
|
printf "${RED}Error: Build failed${NC}\n"
|
|
73
84
|
exit 1
|
|
74
85
|
fi
|
|
@@ -219,6 +230,16 @@ case "$COMMAND" in
|
|
|
219
230
|
"$OXFMT" oxlint.baseline.json > /dev/null 2>&1 || true
|
|
220
231
|
;;
|
|
221
232
|
|
|
233
|
+
tsc)
|
|
234
|
+
# The compiler itself, for the one thing `check` cannot do: EMIT. A
|
|
235
|
+
# repository built with project references runs `tsc --build`, and
|
|
236
|
+
# without this it reaches for `tsc` on PATH — whatever version the tree
|
|
237
|
+
# hoisted, which is how a package that dropped its own `typescript`
|
|
238
|
+
# dependency silently compiled against TypeScript 5.
|
|
239
|
+
cd "$PROJECT_ROOT"
|
|
240
|
+
exec "$(find_tsc)" "$@"
|
|
241
|
+
;;
|
|
242
|
+
|
|
222
243
|
check|fix)
|
|
223
244
|
exec bash "$SCRIPT_DIR/commands/check.sh" "$COMMAND" "$@"
|
|
224
245
|
;;
|
|
@@ -235,6 +256,7 @@ case "$COMMAND" in
|
|
|
235
256
|
printf " docs-layout Check a repository's docs/ against the manual spine\n"
|
|
236
257
|
printf " doctor Report the installed tool versions against the declared ranges\n"
|
|
237
258
|
printf " baseline Record the oxlint baseline this project may not exceed\n"
|
|
259
|
+
printf " tsc Run the TypeScript 7 compiler this package ships (emit, --build)\n"
|
|
238
260
|
printf " check Check types, lint, formatting, and unused code\n"
|
|
239
261
|
printf " fix Auto-fix lint and formatting issues\n"
|
|
240
262
|
printf " clean Remove .artifacts/ — dist/ stays, it is the build's product\n\n"
|
|
@@ -248,6 +270,7 @@ case "$COMMAND" in
|
|
|
248
270
|
printf " typescript docs-layout .\n"
|
|
249
271
|
printf " typescript doctor\n"
|
|
250
272
|
printf " typescript baseline\n"
|
|
273
|
+
printf " typescript tsc --build\n"
|
|
251
274
|
printf " typescript check\n"
|
|
252
275
|
printf " typescript fix\n"
|
|
253
276
|
printf " typescript clean\n"
|
package/lib/check-baseline.js
CHANGED
|
@@ -128,7 +128,18 @@ if (import.meta.main) {
|
|
|
128
128
|
const baseline = readBaseline(root);
|
|
129
129
|
|
|
130
130
|
if (baseline === null) {
|
|
131
|
-
|
|
131
|
+
const rules = Object.keys(counts).toSorted();
|
|
132
|
+
for (const rule of rules) {
|
|
133
|
+
stdout.write(
|
|
134
|
+
`${rule} ${counts[rule]} diagnostic(s), and the project keeps no baseline\n`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (rules.length > 0) {
|
|
138
|
+
stdout.write(
|
|
139
|
+
"Fix them, or run 'typescript baseline' to record where the project stands.\n",
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
exit(rules.length > 0 ? 1 : 0);
|
|
132
143
|
}
|
|
133
144
|
|
|
134
145
|
const broken = judge(counts, baseline);
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The entries a build compiles, read off the consumer's own `exports` map.
|
|
5
|
+
*
|
|
6
|
+
* A package with several public subpaths — `./register`, `./testing`, `./oxlint`
|
|
7
|
+
* — used to need a `tsdown.config.ts` of its own to name them, which means
|
|
8
|
+
* declaring tsdown as a dependency, which is the one-devDependency contract
|
|
9
|
+
* broken ([Developing](../docs/02-developing.md)). The map already says what
|
|
10
|
+
* the package publishes, so the build reads it there.
|
|
11
|
+
*
|
|
12
|
+
* The rule: every subpath whose target is a file under `dist/` is compiled from
|
|
13
|
+
* the same path under `src/`, with a `.ts` extension. A subpath carrying a `*`
|
|
14
|
+
* is skipped — a pattern names a set the map does not enumerate — and so is a
|
|
15
|
+
* target the source tree has no file for, because a build cannot compile what
|
|
16
|
+
* nobody wrote.
|
|
17
|
+
*
|
|
18
|
+
* Prints one entry per line, sorted; prints NOTHING when the package has no
|
|
19
|
+
* exports map or no subpath resolves, and the caller then keeps the preset's
|
|
20
|
+
* single `src/index.ts`.
|
|
21
|
+
*
|
|
22
|
+
* Usage: node entry-points.js [project-root]
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
26
|
+
import { join, resolve } from 'node:path';
|
|
27
|
+
import { argv, stdout } from 'node:process';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The file a subpath's condition tree points at, in the order Node resolves
|
|
31
|
+
* them. The empty string is "no target": a condition tree is data, and every
|
|
32
|
+
* answer here is a path.
|
|
33
|
+
*/
|
|
34
|
+
function targetOf(entry) {
|
|
35
|
+
if (typeof entry === 'string') {
|
|
36
|
+
return entry;
|
|
37
|
+
}
|
|
38
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
39
|
+
return '';
|
|
40
|
+
}
|
|
41
|
+
for (const condition of ['import', 'default', 'require']) {
|
|
42
|
+
const nested = targetOf(entry[condition]);
|
|
43
|
+
if (nested !== '') {
|
|
44
|
+
return nested;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** `./dist/register.js` -> `src/register.ts`, and the empty string otherwise. */
|
|
51
|
+
function sourceOf(target) {
|
|
52
|
+
const match = /^\.\/dist\/(?<path>.+)\.(?:js|mjs|cjs)$/u.exec(target);
|
|
53
|
+
return match === null ? '' : `src/${match.groups.path}.ts`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Every entry the map earns, in one order, each one a file that exists. */
|
|
57
|
+
export function entryPoints(root) {
|
|
58
|
+
const manifest = join(root, 'package.json');
|
|
59
|
+
if (!existsSync(manifest)) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let exported;
|
|
64
|
+
try {
|
|
65
|
+
exported = JSON.parse(readFileSync(manifest, 'utf8')).exports;
|
|
66
|
+
} catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
if (typeof exported !== 'object' || exported === null) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const entries = new Set();
|
|
74
|
+
for (const [subpath, entry] of Object.entries(exported)) {
|
|
75
|
+
if (subpath.includes('*')) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const source = sourceOf(targetOf(entry));
|
|
79
|
+
if (source !== '' && existsSync(join(root, source))) {
|
|
80
|
+
entries.add(source);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return [...entries].toSorted((left, right) => left.localeCompare(right));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (import.meta.main) {
|
|
88
|
+
for (const entry of entryPoints(resolve(argv[2] ?? '.'))) {
|
|
89
|
+
stdout.write(`${entry}\n`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The rules `fix` must not let oxlint rewrite, as the flags that switch them
|
|
5
|
+
* off for one run: `--allow <rule>` per rule, one per line.
|
|
6
|
+
*
|
|
7
|
+
* A fixer that changes MEANING cannot be applied unattended — it turns working
|
|
8
|
+
* code into code that does not compile, or into code that claims something
|
|
9
|
+
* else. The rules are marked in the manifest (`unsafeFix()`), this prints them
|
|
10
|
+
* for the shell, and the oxlint pass of `fix` passes them through. Check mode
|
|
11
|
+
* keeps every one of them armed: the diagnostic is still owed an answer, from
|
|
12
|
+
* a human ([Quality checks](../docs/06-quality-checks.md)).
|
|
13
|
+
*
|
|
14
|
+
* Usage: node unsafe-fixers.js
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { stdout } from 'node:process';
|
|
18
|
+
|
|
19
|
+
import { unsafeFixers } from '../rules/catalog.js';
|
|
20
|
+
|
|
21
|
+
if (import.meta.main) {
|
|
22
|
+
for (const { rule } of unsafeFixers()) {
|
|
23
|
+
stdout.write(`--allow\n${rule}\n`);
|
|
24
|
+
}
|
|
25
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jterrazz/typescript",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.1.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Jean-Baptiste Terrazzoni <contact@jterrazz.com>",
|
|
6
6
|
"repository": {
|
|
@@ -39,12 +39,24 @@
|
|
|
39
39
|
},
|
|
40
40
|
"./tsconfig/*": "./presets/tsconfig/*.json",
|
|
41
41
|
"./tsconfig/*.json": "./presets/tsconfig/*.json",
|
|
42
|
-
"./tsdown/*":
|
|
43
|
-
|
|
42
|
+
"./tsdown/*": {
|
|
43
|
+
"types": "./presets/tsdown/*.d.ts",
|
|
44
|
+
"default": "./presets/tsdown/*.js"
|
|
45
|
+
},
|
|
46
|
+
"./tsdown/*.js": {
|
|
47
|
+
"types": "./presets/tsdown/*.d.ts",
|
|
48
|
+
"default": "./presets/tsdown/*.js"
|
|
49
|
+
},
|
|
44
50
|
"./presets/tsconfig/*": "./presets/tsconfig/*.json",
|
|
45
51
|
"./presets/tsconfig/*.json": "./presets/tsconfig/*.json",
|
|
46
|
-
"./presets/tsdown/*":
|
|
47
|
-
|
|
52
|
+
"./presets/tsdown/*": {
|
|
53
|
+
"types": "./presets/tsdown/*.d.ts",
|
|
54
|
+
"default": "./presets/tsdown/*.js"
|
|
55
|
+
},
|
|
56
|
+
"./presets/tsdown/*.js": {
|
|
57
|
+
"types": "./presets/tsdown/*.d.ts",
|
|
58
|
+
"default": "./presets/tsdown/*.js"
|
|
59
|
+
}
|
|
48
60
|
},
|
|
49
61
|
"publishConfig": {
|
|
50
62
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -72,7 +84,7 @@
|
|
|
72
84
|
"typescript": "^6.0.0"
|
|
73
85
|
},
|
|
74
86
|
"devDependencies": {
|
|
75
|
-
"@jterrazz/test": "^
|
|
87
|
+
"@jterrazz/test": "^15.0.0",
|
|
76
88
|
"@types/node": "^26.1.1",
|
|
77
89
|
"vitest": "^4.1.10"
|
|
78
90
|
},
|
|
@@ -10,7 +10,7 @@ import { PROFILES } from '../../../rules/profiles.js';
|
|
|
10
10
|
* The `.js` extension on every relative import is core's (`import/extensions`
|
|
11
11
|
* at `always`), because Node ESM resolves a specifier literally and a package
|
|
12
12
|
* published as ESM is read by Node before any bundler reads it. It pairs with
|
|
13
|
-
* `presets/
|
|
14
|
-
*
|
|
13
|
+
* `presets/tsdown/bundle.js`, whose `isolatedDeclarations` is what makes the
|
|
14
|
+
* published declarations emit without a type-checker.
|
|
15
15
|
*/
|
|
16
16
|
export default defineConfig(profile(PROFILES.library));
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { defineConfig } from 'oxlint';
|
|
2
|
+
|
|
3
|
+
import { profile } from '../../../rules/compile.js';
|
|
4
|
+
import { PROFILES } from '../../../rules/profiles.js';
|
|
5
|
+
|
|
6
|
+
/** React with no framework under it: the rulebook, plus React and accessibility. */
|
|
7
|
+
export default defineConfig(profile(PROFILES.react));
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"display": "Astro",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"allowJs": true,
|
|
5
|
+
"esModuleInterop": true,
|
|
6
|
+
"exactOptionalPropertyTypes": true,
|
|
7
|
+
"incremental": true,
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"jsxImportSource": "react",
|
|
10
|
+
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
|
11
|
+
"module": "ESNext",
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
"noFallthroughCasesInSwitch": true,
|
|
15
|
+
"noImplicitOverride": true,
|
|
16
|
+
"noUncheckedIndexedAccess": true,
|
|
17
|
+
"noUncheckedSideEffectImports": true,
|
|
18
|
+
"resolveJsonModule": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"strict": true,
|
|
21
|
+
"target": "ESNext",
|
|
22
|
+
"tsBuildInfoFile": "${configDir}/.artifacts/tsc/tsconfig.tsbuildinfo",
|
|
23
|
+
"verbatimModuleSyntax": true
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"display": "React (bundled, no framework)",
|
|
3
|
+
"include": ["${configDir}/**/*.ts", "${configDir}/**/*.tsx"],
|
|
4
|
+
"exclude": [
|
|
5
|
+
"${configDir}/node_modules",
|
|
6
|
+
"${configDir}/dist",
|
|
7
|
+
"${configDir}/.artifacts",
|
|
8
|
+
"${configDir}/**/_fixtures",
|
|
9
|
+
"${configDir}/**/_expected"
|
|
10
|
+
],
|
|
11
|
+
"compilerOptions": {
|
|
12
|
+
"allowJs": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"exactOptionalPropertyTypes": true,
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"isolatedModules": true,
|
|
17
|
+
"jsx": "react-jsx",
|
|
18
|
+
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
|
19
|
+
"module": "ESNext",
|
|
20
|
+
"moduleResolution": "bundler",
|
|
21
|
+
"noEmit": true,
|
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
|
23
|
+
"noImplicitOverride": true,
|
|
24
|
+
"noUncheckedIndexedAccess": true,
|
|
25
|
+
"noUncheckedSideEffectImports": true,
|
|
26
|
+
"resolveJsonModule": true,
|
|
27
|
+
"skipLibCheck": true,
|
|
28
|
+
"strict": true,
|
|
29
|
+
"target": "ESNext",
|
|
30
|
+
"tsBuildInfoFile": "${configDir}/.artifacts/tsc/tsconfig.tsbuildinfo",
|
|
31
|
+
"verbatimModuleSyntax": true
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The shape of a bundler config is tsdown's own fact, so this declaration does
|
|
3
|
+
* not restate it: `UserConfig` is re-exported from the tool, for a consumer
|
|
4
|
+
* that annotates its own config ([Developing](../../docs/02-developing.md)).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { UserConfig } from 'tsdown';
|
|
8
|
+
|
|
9
|
+
/** An application build: one ESM output with declarations and source maps. */
|
|
10
|
+
declare const build: UserConfig;
|
|
11
|
+
|
|
12
|
+
export { type UserConfig } from 'tsdown';
|
|
13
|
+
export default build;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The shape of a bundler config is tsdown's own fact, so this declaration does
|
|
3
|
+
* not restate it: `UserConfig` is re-exported from the tool, for a consumer
|
|
4
|
+
* that annotates its own config ([Developing](../../docs/02-developing.md)).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { UserConfig } from 'tsdown';
|
|
8
|
+
|
|
9
|
+
/** A library bundle: ESM and CJS outputs with declarations and source maps. */
|
|
10
|
+
declare const bundle: UserConfig;
|
|
11
|
+
|
|
12
|
+
export { type UserConfig } from 'tsdown';
|
|
13
|
+
export default bundle;
|
package/presets/tsdown/bundle.js
CHANGED
|
@@ -3,7 +3,16 @@ import { defineConfig } from 'tsdown';
|
|
|
3
3
|
export default defineConfig({
|
|
4
4
|
entry: ['src/index.ts'],
|
|
5
5
|
format: ['esm', 'cjs'],
|
|
6
|
-
|
|
6
|
+
/*
|
|
7
|
+
* `isolatedDeclarations` lives here, not in the `library` tsconfig preset:
|
|
8
|
+
* what it buys is a declaration emitted without a type-checker, which is a
|
|
9
|
+
* property of the PUBLISHED artefact and of nothing else. In the tsconfig
|
|
10
|
+
* it also reached every spec file, where it refused the destructured
|
|
11
|
+
* export a specification hands back ([Developing](../../docs/02-developing.md)).
|
|
12
|
+
* `bundle` is the library command, so the guarantee sits exactly where the
|
|
13
|
+
* tsconfig preset used to put it — and nowhere wider.
|
|
14
|
+
*/
|
|
15
|
+
dts: { compilerOptions: { isolatedDeclarations: true } },
|
|
7
16
|
sourcemap: true,
|
|
8
17
|
clean: true,
|
|
9
18
|
hash: false,
|
package/rules/README.md
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
One nature lives here: a DECISION about a lint rule. Never a config object, never a path, never a tool invocation — those are `presets/`'s and `bin/`'s. What the decisions mean, and the four laws they answer to, is [Lint presets](../docs/07-lint-presets.md); this page only says where each thing is.
|
|
4
4
|
|
|
5
|
-
| File
|
|
6
|
-
|
|
|
7
|
-
| `_contract.js`
|
|
8
|
-
| `compile.js`
|
|
9
|
-
| `profiles.js`
|
|
10
|
-
| `catalog.js`
|
|
11
|
-
| `core/`
|
|
12
|
-
| `react.js` · `a11y.js` · `next.js` · `react-native.js` · `astro.js` | what a framework profile adds to that rulebook
|
|
13
|
-
| `vitest.js`
|
|
14
|
-
| `sorted.js`
|
|
15
|
-
| `architecture/`
|
|
5
|
+
| File | Holds |
|
|
6
|
+
| ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
7
|
+
| `_contract.js` | `fragment()`, `on()`, `typeAware()`, `off()`, `unsafeFix()`, `scoped()` — and the load-time refusal of a level that is not `error`/`off`, of an `off` with no reason and of an unsafe fixer with no reason |
|
|
8
|
+
| `compile.js` | fragment → plain oxlint config, and the deterministic merge behind `compose()`. It never emits `categories` |
|
|
9
|
+
| `profiles.js` | which fragments each of the seven profiles carries, and what none of them lints |
|
|
10
|
+
| `catalog.js` | every decision as one list, and the markdown the chapter carries between its `GENERATED` markers |
|
|
11
|
+
| `core/` | one file per plugin of the rulebook every profile holds: `eslint`, `typescript`, `unicorn`, `oxc`, `import`, `promise`, `node`, `jsdoc` |
|
|
12
|
+
| `react.js` · `a11y.js` · `next.js` · `react-native.js` · `astro.js` · `bundler.js` | what a framework profile adds to that rulebook, and what a bundled tree with no framework still owes |
|
|
13
|
+
| `vitest.js` | the test-file rules, as an `overrides` block — they read a test and say nothing about anything else |
|
|
14
|
+
| `sorted.js` | perfectionist: only what oxfmt does not sort |
|
|
15
|
+
| `architecture/` | `layers.js` turns a declared layer map into `no-restricted-imports` overrides; `hexagonal.js` is the map this package ships |
|
|
16
16
|
|
|
17
17
|
A fragment is loaded, not read: importing one runs its contract, so a decision that breaks an invariant fails at import time rather than at review time.
|
|
18
18
|
|
package/rules/_contract.js
CHANGED
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
* @property {unknown} [options] The rule's options, at their decided value.
|
|
31
31
|
* @property {Reason} [reason] Present on every `off`, absent on every `on`.
|
|
32
32
|
* @property {boolean} [typeAware] Whether the rule needs type information.
|
|
33
|
+
* @property {'unsafe'} [fixer] Present when the rule's own fixer changes meaning.
|
|
34
|
+
* @property {string} [fixerReason] What that rewrite changes — measured, one clause.
|
|
33
35
|
* @property {string} since The version the decision was taken in.
|
|
34
36
|
*
|
|
35
37
|
* @typedef {object} Scoped An `overrides` block, stated in decisions.
|
|
@@ -80,6 +82,20 @@ export function off(reason, since) {
|
|
|
80
82
|
return Object.freeze({ level: 'off', reason: Object.freeze({ ...reason }), since });
|
|
81
83
|
}
|
|
82
84
|
|
|
85
|
+
/**
|
|
86
|
+
* A decision whose FIXER changes meaning. The rule stays on — `check` reports
|
|
87
|
+
* it and a human answers it — but `fix` never applies the rewrite: the eight
|
|
88
|
+
* marked here have each been measured turning working code into code that
|
|
89
|
+
* does not compile, or into code that claims something else
|
|
90
|
+
* ([Lint presets](../docs/07-lint-presets.md)).
|
|
91
|
+
*/
|
|
92
|
+
export function unsafeFix(decision, why) {
|
|
93
|
+
if (typeof why !== 'string' || why.length === 0) {
|
|
94
|
+
throw new TypeError('An unsafe fixer must say what its rewrite changes.');
|
|
95
|
+
}
|
|
96
|
+
return Object.freeze({ ...decision, fixer: 'unsafe', fixerReason: why });
|
|
97
|
+
}
|
|
98
|
+
|
|
83
99
|
/**
|
|
84
100
|
* A rule that is on and needs type information — `oxlint --type-aware`, which
|
|
85
101
|
* every profile of this package turns on. The mark is what the catalogue reads.
|
package/rules/astro.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fragment, off, on, scoped } from './_contract.js';
|
|
2
|
-
import {
|
|
2
|
+
import { ASSETS } from './core/import.js';
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
5
|
* Astro. oxlint parses an `.astro` file's script body but not its frontmatter
|
|
@@ -28,7 +28,14 @@ export default fragment({
|
|
|
28
28
|
}),
|
|
29
29
|
],
|
|
30
30
|
rules: {
|
|
31
|
-
|
|
31
|
+
/*
|
|
32
|
+
* `never`, with two extensions that stay: an asset, which Vite resolves
|
|
33
|
+
* BY its extension, and `.astro` itself — Astro's `resolve.extensions`
|
|
34
|
+
* does not carry it, so a layout imported without it fails the build.
|
|
35
|
+
* An override's options REPLACE the base entry, so the whole list is
|
|
36
|
+
* restated here.
|
|
37
|
+
*/
|
|
38
|
+
'import/extensions': on(['never', { ...ASSETS, astro: 'always' }]),
|
|
32
39
|
'no-restricted-imports': on([
|
|
33
40
|
{
|
|
34
41
|
patterns: [
|
package/rules/bundler.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { fragment } from './_contract.js';
|
|
2
|
+
import { EXTENSIONS_NEVER } from './core/import.js';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* A bundled tree with no framework behind it — Vite, Remotion, a browser
|
|
6
|
+
* extension. oxlint has no plugin to load for one, so this fragment carries a
|
|
7
|
+
* single decision: the bundler resolves the specifier, so an import carries no
|
|
8
|
+
* extension.
|
|
9
|
+
*
|
|
10
|
+
* `rules/next.js`, `rules/astro.js` and `rules/react-native.js` each state the
|
|
11
|
+
* same decision inside their own framework fragment, where it sits beside that
|
|
12
|
+
* framework's plugin. A profile with no framework fragment states it here.
|
|
13
|
+
*/
|
|
14
|
+
export default fragment({
|
|
15
|
+
id: 'bundler',
|
|
16
|
+
rules: {
|
|
17
|
+
'import/extensions': EXTENSIONS_NEVER,
|
|
18
|
+
},
|
|
19
|
+
});
|
package/rules/catalog.js
CHANGED
|
@@ -31,6 +31,12 @@ export const MARKERS = Object.freeze({
|
|
|
31
31
|
start: '<!-- GENERATED -->',
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
+
/** The fence that bounds the list of fixers `fix` refuses to run. */
|
|
35
|
+
export const FIXER_MARKERS = Object.freeze({
|
|
36
|
+
end: '<!-- /GENERATED:fixers -->',
|
|
37
|
+
start: '<!-- GENERATED:fixers -->',
|
|
38
|
+
});
|
|
39
|
+
|
|
34
40
|
/** Every profile there is, so a decision carried by all of them says `all`. */
|
|
35
41
|
const EVERY_PROFILE = Object.keys(PROFILES).length;
|
|
36
42
|
|
|
@@ -132,3 +138,29 @@ function scopeOf(entry) {
|
|
|
132
138
|
}
|
|
133
139
|
return `scoped to ${entry.scoped.map((glob) => `\`${glob}\``).join(', ')}`;
|
|
134
140
|
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Every rule whose own fixer changes meaning, with what the rewrite does.
|
|
144
|
+
* `check` reports them and a human answers them; `fix` runs with each one
|
|
145
|
+
* allowed, so the rewrite is never applied
|
|
146
|
+
* ([Quality checks](../docs/06-quality-checks.md)).
|
|
147
|
+
*/
|
|
148
|
+
export function unsafeFixers() {
|
|
149
|
+
const seen = new Map();
|
|
150
|
+
for (const entry of catalog()) {
|
|
151
|
+
if (entry.fixer === 'unsafe' && !seen.has(entry.rule)) {
|
|
152
|
+
seen.set(entry.rule, entry.fixerReason);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return [...seen.entries()]
|
|
157
|
+
.map(([rule, why]) => ({ rule, why }))
|
|
158
|
+
.toSorted((left, right) => left.rule.localeCompare(right.rule));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The same list as the markdown the chapter carries between its fixer markers. */
|
|
162
|
+
export function renderFixers() {
|
|
163
|
+
return unsafeFixers()
|
|
164
|
+
.map(({ rule, why }) => `- \`${rule}\` — ${why}`)
|
|
165
|
+
.join('\n');
|
|
166
|
+
}
|
package/rules/catalog.test.ts
CHANGED
|
@@ -2,7 +2,14 @@ import { readFileSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import { expect, test } from 'vitest';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
catalog,
|
|
7
|
+
FIXER_MARKERS,
|
|
8
|
+
MARKERS,
|
|
9
|
+
render,
|
|
10
|
+
renderFixers,
|
|
11
|
+
renderReference,
|
|
12
|
+
} from './catalog.js';
|
|
6
13
|
|
|
7
14
|
/*
|
|
8
15
|
* The catalogue has two readers and one source. `docs/07-lint-presets.md` is
|
|
@@ -19,10 +26,17 @@ import { catalog, MARKERS, render, renderReference } from './catalog.js';
|
|
|
19
26
|
|
|
20
27
|
const PROJECTIONS = {
|
|
21
28
|
chapter: {
|
|
29
|
+
markers: MARKERS,
|
|
22
30
|
page: resolve(import.meta.dirname, '../docs/07-lint-presets.md'),
|
|
23
31
|
render,
|
|
24
32
|
},
|
|
33
|
+
'fixer list': {
|
|
34
|
+
markers: FIXER_MARKERS,
|
|
35
|
+
page: resolve(import.meta.dirname, '../docs/07-lint-presets.md'),
|
|
36
|
+
render: renderFixers,
|
|
37
|
+
},
|
|
25
38
|
'skill reference': {
|
|
39
|
+
markers: MARKERS,
|
|
26
40
|
page: resolve(import.meta.dirname, '../skills/jterrazz-typescript/references/rules.md'),
|
|
27
41
|
render: renderReference,
|
|
28
42
|
},
|
|
@@ -54,18 +68,18 @@ test('every decision carries the version it was taken in', () => {
|
|
|
54
68
|
|
|
55
69
|
test.each(Object.entries(PROJECTIONS))(
|
|
56
70
|
'the %s carries the catalogue the manifest renders',
|
|
57
|
-
(_name, { page, render: project }) => {
|
|
71
|
+
(_name, { markers, page, render: project }) => {
|
|
58
72
|
// Given - the generated section of the projection
|
|
59
73
|
const before = readFileSync(page, 'utf8');
|
|
60
|
-
const start = before.indexOf(
|
|
61
|
-
const end = before.indexOf(
|
|
74
|
+
const start = before.indexOf(markers.start);
|
|
75
|
+
const end = before.indexOf(markers.end);
|
|
62
76
|
expect(start, 'the page has lost its GENERATED marker').toBeGreaterThan(-1);
|
|
63
77
|
expect(end, 'the page has lost its /GENERATED marker').toBeGreaterThan(start);
|
|
64
78
|
|
|
65
79
|
// Then - it is exactly what the manifest renders today
|
|
66
80
|
const table = project();
|
|
67
81
|
if (process.env.TEST_UPDATE === '1') {
|
|
68
|
-
const head = before.slice(0, start +
|
|
82
|
+
const head = before.slice(0, start + markers.start.length);
|
|
69
83
|
writeFileSync(page, `${head}\n\n${table}\n\n${before.slice(end)}`);
|
|
70
84
|
}
|
|
71
85
|
|
|
@@ -76,8 +90,8 @@ test.each(Object.entries(PROJECTIONS))(
|
|
|
76
90
|
*/
|
|
77
91
|
const fresh = readFileSync(page, 'utf8');
|
|
78
92
|
const carried = fresh.slice(
|
|
79
|
-
fresh.indexOf(
|
|
80
|
-
fresh.indexOf(
|
|
93
|
+
fresh.indexOf(markers.start) + markers.start.length,
|
|
94
|
+
fresh.indexOf(markers.end),
|
|
81
95
|
);
|
|
82
96
|
expect(cells(carried)).toStrictEqual(cells(table));
|
|
83
97
|
},
|
package/rules/core/import.js
CHANGED
|
@@ -6,8 +6,9 @@ import { allOn, fragment, off, on } from '../_contract.js';
|
|
|
6
6
|
* `import/extensions` is the one rule two platforms answer differently: Node
|
|
7
7
|
* ESM resolves a specifier literally and needs the `.js`, a bundler resolves it
|
|
8
8
|
* and refuses one. Core states the Node answer; `rules/next.js`,
|
|
9
|
-
* `rules/astro.js
|
|
10
|
-
* at `error` either way — a profile changes the convention,
|
|
9
|
+
* `rules/astro.js`, `rules/react-native.js` and `rules/bundler.js` re-decide it
|
|
10
|
+
* for their platform, at `error` either way — a profile changes the convention,
|
|
11
|
+
* never the level.
|
|
11
12
|
*
|
|
12
13
|
* `import/no-cycle` rides here for +0.05s. Mind what it does NOT see: oxlint's
|
|
13
14
|
* implementation ignores type-only imports, so a value import one way and an
|
|
@@ -15,7 +16,7 @@ import { allOn, fragment, off, on } from '../_contract.js';
|
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
/** Asset specifiers keep their extension on every platform — a bundler resolves them by it. */
|
|
18
|
-
const ASSETS = {
|
|
19
|
+
export const ASSETS = {
|
|
19
20
|
avif: 'always',
|
|
20
21
|
css: 'always',
|
|
21
22
|
gif: 'always',
|
|
@@ -58,12 +59,17 @@ export default fragment({
|
|
|
58
59
|
'no-self-import',
|
|
59
60
|
'no-unassigned-import',
|
|
60
61
|
'no-webpack-loader-syntax',
|
|
61
|
-
'unambiguous',
|
|
62
62
|
].map((rule) => `import/${rule}`),
|
|
63
63
|
),
|
|
64
64
|
|
|
65
65
|
// -- On, at the strictest value the option carries ---------------------
|
|
66
|
-
|
|
66
|
+
/*
|
|
67
|
+
* Top level, not inline, and `verbatimModuleSyntax` is why: under it
|
|
68
|
+
* `import { type X } from 'leaflet'` is emitted as
|
|
69
|
+
* `import {} from 'leaflet'` — a runtime side-effect import of a module
|
|
70
|
+
* that may only exist in a browser. signews-web served a 500 from it.
|
|
71
|
+
*/
|
|
72
|
+
'import/consistent-type-specifier-style': on(['prefer-top-level']),
|
|
67
73
|
'import/extensions': EXTENSIONS_ALWAYS,
|
|
68
74
|
|
|
69
75
|
// -- Off, each with its one reason -------------------------------------
|
|
@@ -103,5 +109,9 @@ export default fragment({
|
|
|
103
109
|
by: 'docs/07-lint-presets.md — a module exports what it owns by name',
|
|
104
110
|
kind: 'convention',
|
|
105
111
|
}),
|
|
112
|
+
'import/unambiguous': off({
|
|
113
|
+
by: 'TypeScript — `verbatimModuleSyntax` and a package\'s `"type": "module"` already make every file a module, and what the rule reports beyond that is a file with nothing to export: an entry script, an Astro `is:inline` block',
|
|
114
|
+
kind: 'covered',
|
|
115
|
+
}),
|
|
106
116
|
},
|
|
107
117
|
});
|
package/rules/core/typescript.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { allOn, fragment, off, on, scoped, typeAware } from '../_contract.js';
|
|
1
|
+
import { allOn, fragment, off, on, scoped, typeAware, unsafeFix } from '../_contract.js';
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
4
|
* The `typescript` plugin, all 108 non-nursery rules decided by name. Fifty-one
|
|
@@ -92,7 +92,6 @@ export default fragment({
|
|
|
92
92
|
'dot-notation',
|
|
93
93
|
'no-array-delete',
|
|
94
94
|
'no-base-to-string',
|
|
95
|
-
'no-confusing-void-expression',
|
|
96
95
|
'no-deprecated',
|
|
97
96
|
'no-duplicate-type-constituents',
|
|
98
97
|
'no-floating-promises',
|
|
@@ -107,7 +106,6 @@ export default fragment({
|
|
|
107
106
|
'no-unnecessary-qualifier',
|
|
108
107
|
'no-unnecessary-template-expression',
|
|
109
108
|
'no-unnecessary-type-arguments',
|
|
110
|
-
'no-unnecessary-type-assertion',
|
|
111
109
|
'no-unnecessary-type-conversion',
|
|
112
110
|
'no-unnecessary-type-parameters',
|
|
113
111
|
'no-unsafe-argument',
|
|
@@ -119,7 +117,6 @@ export default fragment({
|
|
|
119
117
|
'no-unsafe-type-assertion',
|
|
120
118
|
'no-unsafe-unary-minus',
|
|
121
119
|
'no-useless-default-assignment',
|
|
122
|
-
'non-nullable-type-assertion-style',
|
|
123
120
|
'only-throw-error',
|
|
124
121
|
'prefer-find',
|
|
125
122
|
'prefer-includes',
|
|
@@ -153,14 +150,36 @@ export default fragment({
|
|
|
153
150
|
'ts-nocheck': true,
|
|
154
151
|
},
|
|
155
152
|
]),
|
|
156
|
-
|
|
153
|
+
/* Three fixers that change meaning, so `fix` never applies them and
|
|
154
|
+
* `check` still reports them: one rewrites a `declare module`
|
|
155
|
+
* augmentation into an alias that no longer merges, one wraps a
|
|
156
|
+
* returned promise so nothing awaits it, one drops a cast a widened
|
|
157
|
+
* platform type needs. */
|
|
158
|
+
'typescript/consistent-type-definitions': unsafeFix(
|
|
159
|
+
on(['type']),
|
|
160
|
+
'rewrites a `declare module` augmentation into an alias, which no longer merges',
|
|
161
|
+
),
|
|
162
|
+
'typescript/no-confusing-void-expression': unsafeFix(
|
|
163
|
+
typeAware(),
|
|
164
|
+
'wraps a returned promise, after which nothing awaits it',
|
|
165
|
+
),
|
|
166
|
+
'typescript/no-unnecessary-type-assertion': unsafeFix(
|
|
167
|
+
typeAware(),
|
|
168
|
+
'drops a cast a widened platform type needs',
|
|
169
|
+
),
|
|
170
|
+
/* `separate-type-imports`, for `import/consistent-type-specifier-style`'s
|
|
171
|
+
* reason: an inline type specifier survives `verbatimModuleSyntax` as an
|
|
172
|
+
* empty runtime import. */
|
|
157
173
|
'typescript/consistent-type-imports': on([
|
|
158
174
|
{
|
|
159
175
|
disallowTypeAnnotations: true,
|
|
160
|
-
fixStyle: '
|
|
176
|
+
fixStyle: 'separate-type-imports',
|
|
161
177
|
prefer: 'type-imports',
|
|
162
178
|
},
|
|
163
179
|
]),
|
|
180
|
+
/* The guard for the emit above: an all-inline type import is the shape
|
|
181
|
+
* `verbatimModuleSyntax` turns into a side effect, and this names it. */
|
|
182
|
+
'typescript/no-import-type-side-effects': on(),
|
|
164
183
|
'typescript/explicit-member-accessibility': on([{ accessibility: 'no-public' }]),
|
|
165
184
|
/* Booleans excepted: `false ?? x` is `false` and `false || x` is `x`,
|
|
166
185
|
* so on a boolean the two operators mean different things and the
|
|
@@ -177,21 +196,21 @@ export default fragment({
|
|
|
177
196
|
kind: 'covered',
|
|
178
197
|
}),
|
|
179
198
|
'typescript/explicit-function-return-type': off({
|
|
180
|
-
by: 'presets/
|
|
199
|
+
by: 'presets/tsdown/bundle.js — isolatedDeclarations requires the annotation exactly where it is load-bearing, on a published export',
|
|
181
200
|
kind: 'covered',
|
|
182
201
|
}),
|
|
183
202
|
'typescript/explicit-module-boundary-types': off({
|
|
184
|
-
by: 'presets/
|
|
203
|
+
by: 'presets/tsdown/bundle.js — isolatedDeclarations requires the annotation exactly where it is load-bearing, on a published export',
|
|
185
204
|
kind: 'covered',
|
|
186
205
|
}),
|
|
187
|
-
'typescript/no-import-type-side-effects': off({
|
|
188
|
-
by: 'import/consistent-type-specifier-style (prefer-inline) — an import whose specifiers are all inline types is exactly the form that rule asks for',
|
|
189
|
-
kind: 'exclusive',
|
|
190
|
-
}),
|
|
191
206
|
'typescript/no-empty-interface': off({
|
|
192
207
|
by: 'typescript/no-empty-object-type — its upstream successor',
|
|
193
208
|
kind: 'covered',
|
|
194
209
|
}),
|
|
210
|
+
'typescript/non-nullable-type-assertion-style': off({
|
|
211
|
+
by: 'typescript/no-non-null-assertion — its fix is the `!` assertion that rule forbids, so no edit closes both',
|
|
212
|
+
kind: 'exclusive',
|
|
213
|
+
}),
|
|
195
214
|
'typescript/no-var-requires': off({
|
|
196
215
|
by: 'typescript/no-require-imports — its upstream successor',
|
|
197
216
|
kind: 'covered',
|
package/rules/core/unicorn.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { allOn, fragment, off, on } from '../_contract.js';
|
|
1
|
+
import { allOn, fragment, off, on, unsafeFix } from '../_contract.js';
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
4
|
* The `unicorn` plugin, all 137 non-nursery rules decided by name. Two of the
|
|
@@ -72,7 +72,6 @@ export default fragment({
|
|
|
72
72
|
'no-useless-promise-resolve-reject',
|
|
73
73
|
'no-useless-spread',
|
|
74
74
|
'no-useless-switch-case',
|
|
75
|
-
'no-useless-undefined',
|
|
76
75
|
'no-zero-fractions',
|
|
77
76
|
'numeric-separators-style',
|
|
78
77
|
'prefer-add-event-listener',
|
|
@@ -96,7 +95,6 @@ export default fragment({
|
|
|
96
95
|
'prefer-event-target',
|
|
97
96
|
'prefer-export-from',
|
|
98
97
|
'prefer-global-this',
|
|
99
|
-
'prefer-import-meta-properties',
|
|
100
98
|
'prefer-keyboard-event-key',
|
|
101
99
|
'prefer-logical-operator-over-ternary',
|
|
102
100
|
'prefer-math-min-max',
|
|
@@ -159,6 +157,18 @@ export default fragment({
|
|
|
159
157
|
* default preference, and `'utf-8'` is the same encoding spelled longer. */
|
|
160
158
|
'unicorn/text-encoding-identifier-case': on(),
|
|
161
159
|
|
|
160
|
+
/* Both fixers change meaning, so `fix` never applies them and `check`
|
|
161
|
+
* still reports them: one strips a REQUIRED argument, the other drops
|
|
162
|
+
* the trailing slash `new URL('.', import.meta.url)` carries. */
|
|
163
|
+
'unicorn/no-useless-undefined': unsafeFix(
|
|
164
|
+
on(),
|
|
165
|
+
'strips a REQUIRED argument, `mockReturnValue(undefined)` included (TS2554)',
|
|
166
|
+
),
|
|
167
|
+
'unicorn/prefer-import-meta-properties': unsafeFix(
|
|
168
|
+
on(),
|
|
169
|
+
"rewrites `fileURLToPath(new URL('.', import.meta.url))` into `import.meta.dirname`, which carries no trailing slash",
|
|
170
|
+
),
|
|
171
|
+
|
|
162
172
|
// -- Off, each with its one reason -------------------------------------
|
|
163
173
|
'unicorn/explicit-length-check': off({
|
|
164
174
|
by: 'typescript/strict-boolean-expressions — it already refuses a length used as a condition',
|
package/rules/profiles.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import a11y from './a11y.js';
|
|
2
2
|
import astro from './astro.js';
|
|
3
|
+
import bundler from './bundler.js';
|
|
3
4
|
import eslint from './core/eslint.js';
|
|
4
5
|
import importPlugin from './core/import.js';
|
|
5
6
|
import jsdoc from './core/jsdoc.js';
|
|
@@ -86,4 +87,9 @@ export const PROFILES = Object.freeze({
|
|
|
86
87
|
fragments: [...CORE],
|
|
87
88
|
ignorePatterns: [...IGNORED],
|
|
88
89
|
},
|
|
90
|
+
react: {
|
|
91
|
+
env: { browser: true, builtin: true, node: true },
|
|
92
|
+
fragments: [...CORE, react, a11y, bundler],
|
|
93
|
+
ignorePatterns: [...IGNORED],
|
|
94
|
+
},
|
|
89
95
|
});
|
package/rules/react.js
CHANGED
|
@@ -144,5 +144,12 @@ export default fragment({
|
|
|
144
144
|
by: 'presets/tsconfig/next.json — the automatic JSX runtime (jsx: react-jsx) imports it',
|
|
145
145
|
kind: 'covered',
|
|
146
146
|
}),
|
|
147
|
+
|
|
148
|
+
/*
|
|
149
|
+
* A stylesheet import IS its own assignment — `import './index.css'`
|
|
150
|
+
* is how a React tree carries its styles, whatever bundles it, and
|
|
151
|
+
* there is nothing to bind it to. One owner for every React profile.
|
|
152
|
+
*/
|
|
153
|
+
'import/no-unassigned-import': on([{ allow: ['**/*.css', '**/*.scss'] }]),
|
|
147
154
|
},
|
|
148
155
|
});
|
package/rules/vitest.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { allOn, fragment, off, on, scoped } from './_contract.js';
|
|
1
|
+
import { allOn, fragment, off, on, scoped, unsafeFix } from './_contract.js';
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
4
|
* The `vitest` plugin, all 73 rules decided by name — inside an `overrides`
|
|
@@ -63,18 +63,16 @@ const ON_IN_TESTS = [
|
|
|
63
63
|
'prefer-mock-return-shorthand',
|
|
64
64
|
'prefer-snapshot-hint',
|
|
65
65
|
'prefer-spy-on',
|
|
66
|
+
'prefer-strict-boolean-matchers',
|
|
66
67
|
'prefer-strict-equal',
|
|
67
68
|
'prefer-to-be',
|
|
68
|
-
'prefer-to-be-falsy',
|
|
69
69
|
'prefer-to-be-object',
|
|
70
|
-
'prefer-to-be-truthy',
|
|
71
70
|
'prefer-to-contain',
|
|
72
71
|
'prefer-to-have-been-called-times',
|
|
73
72
|
'prefer-to-have-length',
|
|
74
73
|
'prefer-todo',
|
|
75
74
|
'require-awaited-expect-poll',
|
|
76
75
|
'require-local-test-context-for-concurrent-snapshots',
|
|
77
|
-
'require-mock-type-parameters',
|
|
78
76
|
'require-to-throw-message',
|
|
79
77
|
'valid-describe-callback',
|
|
80
78
|
'valid-expect',
|
|
@@ -83,6 +81,30 @@ const ON_IN_TESTS = [
|
|
|
83
81
|
'warn-todo',
|
|
84
82
|
].map((rule) => `vitest/${rule}`);
|
|
85
83
|
|
|
84
|
+
/*
|
|
85
|
+
* The three vitest fixers that change meaning, so `fix` never applies them and
|
|
86
|
+
* `check` still reports them. They are named here rather than inline because a
|
|
87
|
+
* decision three calls deep inside an override reads as nesting, not as a
|
|
88
|
+
* decision.
|
|
89
|
+
*
|
|
90
|
+
* `prefer-lowercase-title` carries no `allowedPrefixes`: the rule is stricter
|
|
91
|
+
* than the `j5` rule @jterrazz/test retires for it — it also refuses a title
|
|
92
|
+
* opening on an all-caps identifier (`HTTP 404 …`, `DI …`) — and an existing
|
|
93
|
+
* title is a rename, not a case for an estate-specific escape hatch.
|
|
94
|
+
*/
|
|
95
|
+
const CONSISTENT_TEST_IT = unsafeFix(
|
|
96
|
+
on([{ fn: 'test' }]),
|
|
97
|
+
'rewrites `it(` into `test(` and leaves `import { it }` behind',
|
|
98
|
+
);
|
|
99
|
+
const PREFER_LOWERCASE_TITLE = unsafeFix(
|
|
100
|
+
on(),
|
|
101
|
+
'lower-cases the first character blindly: `CLI …` becomes `cLI …`',
|
|
102
|
+
);
|
|
103
|
+
const REQUIRE_MOCK_TYPE_PARAMETERS = unsafeFix(
|
|
104
|
+
on(),
|
|
105
|
+
"rewrites `vi.mock('x', f)` into `vi.mock(import('x'), f)`, after which the factory owes the module's full type",
|
|
106
|
+
);
|
|
107
|
+
|
|
86
108
|
export default fragment({
|
|
87
109
|
id: 'vitest',
|
|
88
110
|
plugins: ['vitest'],
|
|
@@ -92,13 +114,9 @@ export default fragment({
|
|
|
92
114
|
rules: {
|
|
93
115
|
...allOn(ON_IN_TESTS),
|
|
94
116
|
|
|
95
|
-
'vitest/consistent-test-it':
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
* on an all-caps identifier (`HTTP 404 …`, `DI …`). Strictest
|
|
99
|
-
* sensible wins, and an existing title is a rename, not a case
|
|
100
|
-
* for an estate-specific escape hatch. */
|
|
101
|
-
'vitest/prefer-lowercase-title': on(),
|
|
117
|
+
'vitest/consistent-test-it': CONSISTENT_TEST_IT,
|
|
118
|
+
'vitest/prefer-lowercase-title': PREFER_LOWERCASE_TITLE,
|
|
119
|
+
'vitest/require-mock-type-parameters': REQUIRE_MOCK_TYPE_PARAMETERS,
|
|
102
120
|
|
|
103
121
|
'vitest/no-conditional-in-test': off({
|
|
104
122
|
by: "vitest/no-conditional-expect — the defect is an assertion that may not run, and that rule names it; this one also refuses a golden suite's TEST_UPDATE branch and every comparator",
|
|
@@ -124,8 +142,15 @@ export default fragment({
|
|
|
124
142
|
by: 'docs/07-lint-presets.md — a spec states its assertions; counting them is bookkeeping the reader does not need',
|
|
125
143
|
kind: 'convention',
|
|
126
144
|
}),
|
|
127
|
-
|
|
128
|
-
|
|
145
|
+
/* The pair the other way round. `toBe(true)` is a strict
|
|
146
|
+
* boolean assertion and `toBeTruthy()` is not — the fixer that
|
|
147
|
+
* rewrote one into the other WEAKENED every spec it touched. */
|
|
148
|
+
'vitest/prefer-to-be-falsy': off({
|
|
149
|
+
by: 'vitest/prefer-strict-boolean-matchers — one asks for the strict matcher, the other for the falsy one',
|
|
150
|
+
kind: 'exclusive',
|
|
151
|
+
}),
|
|
152
|
+
'vitest/prefer-to-be-truthy': off({
|
|
153
|
+
by: 'vitest/prefer-strict-boolean-matchers — one asks for the strict matcher, the other for the truthy one',
|
|
129
154
|
kind: 'exclusive',
|
|
130
155
|
}),
|
|
131
156
|
'vitest/require-top-level-describe': off({
|
package/src/docs.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { expect, test } from 'vitest';
|
|
2
2
|
|
|
3
|
-
import { auditDocs
|
|
3
|
+
import { auditDocs } from './docs.js';
|
|
4
|
+
import type { DocsTree } from './docs.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* The manual every repository carries, in its smallest compliant form: a map,
|
package/src/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { OxfmtConfig } from './oxfmt.js';
|
|
2
|
+
import type { OxlintConfig } from './oxlint.js';
|
|
3
3
|
|
|
4
4
|
declare const oxfmtConfig: OxfmtConfig;
|
|
5
5
|
|
|
@@ -11,6 +11,7 @@ declare const oxlintProfiles: {
|
|
|
11
11
|
library: OxlintConfig;
|
|
12
12
|
next: OxlintConfig;
|
|
13
13
|
node: OxlintConfig;
|
|
14
|
+
react: OxlintConfig;
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
declare const defaultExport: {
|
package/src/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import expoProfile from '../presets/oxlint/profiles/expo.js';
|
|
|
5
5
|
import libraryProfile from '../presets/oxlint/profiles/library.js';
|
|
6
6
|
import nextProfile from '../presets/oxlint/profiles/next.js';
|
|
7
7
|
import nodeProfile from '../presets/oxlint/profiles/node.js';
|
|
8
|
+
import reactProfile from '../presets/oxlint/profiles/react.js';
|
|
8
9
|
import hexagonalFragment from '../rules/architecture/hexagonal.js';
|
|
9
10
|
import { compile } from '../rules/compile.js';
|
|
10
11
|
|
|
@@ -18,6 +19,7 @@ export const oxlint = {
|
|
|
18
19
|
library: libraryProfile,
|
|
19
20
|
next: nextProfile,
|
|
20
21
|
node: nodeProfile,
|
|
22
|
+
react: reactProfile,
|
|
21
23
|
};
|
|
22
24
|
|
|
23
25
|
export default { oxfmt, oxlint };
|
package/src/oxfmt.d.ts
CHANGED
package/src/oxfmt.test.ts
CHANGED
|
@@ -12,7 +12,7 @@ test('gives the formatter the three sorters, so no lint rule owns an order', ()
|
|
|
12
12
|
// Given - the shared preset
|
|
13
13
|
// Then - import order, package.json key order and Tailwind class order are oxfmt's
|
|
14
14
|
expect(base.sortImports).toMatchObject({ ignoreCase: true, order: 'asc' });
|
|
15
|
-
expect(base.sortPackageJson).
|
|
15
|
+
expect(base.sortPackageJson).toBe(true);
|
|
16
16
|
expect(base.sortTailwindcss).toStrictEqual({
|
|
17
17
|
functions: ['clsx', 'cn', 'cva', 'tv', 'twMerge', 'twJoin', 'tw'],
|
|
18
18
|
});
|
package/src/oxlint.d.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* restate it: `OxlintConfig` and `OxlintOverride` are re-exported from the
|
|
4
4
|
* tool, under the names a consumer already reads here. A hand copy drifts —
|
|
5
5
|
* this one had `plugins?: string[]` where oxlint takes a closed union, and a
|
|
6
|
-
* consumer's `defineConfig(
|
|
6
|
+
* consumer's `defineConfig(node)` stopped type-checking.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import type { OxlintConfig } from 'oxlint';
|
|
10
10
|
|
|
11
11
|
/** A rule entry as oxlint reads it: a level, or a level and its options. */
|
|
12
12
|
type RuleEntry = NonNullable<OxlintConfig['rules']>[string];
|
|
@@ -27,6 +27,7 @@ declare const hexagonal: OxlintConfig;
|
|
|
27
27
|
declare const library: OxlintConfig;
|
|
28
28
|
declare const next: OxlintConfig;
|
|
29
29
|
declare const node: OxlintConfig;
|
|
30
|
+
declare const react: OxlintConfig;
|
|
30
31
|
|
|
31
32
|
/** The six boundaries the `hexagonal` fragment enforces, as a declared map. */
|
|
32
33
|
declare const HEXAGONAL_MAP: readonly Layer[];
|
|
@@ -59,5 +60,6 @@ export {
|
|
|
59
60
|
library,
|
|
60
61
|
next,
|
|
61
62
|
node,
|
|
63
|
+
react,
|
|
62
64
|
type RuleEntry,
|
|
63
65
|
};
|
package/src/oxlint.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* The tool-facing oxlint entry (`@jterrazz/typescript/oxlint`): the
|
|
2
|
+
* The tool-facing oxlint entry (`@jterrazz/typescript/oxlint`): the seven
|
|
3
3
|
* profiles, the `compose()` merger, the layer-map builder, and oxlint's own
|
|
4
4
|
* `defineConfig`. A consumer names a PROFILE, not a set of fragments:
|
|
5
5
|
*
|
|
6
6
|
* import { defineConfig, node } from '@jterrazz/typescript/oxlint';
|
|
7
7
|
*
|
|
8
|
-
* export default defineConfig(
|
|
8
|
+
* export default defineConfig(node);
|
|
9
9
|
*
|
|
10
10
|
* What each profile carries, and why every rule of every loaded plugin is
|
|
11
11
|
* decided by name, is [Lint presets](../docs/07-lint-presets.md).
|
|
@@ -33,6 +33,7 @@ export { default as expo } from '../presets/oxlint/profiles/expo.js';
|
|
|
33
33
|
export { default as library } from '../presets/oxlint/profiles/library.js';
|
|
34
34
|
export { default as next } from '../presets/oxlint/profiles/next.js';
|
|
35
35
|
export { default as node } from '../presets/oxlint/profiles/node.js';
|
|
36
|
+
export { default as react } from '../presets/oxlint/profiles/react.js';
|
|
36
37
|
|
|
37
38
|
export { HEXAGONAL_MAP } from '../rules/architecture/hexagonal.js';
|
|
38
39
|
|