@jterrazz/typescript 9.3.0 → 10.1.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 (81) hide show
  1. package/README.md +20 -16
  2. package/bin/commands/check.sh +387 -146
  3. package/bin/find-tsc.sh +30 -0
  4. package/bin/typescript.sh +79 -1
  5. package/lib/check-architecture.js +89 -0
  6. package/lib/check-baseline.js +144 -0
  7. package/lib/check-docs.js +4 -3
  8. package/lib/check-drift.js +209 -0
  9. package/lib/check-gitignore.js +4 -4
  10. package/lib/check-markdown.js +279 -0
  11. package/lib/check-names.js +125 -0
  12. package/lib/check-publish.js +150 -0
  13. package/lib/check-secrets.js +115 -0
  14. package/lib/check-suppressions.js +355 -0
  15. package/lib/doctor.js +185 -0
  16. package/lib/entry-points.js +91 -0
  17. package/lib/merge-knip-config.js +57 -25
  18. package/lib/tracked-files.js +165 -0
  19. package/lib/unsafe-fixers.js +25 -0
  20. package/lib/workspace-members.js +5 -6
  21. package/package.json +36 -13
  22. package/presets/oxfmt/index.js +49 -5
  23. package/presets/oxlint/profiles/astro.js +10 -0
  24. package/presets/oxlint/profiles/bun.js +7 -0
  25. package/presets/oxlint/profiles/expo.js +7 -0
  26. package/presets/oxlint/profiles/library.js +16 -0
  27. package/presets/oxlint/profiles/next.js +7 -0
  28. package/presets/oxlint/profiles/node.js +7 -0
  29. package/presets/oxlint/profiles/react.js +7 -0
  30. package/presets/prettier/astro.json +6 -0
  31. package/presets/tsconfig/astro.json +25 -0
  32. package/presets/tsconfig/expo.json +16 -6
  33. package/presets/tsconfig/library.json +17 -0
  34. package/presets/tsconfig/next.json +12 -2
  35. package/presets/tsconfig/node.json +18 -4
  36. package/presets/tsconfig/react.json +33 -0
  37. package/presets/tsdown/build.d.ts +13 -0
  38. package/presets/tsdown/bundle.d.ts +13 -0
  39. package/presets/tsdown/bundle.js +10 -1
  40. package/rules/README.md +23 -0
  41. package/rules/_contract.js +207 -0
  42. package/rules/_contract.test.ts +81 -0
  43. package/rules/a11y.js +51 -0
  44. package/rules/architecture/hexagonal.js +56 -0
  45. package/rules/architecture/layers.js +75 -0
  46. package/rules/astro.js +56 -0
  47. package/rules/bundler.js +19 -0
  48. package/rules/catalog.js +166 -0
  49. package/rules/catalog.test.ts +98 -0
  50. package/rules/compile.js +125 -0
  51. package/rules/core/eslint.js +234 -0
  52. package/rules/core/import.js +117 -0
  53. package/rules/core/jsdoc.js +52 -0
  54. package/rules/core/node.js +36 -0
  55. package/rules/core/oxc.js +54 -0
  56. package/rules/core/promise.js +39 -0
  57. package/rules/core/typescript.js +223 -0
  58. package/rules/core/unicorn.js +210 -0
  59. package/rules/next.js +53 -0
  60. package/rules/profiles.js +95 -0
  61. package/rules/react-native.js +48 -0
  62. package/rules/react.js +155 -0
  63. package/rules/sorted.js +41 -0
  64. package/rules/vitest.js +178 -0
  65. package/src/docs.d.ts +4 -4
  66. package/src/docs.js +75 -57
  67. package/src/docs.test.ts +43 -31
  68. package/src/index.d.ts +14 -9
  69. package/src/index.js +17 -8
  70. package/src/oxfmt.d.ts +15 -2
  71. package/src/oxfmt.test.ts +10 -0
  72. package/src/oxlint.d.ts +59 -10
  73. package/src/oxlint.js +36 -50
  74. package/src/oxlint.test.ts +82 -28
  75. package/presets/oxlint/architectures/hexagonal-rules.js +0 -39
  76. package/presets/oxlint/architectures/hexagonal.js +0 -13
  77. package/presets/oxlint/base.js +0 -145
  78. package/presets/oxlint/expo.js +0 -36
  79. package/presets/oxlint/next.js +0 -43
  80. package/presets/oxlint/node.js +0 -14
  81. package/presets/oxlint/plugins/codestyle.js +0 -231
@@ -39,29 +39,26 @@ import { workspaceMembers } from './workspace-members.js';
39
39
  * alone — a comment marker in a glob (`ignore: ["**\/*.js"]`) is data.
40
40
  */
41
41
  function withoutComments(text) {
42
- let output = '',
43
- index = 0,
44
- inString = false;
42
+ let output = '';
43
+ let index = 0;
44
+ let inString = false;
45
45
 
46
46
  while (index < text.length) {
47
47
  const char = text[index];
48
48
 
49
49
  if (inString) {
50
- const escaped = char === '\\';
51
- output += escaped ? char + (text[index + 1] ?? '') : char;
52
- inString = escaped || char !== '"';
53
- index += escaped ? 2 : 1;
50
+ const step = inStringStep(text, index);
51
+ output += step.kept;
52
+ inString = step.open;
53
+ index = step.next;
54
54
  } else if (char === '"') {
55
55
  inString = true;
56
56
  output += char;
57
57
  index += 1;
58
- } else if (char === '/' && text[index + 1] === '/') {
59
- while (index < text.length && text[index] !== '\n') {
60
- index += 1;
61
- }
62
- } else if (char === '/' && text[index + 1] === '*') {
63
- const end = text.indexOf('*/', index + 2);
64
- index = end === -1 ? text.length : end + 2;
58
+ } else if (opens(text, index, '//')) {
59
+ index = afterLine(text, index);
60
+ } else if (opens(text, index, '/*')) {
61
+ index = afterBlock(text, index);
65
62
  } else {
66
63
  output += char;
67
64
  index += 1;
@@ -71,6 +68,41 @@ function withoutComments(text) {
71
68
  return output;
72
69
  }
73
70
 
71
+ /**
72
+ * One step of a scan already inside a string literal: what it keeps, whether
73
+ * the literal is still `open` after it, and where the scan resumes. A backslash
74
+ * carries the character behind it, so an escaped quote never closes anything.
75
+ */
76
+ function inStringStep(text, index) {
77
+ const char = text[index];
78
+ const escaped = char === '\\';
79
+
80
+ return {
81
+ kept: escaped ? char + (text[index + 1] ?? '') : char,
82
+ next: index + (escaped ? 2 : 1),
83
+ open: escaped || char !== '"',
84
+ };
85
+ }
86
+
87
+ /** Whether a two-character marker opens at `index`. */
88
+ function opens(text, index, marker) {
89
+ return text.startsWith(marker, index);
90
+ }
91
+
92
+ /** Where the scan resumes past a `//` comment: the newline that ends it. */
93
+ function afterLine(text, index) {
94
+ const end = text.indexOf('\n', index);
95
+
96
+ return end === -1 ? text.length : end;
97
+ }
98
+
99
+ /** Where the scan resumes past a block comment: after its closing marker. */
100
+ function afterBlock(text, index) {
101
+ const end = text.indexOf('*/', index + 2);
102
+
103
+ return end === -1 ? text.length : end + 2;
104
+ }
105
+
74
106
  /** Whether the comma at `index` is the last one of its collection — `["a",]`. */
75
107
  function closesCollection(text, index) {
76
108
  if (text[index] !== ',') {
@@ -87,19 +119,19 @@ function closesCollection(text, index) {
87
119
 
88
120
  /** Drops a comma that closes its collection, outside string literals. */
89
121
  function withoutTrailingCommas(text) {
90
- let output = '',
91
- index = 0,
92
- inString = false;
122
+ let output = '';
123
+ let index = 0;
124
+ let inString = false;
93
125
 
94
126
  while (index < text.length) {
95
127
  const char = text[index];
96
128
  const closes = closesCollection(text, index);
97
129
 
98
130
  if (inString) {
99
- const escaped = char === '\\';
100
- output += escaped ? char + (text[index + 1] ?? '') : char;
101
- inString = escaped || char !== '"';
102
- index += escaped ? 2 : 1;
131
+ const step = inStringStep(text, index);
132
+ output += step.kept;
133
+ inString = step.open;
134
+ index = step.next;
103
135
  } else {
104
136
  inString = char === '"';
105
137
  output += closes ? '' : char;
@@ -115,8 +147,8 @@ function withoutTrailingCommas(text) {
115
147
  * package's tree, and two token classes do not earn a dependency.
116
148
  */
117
149
  function readJsonc(path) {
118
- const document = readFileSync(path, 'utf8'),
119
- json = withoutTrailingCommas(withoutComments(document));
150
+ const document = readFileSync(path, 'utf8');
151
+ const json = withoutTrailingCommas(withoutComments(document));
120
152
 
121
153
  return JSON.parse(json);
122
154
  }
@@ -302,7 +334,7 @@ for (const dep of devDeps) {
302
334
  for (const dep of [...prodDeps, ...devDeps]) {
303
335
  // @scope/sub-package when the unscoped root is a peer or dependency
304
336
  // E.g. @hono/node-server → hono
305
- const scopeMatch = dep.match(/^@(?<scope>[^/]+)\//);
337
+ const scopeMatch = /^@(?<scope>[^/]+)\//u.exec(dep);
306
338
  if (
307
339
  scopeMatch &&
308
340
  (peerDeps.includes(scopeMatch.groups.scope) ||
@@ -314,7 +346,7 @@ for (const dep of [...prodDeps, ...devDeps]) {
314
346
 
315
347
  // <parent>-plugin-* or <parent>-preset-* when <parent> is installed
316
348
  // E.g. vitepress-plugin-llms → vitepress
317
- const pluginMatch = dep.match(/^(?<parent>.+?)-(?:plugin|preset|transformer|loader)-/);
349
+ const pluginMatch = /^(?<parent>.+?)-(?:plugin|preset|transformer|loader)-/u.exec(dep);
318
350
  if (pluginMatch && allDepNames.includes(pluginMatch.groups.parent)) {
319
351
  autoIgnoreDeps.push(dep);
320
352
  continue;
@@ -0,0 +1,165 @@
1
+ /**
2
+ * The files a project would carry into a commit — the one sweep every
3
+ * tree-reading gate starts from.
4
+ *
5
+ * `git ls-files --cached --others --exclude-standard` is the answer wherever
6
+ * there is a git tree: it names what is tracked AND what the next `git add`
7
+ * would track, so a file fails a gate before it is committed, not after. A
8
+ * directory git knows nothing about — a fixture the spec runner copied, a tree
9
+ * nobody has `git init`ed — is walked instead, because a gate that goes silent
10
+ * outside git is a gate that passes on everything.
11
+ *
12
+ * The sweep REFUSES an answer that is empty while the tree is not. A read that
13
+ * silently sees nothing turns every rule above it vacuously green, which is the
14
+ * one failure mode a gate must never have — and a genuinely bare directory,
15
+ * which the sandboxes of `specs/cli/` build, is told apart from it by looking.
16
+ *
17
+ * It also refuses to answer for the GROUND a spec stands on. `_fixtures/`,
18
+ * `_expected/`, `_stubs/` and `_binary/` hold a tree deliberately broken, a
19
+ * golden held byte for byte, a fake system and a build — each belonging to the
20
+ * scenario that mounts it, not to the project carrying it. A gate judges that
21
+ * tree where the scenario runs it, on its own ground, and never twice.
22
+ */
23
+
24
+ import { execFileSync } from 'node:child_process';
25
+ import { lstatSync, readdirSync, readFileSync } from 'node:fs';
26
+ import { join, matchesGlob } from 'node:path';
27
+
28
+ /** Never source, never prose: what no gate of this toolchain has a question about. */
29
+ const UNSWEPT = new Set([
30
+ '.artifacts',
31
+ '.git',
32
+ '.next',
33
+ '.turbo',
34
+ '.vite',
35
+ 'coverage',
36
+ 'dist',
37
+ 'node_modules',
38
+ ]);
39
+
40
+ /** Extensions whose bytes are not text — reading them for patterns is noise. */
41
+ const BINARY = /\.(?:avif|bin|gif|ico|jpeg|jpg|lock|mp4|pdf|png|svg|ttf|webp|woff2?|zip)$/iu;
42
+
43
+ /** Every path git carries or would carry, or null when there is no git tree here. */
44
+ function gitSweep(root) {
45
+ try {
46
+ return execFileSync(
47
+ 'git',
48
+ ['ls-files', '--cached', '--others', '--exclude-standard', '-z'],
49
+ { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
50
+ )
51
+ .split('\0')
52
+ .filter((path) => path !== '');
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ /** The same listing, derived from the filesystem, for a tree git does not know. */
59
+ function walkSweep(root) {
60
+ const found = [];
61
+
62
+ function walk(relativePath) {
63
+ let entries;
64
+ try {
65
+ entries = readdirSync(join(root, relativePath), { withFileTypes: true });
66
+ } catch {
67
+ return;
68
+ }
69
+
70
+ for (const entry of entries.toSorted((a, b) => (a.name < b.name ? -1 : 1))) {
71
+ if (UNSWEPT.has(entry.name)) {
72
+ continue;
73
+ }
74
+ const child = relativePath === '' ? entry.name : `${relativePath}/${entry.name}`;
75
+ if (entry.isDirectory()) {
76
+ walk(child);
77
+ } else if (entry.isFile()) {
78
+ found.push(child);
79
+ }
80
+ }
81
+ }
82
+
83
+ walk('');
84
+
85
+ return found;
86
+ }
87
+
88
+ /** Whether anything the gates would ever read sits at the root at all. */
89
+ function holdsSomething(root) {
90
+ try {
91
+ return readdirSync(root).some((name) => !UNSWEPT.has(name));
92
+ } catch {
93
+ return false;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * The ground a spec stands on, by the four names `@jterrazz/test` 14 gives it.
99
+ * `_common/` is deliberately absent: it is the project's own shared code, and
100
+ * the marker there says "of the row", not "not mine".
101
+ */
102
+ const GROUND = new Set(['_binary', '_expected', '_fixtures', '_stubs']);
103
+
104
+ /** Whether a path lies under the ground a scenario mounts rather than in the project. */
105
+ const isGround = (path) => path.split('/').some((segment) => GROUND.has(segment));
106
+
107
+ /**
108
+ * The sweep, filtered by what the run was told to overlook. `ignorePatterns`
109
+ * carries the `--ignore-pattern` globs `check` received, so one flag answers
110
+ * for the linter and for every gate that reads the same tree.
111
+ */
112
+ export function trackedFiles(root = '.', { ignorePatterns = [] } = {}) {
113
+ const swept = gitSweep(root) ?? walkSweep(root);
114
+
115
+ if (swept.length === 0) {
116
+ if (holdsSomething(root)) {
117
+ throw new Error(
118
+ `nothing to read under ${root} — a gate sweeping no file passes on every file`,
119
+ );
120
+ }
121
+
122
+ return [];
123
+ }
124
+
125
+ return swept
126
+ .filter((path) => !path.split('/').some((segment) => UNSWEPT.has(segment)))
127
+ .filter((path) => !isGround(path))
128
+ .filter((path) => !ignorePatterns.some((pattern) => matchesGlob(path, pattern)))
129
+ .toSorted((left, right) => (left < right ? -1 : 1));
130
+ }
131
+
132
+ /**
133
+ * A tracked file's text, or null when there is no text to read: bytes, a file
134
+ * too large to be prose or source, one that is gone, or a SYMLINK — whose
135
+ * content belongs to its target, and is judged there once instead of twice.
136
+ */
137
+ export function readText(root, path) {
138
+ if (BINARY.test(path)) {
139
+ return null;
140
+ }
141
+
142
+ try {
143
+ const stats = lstatSync(join(root, path));
144
+ if (stats.isSymbolicLink() || stats.size > 1_000_000) {
145
+ return null;
146
+ }
147
+ const text = readFileSync(join(root, path), 'utf8');
148
+
149
+ return text.includes('\0') ? null : text;
150
+ } catch {
151
+ return null;
152
+ }
153
+ }
154
+
155
+ /** The `--ignore-pattern` globs of an argument list, in the order they arrived. */
156
+ export function ignorePatternsOf(argv) {
157
+ const patterns = [];
158
+ for (const [index, argument] of argv.entries()) {
159
+ if (argument === '--ignore-pattern' && argv[index + 1] !== undefined) {
160
+ patterns.push(argv[index + 1]);
161
+ }
162
+ }
163
+
164
+ return patterns;
165
+ }
@@ -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
+ }
@@ -29,19 +29,18 @@
29
29
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
30
30
  import { join, relative, resolve, sep } from 'node:path';
31
31
  import { argv, stdout } from 'node:process';
32
- import { fileURLToPath } from 'node:url';
33
32
 
34
33
  const SKIPPED = new Set(['dist', 'node_modules']);
35
34
 
36
35
  /** A glob over path segments — `*` stops at a separator, `**` does not. */
37
36
  function toPattern(glob) {
38
37
  const escaped = glob
39
- .replaceAll(/[.+^${}()|[\]\\]/g, String.raw`\$&`)
38
+ .replaceAll(/[.+^${}()|[\]\\]/gu, String.raw`\$&`)
40
39
  .replaceAll('**', ' ')
41
40
  .replaceAll('*', '[^/]*')
42
41
  .replaceAll(' ', '.*');
43
42
 
44
- return new RegExp(`^${escaped}$`);
43
+ return new RegExp(`^${escaped}$`, 'u');
45
44
  }
46
45
 
47
46
  /** The `workspaces` globs of a package.json, in either declared form. */
@@ -70,7 +69,7 @@ export function workspaceMembers(root = '.') {
70
69
  const absolute = resolve(root);
71
70
  const patterns = declaredGlobs(absolute)
72
71
  .filter((glob) => typeof glob === 'string' && !glob.startsWith('!'))
73
- .map((glob) => toPattern(glob.replace(/\/+$/, '')));
72
+ .map((glob) => toPattern(glob.replace(/\/+$/u, '')));
74
73
 
75
74
  if (patterns.length === 0) {
76
75
  return [];
@@ -113,11 +112,11 @@ export function workspaceMembers(root = '.') {
113
112
 
114
113
  walk(absolute, 0);
115
114
 
116
- return [...members].sort();
115
+ return [...members].toSorted((left, right) => (left < right ? -1 : 1));
117
116
  }
118
117
 
119
118
  // CLI form — only when this file IS the process entry, never on import.
120
- if (argv[1] && resolve(argv[1]) === fileURLToPath(import.meta.url)) {
119
+ if (argv[1] && resolve(argv[1]) === import.meta.filename) {
121
120
  const members = workspaceMembers(argv[2] ?? '.');
122
121
  if (members.length > 0) {
123
122
  stdout.write(`${members.join('\n')}\n`);
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@jterrazz/typescript",
3
- "version": "9.3.0",
3
+ "version": "10.1.0",
4
+ "license": "MIT",
4
5
  "author": "Jean-Baptiste Terrazzoni <contact@jterrazz.com>",
5
6
  "repository": {
6
7
  "type": "git",
7
- "url": "https://github.com/jterrazz/package-typescript"
8
+ "url": "git+https://github.com/jterrazz/package-typescript.git"
8
9
  },
9
10
  "bin": {
10
11
  "typescript": "bin/typescript.sh"
@@ -13,6 +14,7 @@
13
14
  "bin",
14
15
  "lib",
15
16
  "presets",
17
+ "rules",
16
18
  "src"
17
19
  ],
18
20
  "type": "module",
@@ -37,34 +39,52 @@
37
39
  },
38
40
  "./tsconfig/*": "./presets/tsconfig/*.json",
39
41
  "./tsconfig/*.json": "./presets/tsconfig/*.json",
40
- "./tsdown/*": "./presets/tsdown/*.js",
41
- "./tsdown/*.js": "./presets/tsdown/*.js",
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
+ },
42
50
  "./presets/tsconfig/*": "./presets/tsconfig/*.json",
43
51
  "./presets/tsconfig/*.json": "./presets/tsconfig/*.json",
44
- "./presets/tsdown/*": "./presets/tsdown/*.js",
45
- "./presets/tsdown/*.js": "./presets/tsdown/*.js"
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
+ }
46
60
  },
47
61
  "publishConfig": {
48
62
  "registry": "https://registry.npmjs.org/"
49
63
  },
50
64
  "scripts": {
51
65
  "build": "# no build script",
52
- "lint": "./bin/typescript.sh check --ignore-pattern '**/_fixtures/**'",
53
- "lint:fix": "./bin/typescript.sh fix --ignore-pattern '**/_fixtures/**'",
66
+ "lint": "./bin/typescript.sh check",
67
+ "lint:fix": "./bin/typescript.sh fix",
54
68
  "test": "vitest --run"
55
69
  },
56
70
  "dependencies": {
71
+ "@arethetypeswrong/cli": "^0.18.5",
72
+ "dependency-cruiser": "^18.3.1",
57
73
  "eslint-plugin-perfectionist": "^5.10.0",
58
- "knip": "^6.27.0",
59
- "oxfmt": "^0.59.0",
60
- "oxlint": "^1.74.0",
61
- "tsdown": "^0.22.9",
74
+ "knip": "^6.35.1",
75
+ "oxfmt": "^0.68.0",
76
+ "oxlint": "^1.83.0",
77
+ "oxlint-tsgolint": "7.0.2001",
78
+ "prettier": "^3.9.6",
79
+ "prettier-plugin-astro": "^1.0.0",
80
+ "publint": "^0.3.24",
81
+ "tsdown": "^0.23.0",
62
82
  "typedoc": "^0.28.20",
63
83
  "typedoc-plugin-markdown": "^4.12.0",
64
84
  "typescript": "^6.0.0"
65
85
  },
66
86
  "devDependencies": {
67
- "@jterrazz/test": "^14.0.0",
87
+ "@jterrazz/test": "^15.0.0",
68
88
  "@types/node": "^26.1.1",
69
89
  "vitest": "^4.1.10"
70
90
  },
@@ -76,5 +96,8 @@
76
96
  "@typescript/typescript-linux-x64": "^7.0.2",
77
97
  "@typescript/typescript-win32-arm64": "^7.0.2",
78
98
  "@typescript/typescript-win32-x64": "^7.0.2"
99
+ },
100
+ "engines": {
101
+ "node": ">=24.0.0"
79
102
  }
80
103
  }
@@ -1,18 +1,62 @@
1
1
  import { defineConfig } from 'oxfmt';
2
2
 
3
+ /*
4
+ * The formatting decisions, in one place. Four of them are the estate's and do
5
+ * not move — 100 columns, 4 spaces, single quotes, trailing commas everywhere —
6
+ * because twenty-eight repositories are written that way and two of them hold
7
+ * signed attestation bytes a reflow would invalidate.
8
+ *
9
+ * The three sorters are the fourth law of the rulebook: sorting is formatting.
10
+ * oxfmt owns import order, package.json key order and Tailwind class order, so
11
+ * no lint rule reorders those bytes and no two tools fight over them
12
+ * ([Lint presets](../../docs/07-lint-presets.md)).
13
+ */
3
14
  export default defineConfig({
15
+ bracketSpacing: true,
16
+ endOfLine: 'lf',
4
17
  printWidth: 100,
5
- tabWidth: 4,
6
- useTabs: false,
7
18
  semi: true,
8
19
  singleQuote: true,
20
+ /* The grouping the estate has read for three years: builtins and externals
21
+ * together at the top, then internals, then the relative block, then
22
+ * styles. A blank line between groups, case-insensitive inside one. */
23
+ sortImports: {
24
+ groups: [
25
+ ['builtin', 'external'],
26
+ ['internal', 'subpath'],
27
+ ['parent', 'sibling', 'index'],
28
+ 'style',
29
+ 'unknown',
30
+ ],
31
+ ignoreCase: true,
32
+ newlinesBetween: true,
33
+ order: 'asc',
34
+ },
35
+ sortPackageJson: true,
36
+ /* Tailwind's own order, on `class`/`className` and on the class-holding
37
+ * helpers every repo of the estate uses. Regex matchers are not supported
38
+ * yet — these are exact names. */
39
+ sortTailwindcss: {
40
+ functions: ['clsx', 'cn', 'cva', 'tv', 'twMerge', 'twJoin', 'tw'],
41
+ },
42
+ tabWidth: 4,
9
43
  trailingComma: 'all',
10
- bracketSpacing: true,
11
- endOfLine: 'lf',
44
+ useTabs: false,
12
45
  /* `typescript docs` writes the byte-for-byte typedoc tree to docs/reference/ —
13
46
  * formatting it would fight the Docs (sync) pass, so skip it by default.
14
47
  * Asset trees and build caches are not source either — and a content tree
15
48
  * can hold signed bytes (jterrazz-web's attestations), where one formatter
16
49
  * pass invalidates every proof. */
17
- ignorePatterns: ['docs/reference', 'dist/**', 'assets/**', 'public/**', '.next/**', '.expo/**'],
50
+ ignorePatterns: [
51
+ 'docs/reference',
52
+ 'dist/**',
53
+ 'assets/**',
54
+ 'public/**',
55
+ '.next/**',
56
+ '.expo/**',
57
+ // A spec's input is deliberately mis-shaped and its golden is
58
+ // Byte-for-byte; formatting either would rewrite the claim.
59
+ '**/_fixtures/**',
60
+ '**/_expected/**',
61
+ ],
18
62
  });
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'oxlint';
2
+
3
+ import { profile } from '../../../rules/compile.js';
4
+ import { PROFILES } from '../../../rules/profiles.js';
5
+
6
+ /*
7
+ * Astro: the rulebook, plus React (an island is a React component here),
8
+ * accessibility, and the decisions an `.astro` file's own shape forces.
9
+ */
10
+ export default defineConfig(profile(PROFILES.astro));
@@ -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
+ /** Bun: the node rulebook, plus the globals Bun's runtime defines. */
7
+ export default defineConfig(profile(PROFILES.bun));
@@ -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
+ /** Expo and React Native: the rulebook, plus React, accessibility and the platform. */
7
+ export default defineConfig(profile(PROFILES.expo));
@@ -0,0 +1,16 @@
1
+ import { defineConfig } from 'oxlint';
2
+
3
+ import { profile } from '../../../rules/compile.js';
4
+ import { PROFILES } from '../../../rules/profiles.js';
5
+
6
+ /*
7
+ * A published package: the node rulebook, plus the one tree a library carries
8
+ * and never lints — the committed typedoc projection.
9
+ *
10
+ * The `.js` extension on every relative import is core's (`import/extensions`
11
+ * at `always`), because Node ESM resolves a specifier literally and a package
12
+ * published as ESM is read by Node before any bundler reads it. It pairs with
13
+ * `presets/tsdown/bundle.js`, whose `isolatedDeclarations` is what makes the
14
+ * published declarations emit without a type-checker.
15
+ */
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
+ /** Next.js: the rulebook, plus React, accessibility and Next's own 21 rules. */
7
+ export default defineConfig(profile(PROFILES.next));
@@ -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
+ /** Node.js services and command-line tools: the rulebook, and nothing added. */
7
+ export default defineConfig(profile(PROFILES.node));
@@ -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,6 @@
1
+ {
2
+ "printWidth": 100,
3
+ "singleQuote": true,
4
+ "tabWidth": 4,
5
+ "trailingComma": "all"
6
+ }
@@ -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
+ }