@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
@@ -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
+ }
@@ -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.2.1",
3
+ "version": "10.0.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",
@@ -49,16 +51,22 @@
49
51
  },
50
52
  "scripts": {
51
53
  "build": "# no build script",
52
- "lint": "./bin/typescript.sh check --ignore-pattern '**/_fixtures/**'",
53
- "lint:fix": "./bin/typescript.sh fix --ignore-pattern '**/_fixtures/**'",
54
+ "lint": "./bin/typescript.sh check",
55
+ "lint:fix": "./bin/typescript.sh fix",
54
56
  "test": "vitest --run"
55
57
  },
56
58
  "dependencies": {
59
+ "@arethetypeswrong/cli": "^0.18.5",
60
+ "dependency-cruiser": "^18.3.1",
57
61
  "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",
62
+ "knip": "^6.35.1",
63
+ "oxfmt": "^0.68.0",
64
+ "oxlint": "^1.83.0",
65
+ "oxlint-tsgolint": "7.0.2001",
66
+ "prettier": "^3.9.6",
67
+ "prettier-plugin-astro": "^1.0.0",
68
+ "publint": "^0.3.24",
69
+ "tsdown": "^0.23.0",
62
70
  "typedoc": "^0.28.20",
63
71
  "typedoc-plugin-markdown": "^4.12.0",
64
72
  "typescript": "^6.0.0"
@@ -76,5 +84,8 @@
76
84
  "@typescript/typescript-linux-x64": "^7.0.2",
77
85
  "@typescript/typescript-win32-arm64": "^7.0.2",
78
86
  "@typescript/typescript-win32-x64": "^7.0.2"
87
+ },
88
+ "engines": {
89
+ "node": ">=24.0.0"
79
90
  }
80
91
  }
@@ -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/tsconfig/library.json`, whose `isolatedDeclarations` is what makes
14
+ * the 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,6 @@
1
+ {
2
+ "printWidth": 100,
3
+ "singleQuote": true,
4
+ "tabWidth": 4,
5
+ "trailingComma": "all"
6
+ }
@@ -1,23 +1,33 @@
1
1
  {
2
2
  "display": "Expo",
3
3
  "include": ["${configDir}/**/*.ts", "${configDir}/**/*.tsx"],
4
- "exclude": ["${configDir}/node_modules"],
5
-
4
+ "exclude": [
5
+ "${configDir}/node_modules",
6
+ "${configDir}/**/_fixtures",
7
+ "${configDir}/**/_expected"
8
+ ],
6
9
  "compilerOptions": {
7
10
  "allowJs": true,
8
- "baseUrl": ".",
9
11
  "esModuleInterop": true,
12
+ "exactOptionalPropertyTypes": true,
10
13
  "incremental": true,
11
14
  "jsx": "react-native",
12
15
  "lib": ["DOM", "ESNext"],
13
- "moduleResolution": "node",
16
+ "module": "ESNext",
17
+ "moduleResolution": "bundler",
14
18
  "noEmit": true,
19
+ "noFallthroughCasesInSwitch": true,
20
+ "noImplicitOverride": true,
21
+ "noUncheckedIndexedAccess": true,
22
+ "noUncheckedSideEffectImports": true,
15
23
  "paths": {
16
- "@/*": ["src/*"]
24
+ "@/*": ["${configDir}/src/*"]
17
25
  },
18
26
  "resolveJsonModule": true,
19
27
  "skipLibCheck": true,
28
+ "strict": true,
20
29
  "target": "ESNext",
21
- "tsBuildInfoFile": "${configDir}/.artifacts/tsc/tsconfig.tsbuildinfo"
30
+ "tsBuildInfoFile": "${configDir}/.artifacts/tsc/tsconfig.tsbuildinfo",
31
+ "verbatimModuleSyntax": true
22
32
  }
23
33
  }
@@ -0,0 +1,18 @@
1
+ {
2
+ "display": "Library (ESM, declaration-emitting)",
3
+ "extends": "./node.json",
4
+ "compilerOptions": {
5
+ "allowJs": false,
6
+ "declaration": true,
7
+ "erasableSyntaxOnly": true,
8
+ "isolatedDeclarations": true
9
+ },
10
+ "include": ["${configDir}/**/*.ts"],
11
+ "exclude": [
12
+ "${configDir}/node_modules",
13
+ "${configDir}/dist",
14
+ "${configDir}/.artifacts",
15
+ "${configDir}/**/_fixtures",
16
+ "${configDir}/**/_expected"
17
+ ]
18
+ }
@@ -18,7 +18,13 @@
18
18
  {
19
19
  "name": "next"
20
20
  }
21
- ]
21
+ ],
22
+ "exactOptionalPropertyTypes": true,
23
+ "noFallthroughCasesInSwitch": true,
24
+ "noImplicitOverride": true,
25
+ "noUncheckedIndexedAccess": true,
26
+ "noUncheckedSideEffectImports": true,
27
+ "verbatimModuleSyntax": true
22
28
  },
23
29
  "include": [
24
30
  "${configDir}/next-env.d.ts",
@@ -27,5 +33,9 @@
27
33
  "${configDir}/**/*.ts",
28
34
  "${configDir}/**/*.tsx"
29
35
  ],
30
- "exclude": ["${configDir}/node_modules"]
36
+ "exclude": [
37
+ "${configDir}/node_modules",
38
+ "${configDir}/**/_fixtures",
39
+ "${configDir}/**/_expected"
40
+ ]
31
41
  }
@@ -1,19 +1,33 @@
1
1
  {
2
2
  "display": "Node ESM",
3
- "include": ["${configDir}/**/*.ts"],
4
- "exclude": ["${configDir}/node_modules"],
5
-
3
+ "include": ["${configDir}/**/*.ts", "${configDir}/**/*.js"],
4
+ "exclude": [
5
+ "${configDir}/node_modules",
6
+ "${configDir}/dist",
7
+ "${configDir}/.artifacts",
8
+ "${configDir}/**/_fixtures",
9
+ "${configDir}/**/_expected"
10
+ ],
6
11
  "compilerOptions": {
7
12
  "moduleResolution": "Bundler",
8
13
  "module": "ESNext",
9
14
  "target": "ESNext",
10
15
  "types": ["node"],
11
16
  "strict": true,
17
+ "exactOptionalPropertyTypes": true,
18
+ "noFallthroughCasesInSwitch": true,
19
+ "noImplicitOverride": true,
20
+ "noUncheckedIndexedAccess": true,
21
+ "noUncheckedSideEffectImports": true,
22
+ "verbatimModuleSyntax": true,
12
23
  "incremental": true,
13
24
  "tsBuildInfoFile": "${configDir}/.artifacts/tsc/tsconfig.tsbuildinfo",
14
25
  "experimentalDecorators": true,
15
26
  "esModuleInterop": true,
16
27
  "resolveJsonModule": true,
17
- "skipLibCheck": true
28
+ "skipLibCheck": true,
29
+ "allowJs": true,
30
+ "checkJs": false,
31
+ "noEmit": true
18
32
  }
19
33
  }
@@ -0,0 +1,23 @@
1
+ # `rules/` — the manifest
2
+
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
+
5
+ | File | Holds |
6
+ | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
7
+ | `_contract.js` | `fragment()`, `on()`, `typeAware()`, `off()`, `scoped()` — and the load-time refusal of a level that is not `error`/`off` and of an `off` 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 six 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` | what a framework profile adds to that rulebook |
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
+
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
+
19
+ ## What proves it
20
+
21
+ - `_contract.test.ts` — the invariants, and the compile of a fragment.
22
+ - `catalog.test.ts` — the chapter is the manifest's projection, and nothing else.
23
+ - `specs/cli/preset/` — the resolved rule set, the plugin coverage, the fixpoint against the formatter, the behaviour of each decision, and the exclusive pairs.