@jterrazz/typescript 9.2.1 → 10.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.
Files changed (71) hide show
  1. package/README.md +20 -16
  2. package/bin/commands/check.sh +348 -117
  3. package/bin/typescript.sh +55 -0
  4. package/lib/check-architecture.js +89 -0
  5. package/lib/check-baseline.js +144 -0
  6. package/lib/check-docs.js +4 -3
  7. package/lib/check-drift.js +209 -0
  8. package/lib/check-gitignore.js +4 -4
  9. package/lib/check-markdown.js +279 -0
  10. package/lib/check-names.js +125 -0
  11. package/lib/check-publish.js +150 -0
  12. package/lib/check-secrets.js +115 -0
  13. package/lib/check-suppressions.js +355 -0
  14. package/lib/doctor.js +185 -0
  15. package/lib/merge-knip-config.js +57 -25
  16. package/lib/tracked-files.js +165 -0
  17. package/lib/workspace-members.js +5 -6
  18. package/package.json +19 -8
  19. package/presets/oxfmt/index.js +49 -5
  20. package/presets/oxlint/profiles/astro.js +10 -0
  21. package/presets/oxlint/profiles/bun.js +7 -0
  22. package/presets/oxlint/profiles/expo.js +7 -0
  23. package/presets/oxlint/profiles/library.js +16 -0
  24. package/presets/oxlint/profiles/next.js +7 -0
  25. package/presets/oxlint/profiles/node.js +7 -0
  26. package/presets/prettier/astro.json +6 -0
  27. package/presets/tsconfig/expo.json +16 -6
  28. package/presets/tsconfig/library.json +18 -0
  29. package/presets/tsconfig/next.json +12 -2
  30. package/presets/tsconfig/node.json +18 -4
  31. package/rules/README.md +23 -0
  32. package/rules/_contract.js +191 -0
  33. package/rules/_contract.test.ts +81 -0
  34. package/rules/a11y.js +51 -0
  35. package/rules/architecture/hexagonal.js +56 -0
  36. package/rules/architecture/layers.js +75 -0
  37. package/rules/astro.js +49 -0
  38. package/rules/catalog.js +134 -0
  39. package/rules/catalog.test.ts +84 -0
  40. package/rules/compile.js +125 -0
  41. package/rules/core/eslint.js +234 -0
  42. package/rules/core/import.js +107 -0
  43. package/rules/core/jsdoc.js +52 -0
  44. package/rules/core/node.js +36 -0
  45. package/rules/core/oxc.js +54 -0
  46. package/rules/core/promise.js +39 -0
  47. package/rules/core/typescript.js +204 -0
  48. package/rules/core/unicorn.js +200 -0
  49. package/rules/next.js +53 -0
  50. package/rules/profiles.js +89 -0
  51. package/rules/react-native.js +48 -0
  52. package/rules/react.js +148 -0
  53. package/rules/sorted.js +41 -0
  54. package/rules/vitest.js +153 -0
  55. package/src/docs.d.ts +4 -4
  56. package/src/docs.js +75 -47
  57. package/src/docs.test.ts +136 -29
  58. package/src/index.d.ts +13 -9
  59. package/src/index.js +15 -8
  60. package/src/oxfmt.d.ts +15 -2
  61. package/src/oxfmt.test.ts +10 -0
  62. package/src/oxlint.d.ts +57 -10
  63. package/src/oxlint.js +35 -50
  64. package/src/oxlint.test.ts +82 -28
  65. package/presets/oxlint/architectures/hexagonal-rules.js +0 -39
  66. package/presets/oxlint/architectures/hexagonal.js +0 -13
  67. package/presets/oxlint/base.js +0 -145
  68. package/presets/oxlint/expo.js +0 -36
  69. package/presets/oxlint/next.js +0 -43
  70. package/presets/oxlint/node.js +0 -14
  71. package/presets/oxlint/plugins/codestyle.js +0 -231
package/bin/typescript.sh CHANGED
@@ -41,6 +41,21 @@ find_binary() {
41
41
 
42
42
  TSDOWN=$(find_binary tsdown)
43
43
 
44
+ # oxlint's type-aware rules run in `tsgolint`, a separate binary it looks up on
45
+ # PATH — and a consumer's PATH has no reason to carry this package's bin dir. It
46
+ # is a dependency here, so the lookup is made to succeed by putting the
47
+ # directory that holds it in front, for this process and its children only.
48
+ add_tsgolint_to_path() {
49
+ local tsgolint
50
+ tsgolint=$(find_binary tsgolint)
51
+ case "$tsgolint" in
52
+ */*)
53
+ PATH="$(cd -P "$(dirname "$tsgolint")" && pwd):$PATH"
54
+ export PATH
55
+ ;;
56
+ esac
57
+ }
58
+
44
59
  # Parse command
45
60
  COMMAND="$1"
46
61
  shift 2>/dev/null || true
@@ -168,6 +183,42 @@ case "$COMMAND" in
168
183
  exec node "$PACKAGE_ROOT/lib/check-docs.js" "${1:-$PROJECT_ROOT}"
169
184
  ;;
170
185
 
186
+ doctor)
187
+ # Cheap, and read-only: what the toolchain is actually running against
188
+ # what it says it needs. No binary is spawned — every version is read
189
+ # off an installed package's own manifest, which is the one place that
190
+ # cannot disagree with what node will load.
191
+ printf "${CYAN_BG}${BRIGHT_WHITE} TYPESCRIPT ${NC} Checking the toolchain...\n\n"
192
+
193
+ exec node "$PACKAGE_ROOT/lib/doctor.js"
194
+ ;;
195
+
196
+ baseline)
197
+ # The ratchet, recorded. A command of its own because it neither checks
198
+ # nor repairs: it writes down where the project actually stands, so the
199
+ # oxlint pass can refuse to let that number rise. `fix --baseline` would
200
+ # bury a rewrite of a tracked file inside the everyday gesture.
201
+ cd "$PROJECT_ROOT"
202
+
203
+ add_tsgolint_to_path
204
+
205
+ OXLINT=$(find_binary oxlint)
206
+ BASELINE_REPORT=$(mktemp)
207
+ trap 'rm -f "$BASELINE_REPORT"' EXIT
208
+
209
+ printf "${CYAN_BG}${BRIGHT_WHITE} TYPESCRIPT ${NC} Recording the oxlint baseline...\n\n"
210
+
211
+ # A non-zero exit is the whole point of the recording, not a failure.
212
+ "$OXLINT" --type-aware --format json "$@" > "$BASELINE_REPORT" 2>/dev/null || true
213
+
214
+ node "$PACKAGE_ROOT/lib/check-baseline.js" "$BASELINE_REPORT" . --write
215
+
216
+ # The file is tracked, so it is the formatter's like every other tracked
217
+ # file — written here, shaped by the project's own oxfmt, never both.
218
+ OXFMT=$(find_binary oxfmt)
219
+ "$OXFMT" oxlint.baseline.json > /dev/null 2>&1 || true
220
+ ;;
221
+
171
222
  check|fix)
172
223
  exec bash "$SCRIPT_DIR/commands/check.sh" "$COMMAND" "$@"
173
224
  ;;
@@ -182,6 +233,8 @@ case "$COMMAND" in
182
233
  printf " dev Build, run, and rebuild on changes\n"
183
234
  printf " docs Generate the committed docs/reference tree; --check verifies sync\n"
184
235
  printf " docs-layout Check a repository's docs/ against the manual spine\n"
236
+ printf " doctor Report the installed tool versions against the declared ranges\n"
237
+ printf " baseline Record the oxlint baseline this project may not exceed\n"
185
238
  printf " check Check types, lint, formatting, and unused code\n"
186
239
  printf " fix Auto-fix lint and formatting issues\n"
187
240
  printf " clean Remove .artifacts/ — dist/ stays, it is the build's product\n\n"
@@ -193,6 +246,8 @@ case "$COMMAND" in
193
246
  printf " typescript docs\n"
194
247
  printf " typescript docs --check\n"
195
248
  printf " typescript docs-layout .\n"
249
+ printf " typescript doctor\n"
250
+ printf " typescript baseline\n"
196
251
  printf " typescript check\n"
197
252
  printf " typescript fix\n"
198
253
  printf " typescript clean\n"
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * The layer map a project declared, checked against the graph it actually has.
5
+ *
6
+ * oxlint's `no-restricted-imports` reads a path and a pattern; dependency-
7
+ * cruiser resolves the module graph, which is the only way to see a cycle that
8
+ * runs through three files or an edge that hides behind a barrel. Where a
9
+ * project declares a map — `.dependency-cruiser.cjs`, `.js` or `.mjs` at its
10
+ * root — this gate is the one that reads it.
11
+ *
12
+ * The rule ids are the config's own `name`s. That is deliberate and it is the
13
+ * only gate of this toolchain whose vocabulary the consumer writes: a layer map
14
+ * is a project's own architecture, and naming its rules for it is the point.
15
+ *
16
+ * Usage: node check-architecture.js [root] [--depcruise <path>]
17
+ *
18
+ * dependency-cruiser's `err-long` report, verbatim, when the graph breaks the
19
+ * map. Exit code: 0 when it holds, 1 otherwise. No `--fix`: moving a module
20
+ * across a layer is a design decision.
21
+ */
22
+
23
+ import { spawnSync } from 'node:child_process';
24
+ import { existsSync, readdirSync } from 'node:fs';
25
+ import { join, resolve } from 'node:path';
26
+ import { argv, exit, stdout } from 'node:process';
27
+
28
+ /** The three spellings dependency-cruiser answers to at a project root. */
29
+ const CONFIGS = ['.dependency-cruiser.cjs', '.dependency-cruiser.js', '.dependency-cruiser.mjs'];
30
+
31
+ /**
32
+ * What a cruise starts from. `src/` is the whole answer wherever there is one;
33
+ * a workspace that keeps its code in `apps/` and `packages/` is cruised from
34
+ * those instead, so a map never has to restate the tree's shape.
35
+ */
36
+ const ROOTS = ['apps', 'lib', 'packages'];
37
+
38
+ const flagged = (name, fallback) => {
39
+ const at = argv.indexOf(name);
40
+
41
+ return at === -1 || argv[at + 1] === undefined ? fallback : argv[at + 1];
42
+ };
43
+
44
+ const root = resolve(
45
+ argv
46
+ .slice(2)
47
+ .find(
48
+ (argument, index) => !argument.startsWith('--') && argv[index + 1] !== '--depcruise',
49
+ ) ?? '.',
50
+ );
51
+
52
+ const config = CONFIGS.find((name) => existsSync(join(root, name)));
53
+ if (config === undefined) {
54
+ exit(0);
55
+ }
56
+
57
+ const cruised = existsSync(join(root, 'src'))
58
+ ? ['src']
59
+ : ROOTS.filter((name) => existsSync(join(root, name)));
60
+
61
+ if (cruised.length === 0) {
62
+ stdout.write(
63
+ `${config} declares a layer map, but there is no src/, apps/, packages/ or lib/ to cruise\n`,
64
+ );
65
+ exit(1);
66
+ }
67
+
68
+ /* An empty root is a cruise of nothing, and dependency-cruiser refuses one. */
69
+ const populated = cruised.filter((name) => readdirSync(join(root, name)).length > 0);
70
+ if (populated.length === 0) {
71
+ exit(0);
72
+ }
73
+
74
+ const run = spawnSync(
75
+ flagged('--depcruise', 'depcruise'),
76
+ ['--config', config, '--output-type', 'err-long', ...populated],
77
+ { cwd: root, encoding: 'utf8' },
78
+ );
79
+
80
+ if (run.error) {
81
+ stdout.write(`depcruise could not be run: ${run.error.message}\n`);
82
+ exit(1);
83
+ }
84
+
85
+ if (run.status !== 0) {
86
+ stdout.write(`${run.stdout ?? ''}${run.stderr ?? ''}`);
87
+ }
88
+
89
+ exit(run.status === 0 ? 0 : 1);
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * The migration ratchet: a count per rule that may fall and never rise.
5
+ *
6
+ * A project adopting a stricter rulebook has two honest options — burn every
7
+ * diagnostic down before the first green run, or record where it stands and
8
+ * refuse to go backwards. `oxlint.baseline.json` is the second: `{ "<rule>":
9
+ * <count> }`, tracked, and read by the oxlint pass instead of the raw exit
10
+ * code. Without the file a single diagnostic fails, exactly as before.
11
+ *
12
+ * Three things fail a run that has one:
13
+ *
14
+ * - a rule whose count EXCEEDS its entry — the debt grew;
15
+ * - a rule with diagnostics and NO entry — a rule nobody recorded owing;
16
+ * - an entry whose count is now zero — the ratchet moved, so the entry goes.
17
+ *
18
+ * The third is what makes the file shrink. Without it a baseline records a debt
19
+ * that was paid years ago and nothing ever says so.
20
+ *
21
+ * Usage: node check-baseline.js <oxlint-json> [root] [--write]
22
+ *
23
+ * `--write` rewrites the file from the current counts — that is `typescript
24
+ * baseline`, a command of its own because it RECORDS rather than checks or
25
+ * repairs. Exit code: 0 when the ratchet holds, 1 otherwise.
26
+ */
27
+
28
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
29
+ import { join, resolve } from 'node:path';
30
+ import { argv, exit, stdout } from 'node:process';
31
+
32
+ /** The file, at the project root, tracked beside the config it ratchets. */
33
+ export const BASELINE = 'oxlint.baseline.json';
34
+
35
+ /** `eslint(no-debugger)` is how oxlint's JSON spells `eslint/no-debugger`. */
36
+ function ruleOf(code) {
37
+ const match = /^(?<plugin>[\w-]+)\((?<rule>[^)]+)\)$/u.exec(code ?? '');
38
+
39
+ return match === null ? (code ?? 'unknown') : `${match.groups.plugin}/${match.groups.rule}`;
40
+ }
41
+
42
+ /** How many diagnostics each rule accounts for, in oxlint's JSON report. */
43
+ export function countsOf(report) {
44
+ const counts = {};
45
+ for (const diagnostic of report.diagnostics ?? []) {
46
+ const rule = ruleOf(diagnostic.code);
47
+ counts[rule] = (counts[rule] ?? 0) + 1;
48
+ }
49
+
50
+ return counts;
51
+ }
52
+
53
+ /** The recorded counts, or null when the project does not keep a baseline. */
54
+ export function readBaseline(root) {
55
+ const path = join(root, BASELINE);
56
+ if (!existsSync(path)) {
57
+ return null;
58
+ }
59
+
60
+ try {
61
+ return JSON.parse(readFileSync(path, 'utf8'));
62
+ } catch {
63
+ return {};
64
+ }
65
+ }
66
+
67
+ /** Every way the counts break the ratchet, in the order a reader wants them. */
68
+ export function judge(counts, baseline) {
69
+ const broken = [];
70
+
71
+ for (const [rule, count] of Object.entries(counts).toSorted(([left], [right]) =>
72
+ left < right ? -1 : 1,
73
+ )) {
74
+ const allowed = baseline[rule];
75
+ if (allowed === undefined) {
76
+ broken.push(`${rule} has ${count} diagnostic(s) and no entry — it is new debt`);
77
+ } else if (count > allowed) {
78
+ broken.push(`${rule} is at ${count}, above its baseline of ${allowed}`);
79
+ }
80
+ }
81
+
82
+ for (const rule of Object.keys(baseline).toSorted()) {
83
+ if ((counts[rule] ?? 0) === 0) {
84
+ broken.push(`${rule} is at zero — the ratchet moved, delete its entry`);
85
+ }
86
+ }
87
+
88
+ return broken;
89
+ }
90
+
91
+ /** The file's text: sorted, two-space, so a diff reads as a burn-down. */
92
+ function serialise(counts) {
93
+ const sorted = Object.fromEntries(
94
+ Object.entries(counts).toSorted(([left], [right]) => (left < right ? -1 : 1)),
95
+ );
96
+
97
+ return `${JSON.stringify(sorted, null, 2)}\n`;
98
+ }
99
+
100
+ /* Imported for the ratchet's reading, run for the gate — never both at once. */
101
+ if (import.meta.main) {
102
+ const isWrite = argv.includes('--write');
103
+ const positional = argv.slice(2).filter((argument) => !argument.startsWith('--'));
104
+ const reportPath = positional[0];
105
+ const root = resolve(positional[1] ?? '.');
106
+
107
+ let report;
108
+ try {
109
+ report = JSON.parse(readFileSync(reportPath, 'utf8'));
110
+ } catch {
111
+ stdout.write(
112
+ `oxlint wrote no JSON report at ${reportPath} — the baseline cannot be read\n`,
113
+ );
114
+ exit(1);
115
+ }
116
+
117
+ const counts = countsOf(report);
118
+
119
+ if (isWrite) {
120
+ const total = Object.values(counts).reduce((sum, count) => sum + count, 0);
121
+ writeFileSync(join(root, BASELINE), serialise(counts));
122
+ stdout.write(
123
+ `${BASELINE} written — ${total} diagnostic(s) across ${Object.keys(counts).length} rule(s)\n`,
124
+ );
125
+ exit(0);
126
+ }
127
+
128
+ const baseline = readBaseline(root);
129
+
130
+ if (baseline === null) {
131
+ exit(Object.keys(counts).length > 0 ? 1 : 0);
132
+ }
133
+
134
+ const broken = judge(counts, baseline);
135
+
136
+ for (const reason of broken) {
137
+ stdout.write(`baseline-ratchet ${BASELINE} ${reason}\n`);
138
+ }
139
+ if (broken.length > 0) {
140
+ stdout.write("Run 'typescript baseline' to record where the project actually stands.\n");
141
+ }
142
+
143
+ exit(broken.length > 0 ? 1 : 0);
144
+ }
package/lib/check-docs.js CHANGED
@@ -22,7 +22,8 @@
22
22
  * place the workspace shows through is the `04-operating.md` presence test,
23
23
  * whose Dockerfile and `.infrastructure/` clauses are read at the root AND at
24
24
  * every workspace member's root — a monorepo deploys from a member as readily
25
- * as from its root (ADR-007, the toolchain's unit is the workspace package).
25
+ * as from its root ([Architecture](../docs/01-architecture.md), "The unit is the
26
+ * workspace package").
26
27
  * The publishable clause stays on the ROOT manifest: a private root that holds
27
28
  * a publishable member is a question for its owner, not a verdict for a gate.
28
29
  */
@@ -35,7 +36,7 @@ import { auditDocs, HEAD_LINES } from '../src/docs.js';
35
36
  import { workspaceMembers } from './workspace-members.js';
36
37
 
37
38
  /** Every markdown link target of a page, in the order the page carries them. */
38
- const LINK = /!?\[[^\]]*]\(\s*(?<target>[^\s)]+)/g;
39
+ const LINK = /!?\[[^\]]*\]\(\s*(?<target>[^\s)]+)/gu;
39
40
 
40
41
  /** Where a repository declares a deployment it owns. */
41
42
  const INFRASTRUCTURE = '.infrastructure';
@@ -53,7 +54,7 @@ function listDocs(root) {
53
54
  paths.push(`${relativePath}/`);
54
55
 
55
56
  const entries = readdirSync(join(root, relativePath), { withFileTypes: true });
56
- for (const entry of entries.sort((a, b) => (a.name < b.name ? -1 : 1))) {
57
+ for (const entry of entries.toSorted((a, b) => (a.name < b.name ? -1 : 1))) {
57
58
  const child = `${relativePath}/${entry.name}`;
58
59
  if (entry.isDirectory()) {
59
60
  walk(child);
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * How far this project stands from the profile it says it extends.
5
+ *
6
+ * Four numbers, printed at the end of every `check`, because the alternative is
7
+ * what the estate had: twenty-eight repositories each quietly a little further
8
+ * from the shared rulebook, and nobody able to say by how much without opening
9
+ * twenty-eight config files. A rule turned off, a suppression written, a
10
+ * baseline entry recorded and a tool left behind are the four ways a project
11
+ * drifts, and each of them is a number here.
12
+ *
13
+ * One of the four is also a GATE. A rule the profile has on may be turned off —
14
+ * a project knows things the profile does not — but not silently: the line that
15
+ * turns it off carries a `// reason:` comment, or the run fails on
16
+ * `drift-unreasoned`. The other three only ever report.
17
+ *
18
+ * Usage: node check-drift.js [root] [--oxlint <path>] [--json]
19
+ * [--ignore-pattern <glob>]…
20
+ *
21
+ * Exit code: 0 unless a rule is turned off with no reason beside it.
22
+ */
23
+
24
+ import { execFileSync } from 'node:child_process';
25
+ import { existsSync, readFileSync } from 'node:fs';
26
+ import { join, resolve } from 'node:path';
27
+ import { argv, exit, stdout } from 'node:process';
28
+
29
+ import { PROFILES } from '../rules/profiles.js';
30
+ import { readBaseline } from './check-baseline.js';
31
+ import { countSuppressions } from './check-suppressions.js';
32
+ import { toolVersions } from './doctor.js';
33
+ import { ignorePatternsOf } from './tracked-files.js';
34
+
35
+ const PACKAGE_ROOT = resolve(import.meta.dirname, '..');
36
+
37
+ /** Where a consumer declares its rules, in the order oxlint looks. */
38
+ const CONFIGS = [
39
+ 'oxlint.config.ts',
40
+ 'oxlint.config.mjs',
41
+ 'oxlint.config.js',
42
+ 'oxlint.config.cjs',
43
+ '.oxlintrc.json',
44
+ ];
45
+
46
+ /** The profiles this package ships, read off the manifest that defines them. */
47
+ const PROFILE_NAMES = new Set(Object.keys(PROFILES));
48
+
49
+ /** The comment that makes a rule turned off a decision rather than a drift. */
50
+ const REASON = /\/\/\s*reason:/iu;
51
+
52
+ /** A resolved rule set, as `name -> level`, or null when oxlint refused. */
53
+ function resolvedRules(root, oxlint, configPath) {
54
+ try {
55
+ const printed = execFileSync(
56
+ oxlint,
57
+ configPath === null ? ['--print-config'] : ['-c', configPath, '--print-config'],
58
+ { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
59
+ );
60
+
61
+ return JSON.parse(printed).rules ?? {};
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * The profile a consumer config extends, read off the config's own source, or
69
+ * null when it extends none of ours.
70
+ *
71
+ * The SOURCE is the evidence, and it is the only evidence available: oxlint's
72
+ * config schema is closed — an unknown top-level key makes it print `Failed to
73
+ * parse oxlint configuration file` — so a compiled profile cannot carry its own
74
+ * name through to `--print-config`. A config that imports `node` from this
75
+ * package's oxlint entry is a `node` project, and that import is what this
76
+ * reads.
77
+ *
78
+ * Null matters: a project that extends no profile of ours is not DRIFTING from
79
+ * one, it never joined it, and reporting two hundred "rules off" would say
80
+ * nothing about anything.
81
+ */
82
+ function profileOf(source) {
83
+ const imported =
84
+ /import\s*\{(?<names>[^}]*)\}\s*from\s*['"]@jterrazz\/typescript\/oxlint['"]/u.exec(
85
+ source ?? '',
86
+ );
87
+ const named = (imported?.groups.names ?? '').split(',').map((name) => name.trim());
88
+
89
+ return named.find((name) => PROFILE_NAMES.has(name)) ?? null;
90
+ }
91
+
92
+ /** A rule the profile runs and this project does not. */
93
+ function turnedOff(profile, consumer) {
94
+ const off = [];
95
+ for (const [rule, level] of Object.entries(profile).toSorted(([left], [right]) =>
96
+ left < right ? -1 : 1,
97
+ )) {
98
+ if (level === 'allow') {
99
+ continue;
100
+ }
101
+ const here = consumer[rule];
102
+ if (here === undefined || here === 'allow') {
103
+ off.push(rule);
104
+ }
105
+ }
106
+
107
+ return off;
108
+ }
109
+
110
+ /** The line of the config that turns a rule off, when the config names it at all. */
111
+ function lineNaming(source, rule) {
112
+ const bare = rule.split('/').at(-1);
113
+
114
+ return (source ?? '')
115
+ .split('\n')
116
+ .find(
117
+ (line) =>
118
+ line.includes(rule) || line.includes(`'${bare}'`) || line.includes(`"${bare}"`),
119
+ );
120
+ }
121
+
122
+ const isJson = argv.includes('--json');
123
+ const oxlintAt = argv[argv.indexOf('--oxlint') + 1];
124
+ const oxlint = argv.includes('--oxlint') && oxlintAt !== undefined ? oxlintAt : 'oxlint';
125
+ const root = resolve(
126
+ argv.slice(2).find((argument, index) => {
127
+ const previous = argv[index + 1];
128
+
129
+ return (
130
+ !argument.startsWith('--') && previous !== '--oxlint' && previous !== '--ignore-pattern'
131
+ );
132
+ }) ?? '.',
133
+ );
134
+
135
+ const configName = CONFIGS.find((name) => existsSync(join(root, name)));
136
+ const source = configName === undefined ? null : readFileSync(join(root, configName), 'utf8');
137
+ const profileName = profileOf(source);
138
+
139
+ const consumerRules = resolvedRules(root, oxlint, null) ?? {};
140
+ const profileRules =
141
+ profileName === null
142
+ ? {}
143
+ : (resolvedRules(
144
+ root,
145
+ oxlint,
146
+ join(PACKAGE_ROOT, 'presets/oxlint/profiles', `${profileName}.js`),
147
+ ) ?? {});
148
+
149
+ const off = turnedOff(profileRules, consumerRules);
150
+ const unreasoned = off.filter((rule) => {
151
+ const line = lineNaming(source, rule);
152
+
153
+ return line === undefined || !REASON.test(line);
154
+ });
155
+
156
+ const baseline = readBaseline(root);
157
+ const baselineTotal =
158
+ baseline === null ? null : Object.values(baseline).reduce((sum, count) => sum + count, 0);
159
+ const suppressions = countSuppressions(root, ignorePatternsOf(argv));
160
+ const behind = toolVersions().filter(({ verdict }) => verdict !== 'ok');
161
+
162
+ if (isJson) {
163
+ stdout.write(
164
+ `${JSON.stringify(
165
+ {
166
+ baseline: baselineTotal,
167
+ profile: profileName,
168
+ rulesOff: off,
169
+ suppressions,
170
+ tools: behind,
171
+ unreasoned,
172
+ },
173
+ null,
174
+ 2,
175
+ )}\n`,
176
+ );
177
+ exit(unreasoned.length > 0 ? 1 : 0);
178
+ }
179
+
180
+ /** The one line a reader looks for: what this project runs that the profile does not. */
181
+ function rulesOffLine() {
182
+ if (profileName === null) {
183
+ return 'not measured';
184
+ }
185
+
186
+ return off.length === 0 ? 'none' : `${off.length} (${off.join(', ')})`;
187
+ }
188
+
189
+ stdout.write(` profile ${profileName ?? "none of this package's"}\n`);
190
+ stdout.write(` rules off vs profile ${rulesOffLine()}\n`);
191
+ stdout.write(` suppressions ${suppressions}\n`);
192
+ stdout.write(` baseline ${baselineTotal === null ? 'absent' : baselineTotal}\n`);
193
+ stdout.write(
194
+ ` tool versions ${
195
+ behind.length === 0
196
+ ? 'in range'
197
+ : behind
198
+ .map(({ installed, name, verdict }) => `${name} ${installed} ${verdict}`)
199
+ .join(', ')
200
+ }\n`,
201
+ );
202
+
203
+ for (const rule of unreasoned) {
204
+ stdout.write(
205
+ `\ndrift-unreasoned ${configName ?? 'oxlint.config'} ${rule} is off and the line that turns it off carries no '// reason:'\n`,
206
+ );
207
+ }
208
+
209
+ exit(unreasoned.length > 0 ? 1 : 0);
@@ -125,7 +125,7 @@ function nextConfigDeclaresExport(dir) {
125
125
  continue;
126
126
  }
127
127
  try {
128
- if (/output\s*:\s*['"]export['"]/.test(readFileSync(path, 'utf8'))) {
128
+ if (/output\s*:\s*['"]export['"]/u.test(readFileSync(path, 'utf8'))) {
129
129
  return true;
130
130
  }
131
131
  } catch {
@@ -192,7 +192,7 @@ function findWorkspaceRoot(root) {
192
192
  * matches at any depth below it.
193
193
  */
194
194
  function isAnchored(pattern) {
195
- return pattern.replace(/\/+$/, '').includes('/');
195
+ return pattern.replace(/\/+$/u, '').includes('/');
196
196
  }
197
197
 
198
198
  /**
@@ -203,11 +203,11 @@ function isAnchored(pattern) {
203
203
  */
204
204
  function subject(pattern) {
205
205
  const segments = pattern
206
- .replace(/\/+$/, '')
206
+ .replace(/\/+$/u, '')
207
207
  .split('/')
208
208
  .filter((segment) => segment !== '' && segment !== '**');
209
209
 
210
- return (segments.at(-1) ?? '').replace(/\*$/, '');
210
+ return (segments.at(-1) ?? '').replace(/\*$/u, '');
211
211
  }
212
212
 
213
213
  /** The `.artifacts/` home of the artefact a pattern names, or null. */