dlinter-ts-react 0.3.1 → 0.4.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 CHANGED
@@ -79,6 +79,7 @@ export default createRecommendedConfig({
79
79
  | `infrastructure` | `undefined` (rule off) | Import patterns + runtime globals that views must never touch directly |
80
80
  | `deliveryGlobs` | `['src/App.tsx', 'src/app/**/*.{ts,tsx}']` | Which files form the composition-only delivery layer |
81
81
  | `tsconfigPath` | `./tsconfig.json` | tsconfig used by the import resolver |
82
+ | `reactCompiler` | `false` | Project compiles with React Compiler → `react-doctor/react-compiler-no-manual-memoization` activates at its upstream severity (manual memoization becomes redundant noise). Without the compiler it stays off — manual memoization is load-bearing |
82
83
 
83
84
  Presets are named after **architecture concepts** (`recommended`, `dumb-ui`), never after projects. Your project's specifics are options, not preset names.
84
85
 
@@ -90,9 +91,18 @@ Beyond the `dlinter/` rules, the preset composes the bundled ecosystem with prov
90
91
  - **`import-x`**: cycle detection (`no-cycle`), duplicate imports, TypeScript-aware resolution.
91
92
  - **`jsdoc/require-jsdoc`** on every exported function, interface, and type alias.
92
93
  - **`check-file`**: tests belong in `__tests__/`, feature folders are kebab-case, `utils.ts` is banned (name the role: `*.helpers.ts`).
93
- - **`sonarjs` + `react-doctor` as warnings** — advisory signal, never gate failures.
94
94
  - **`max-lines`**: 500 effective lines, hard error.
95
- - **`react-hooks`**: `rules-of-hooks` stays an error; nothing else replaces that safety net.
95
+ - **`react-hooks`**: `rules-of-hooks` blocks (zero false positives; violations corrupt React's hook order at runtime). `exhaustive-deps` is deliberately advisory — its documented false-positive patterns would force worse code or disable-comments at `error`.
96
+
97
+ ### Upstream severities are respected, never blanket-downgraded
98
+
99
+ Every bundled plugin whose recommended config the preset spreads keeps **its own author's per-rule triage** — that triage is exactly the work this package does for you, and sweeping a whole ruleset to `warn` would throw it away. Only *named* rules are re-tuned, each with a documented reason in the source:
100
+
101
+ - **`@typescript-eslint`** — the recommended tier (20 definite-bug rules at `error`: `no-explicit-any`, `no-misused-new`, `no-unsafe-function-type`, …) applies to all TypeScript. The **type-checked tier** (`no-floating-promises`, `no-misused-promises`, `await-thenable`, the `no-unsafe-*` family) runs on production `src/**` through the TypeScript project service — **your tsconfig must cover `src/`**. Tests are exempt (mock-assertion patterns are documented `unbound-method` misfires).
102
+ - **`sonarjs`** — upstream ships 206 bug and security rules at `error` and they block as-is, including `cognitive-complexity`, `no-identical-functions`, and `no-nested-conditional` (kept at `error` so the ESLint gate and a Fallow audit agree on one standard) and `prefer-read-only-props` (kept for SonarQube Cloud parity — it coexists with `dlinter/readonly-props`, which additionally governs `*.types.ts` contracts and needs no type information). Surgical exceptions, each with a reason: `todo-tag`/`fixme-tag` warn instead of blocking tracked work, `pseudo-random` warns (Math.random is legitimate for non-crypto frontend uses), `no-unused-vars` is off because `@typescript-eslint` owns that contract with the underscore convention. In test files only the fake-credential rules go quiet — mirroring SonarQube Cloud's reduced test-file profile — while `no-exclusive-tests` keeps guarding your suite.
103
+ - **`react-doctor`** — 34 upstream `error` rules block (definite bugs and a11y: `jsx-key`, `no-direct-mutation-state`, `require-render-return`, `alt-text`, the `aria-*` set…); ~198 heuristics stay warnings. Sole override: `react-compiler-no-manual-memoization` is off by default and returns at upstream severity via `createRecommendedConfig({ reactCompiler: true })`.
104
+
105
+ Each spread plugin is **pinned to an exact version**, and a drift test locks its upstream error-set (`src/configs/__tests__/upstream-severity-contracts/`) — a bump that changes the set fails CI and forces a deliberate re-triage, never silent drift.
96
106
 
97
107
  ## CLI
98
108
 
@@ -110,17 +120,22 @@ Scaffolds `lefthook.yml` with a lint + typecheck + test pre-commit gate. Never o
110
120
  | `typescript` | `>=5.0.0 <6.1.0` — TypeScript 7 (native) is not yet supported by `@typescript-eslint`; the ceiling moves when the ecosystem does |
111
121
  | `node` | `>=20.19.0` |
112
122
 
123
+ The type-checked rule tier builds a real TypeScript program via the project service: every file under `src/` must be covered by a `tsconfig.json` (the nearest one to each file wins). A standard `"include": ["src"]` already satisfies this.
124
+
113
125
  ## Design principles
114
126
 
115
127
  | Principle | In practice |
116
128
  |-----------|-------------|
117
129
  | First-class rules, never selector catalogs | `no-restricted-syntax` is a single rule slot consumers can silently override; every constraint here has its own rule ID, its own docs, and per-rule disable |
118
130
  | Don't reinvent the wheel | Existing plugins are bundled wheels (`max-lines`, `check-file`, `import-x`, …). Custom rules exist only for contracts no plugin ships — e.g. conditional folder ownership, pure-barrel enforcement |
131
+ | Respect upstream triage, never blanket-downgrade | A bundled plugin's own per-rule severities are the plugin author's triage — honored as-is. Only specific rule IDs are re-tuned, each with a documented reason; a drift test locks the upstream error-set so a version bump forces a deliberate re-triage instead of silent drift. Bulk-demoting a whole ruleset is the exact config archaeology this package exists to spare consumers |
119
132
  | Tests are the proof | Every rule is TDD-built with RED/GREEN `RuleTester` cases; presets are tested through a real `ESLint` instance; releases are gated by installing the actual tarball into a fresh consumer project |
120
133
  | Validated against production | The full preset reproduces the source system's lint verdicts at exact file:line parity (30/30) on the codebase it was extracted from |
121
134
 
122
135
  ## Development
123
136
 
137
+ Contributor and agent documentation lives in [`docs/`](./docs/README.md) — architecture (with diagrams), the severity policy and its override ledger, the add-a-rule recipe, and the testing harnesses.
138
+
124
139
  ```bash
125
140
  bun install
126
141
  bun run test # vitest: RuleTester per rule + ESLint-instance integration harness
@@ -144,7 +159,9 @@ Every commit and release must pass the same architecture this package sells. Two
144
159
 
145
160
  ### Releasing
146
161
 
147
- Releases are automated with [release-please](https://github.com/googleapis/release-please): conventional commits on `main` drive the next semver (`fix:` patch, `feat:` minor, `feat!:`/`BREAKING CHANGE:` major). The bot maintains a Release PR with the computed version and CHANGELOG; **merging that PR** cuts the GitHub release and publishes to npm after the tarball end-to-end gate passes.
162
+ Code lands on `main` only through **feature-branch pull requests** `main` is protected and rejects direct pushes, so CI and SonarCloud analyze the real diff on every change. See [docs/release-and-ci.md](./docs/release-and-ci.md) for branch protection, CI, and the CI-based Sonar setup.
163
+
164
+ Releases are automated with [release-please](https://github.com/googleapis/release-please): the conventional commits merged into `main` drive the next semver (`fix:` → patch, `feat:` → minor, `feat!:`/`BREAKING CHANGE:` → major). The bot maintains a Release PR with the computed version and CHANGELOG; **merging that PR** cuts the GitHub release and publishes to npm — after the tarball end-to-end gate passes.
148
165
 
149
166
  Publishing authenticates via [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/) (OIDC): no tokens, nothing to rotate, provenance built in. The trusted publisher is configured in the package settings on npmjs.com, bound to this repository's `release-please.yml` workflow. Emergency escape hatch: publish locally with `npm publish` (2FA OTP applies).
150
167
 
@@ -52,9 +52,12 @@ async function main() {
52
52
  for (const file of result.created) process.stdout.write(`created ${file}\n`);
53
53
  for (const file of result.skipped) process.stdout.write(`skipped ${file} (already exists)\n`);
54
54
  }
55
- main().catch((error) => {
56
- process.stderr.write(`dlinter failed: ${error.message}\n`);
55
+ try {
56
+ await main();
57
+ } catch (error) {
58
+ const message = error instanceof Error ? error.message : String(error);
59
+ process.stderr.write(`dlinter failed: ${message}\n`);
57
60
  process.exitCode = 1;
58
- });
61
+ }
59
62
  //#endregion
60
63
  export {};
package/dist/index.d.mts CHANGED
@@ -1128,6 +1128,14 @@ interface RecommendedConfigOptions {
1128
1128
  readonly deliveryGlobs?: readonly string[];
1129
1129
  /** tsconfig used by the import resolver. Default: `./tsconfig.json`. */
1130
1130
  readonly tsconfigPath?: string;
1131
+ /**
1132
+ * The project compiles with React Compiler. Activates
1133
+ * `react-doctor/react-compiler-no-manual-memoization` at its upstream
1134
+ * severity (manual `useMemo`/`useCallback` become redundant noise once the
1135
+ * compiler memoizes). Default `false`: without the compiler, manual
1136
+ * memoization is load-bearing and the rule would only mislead.
1137
+ */
1138
+ readonly reactCompiler?: boolean;
1131
1139
  }
1132
1140
  //#endregion
1133
1141
  //#region src/configs/recommended/recommended.d.ts
package/dist/index.mjs CHANGED
@@ -210,7 +210,7 @@ const noInfrastructureInView = {
210
210
  type: "array",
211
211
  items: {
212
212
  type: "string",
213
- pattern: "^[A-Za-z_$][\\w$]*\\.[A-Za-z_$][\\w$]*$"
213
+ pattern: String.raw`^[A-Za-z_$][\w$]*\.[A-Za-z_$][\w$]*$`
214
214
  }
215
215
  }
216
216
  },
@@ -525,28 +525,62 @@ const pluginBase = {
525
525
  }
526
526
  };
527
527
  //#endregion
528
- //#region src/configs/severities.ts
529
- /**
530
- * Re-emits a rules map with every active severity downgraded to "warn".
531
- * Used to surface advisory plugins (react-doctor) as warnings instead of hard
532
- * errors that would break the gate on pre-existing patterns.
533
- * @param rules - source rules map keyed by rule name.
534
- * @returns the same rules with non-off severities set to "warn".
535
- */
536
- function downgradeRuleSeverities(rules) {
537
- return Object.fromEntries(Object.entries(rules).map(([ruleName, ruleValue]) => {
538
- if (ruleValue === "off" || ruleValue === 0) return [ruleName, "off"];
539
- if (Array.isArray(ruleValue)) return [ruleName, ["warn", ...ruleValue.slice(1)]];
540
- return [ruleName, "warn"];
541
- }));
542
- }
543
- //#endregion
544
528
  //#region src/configs/recommended/recommended.constants.ts
545
529
  /**
546
530
  * The @typescript-eslint plugin object types its rules more richly than core
547
531
  * ESLint's Plugin contract; the runtime shape is compatible.
548
532
  */
549
533
  const typescriptPlugin = tsPlugin;
534
+ const tsPluginConfigs = tsPlugin.configs;
535
+ /**
536
+ * @typescript-eslint's recommended (non-type-checked) rules — the plugin's own
537
+ * per-rule triage (20 definite-bug rules at "error"), spread as-is across all
538
+ * maintained TypeScript. The error-set is locked by the drift test in
539
+ * upstream-severity-drift.test.ts; the plugin is pinned to an exact version.
540
+ */
541
+ const typescriptRecommendedRules = tsPluginConfigs["recommended"]?.rules ?? {};
542
+ /**
543
+ * @typescript-eslint's type-checked-only tier — the highest-value bug rules a
544
+ * real TypeScript program unlocks (no-floating-promises, no-misused-promises,
545
+ * await-thenable, the no-unsafe-* family), upstream severities respected
546
+ * as-is. Applied only where the project service can build a program
547
+ * (production src); locked by the same drift test.
548
+ */
549
+ const typescriptTypeCheckedOnlyRules = tsPluginConfigs["recommended-type-checked-only"]?.rules ?? {};
550
+ /**
551
+ * sonarjs' recommended rules — the plugin author's per-rule triage (206 rules
552
+ * at "error", 62 off upstream), spread AS-IS. The preset never
553
+ * blanket-downgrades a bundled plugin: only the named rules in
554
+ * sonarjsSurgicalOverrides are re-tuned, each with a documented reason. The
555
+ * upstream error-set is locked by the drift test and sonarjs is pinned exact.
556
+ */
557
+ const sonarjsRecommendedRules = sonarjsPlugin.configs.recommended?.rules ?? {};
558
+ /**
559
+ * Surgical severity overrides for sonarjs' recommended set — the ONLY rules
560
+ * re-tuned from upstream, each a deliberate decision with a reason. Everything
561
+ * else in the 206-rule error tier blocks, exactly as sonarjs triaged it.
562
+ */
563
+ const sonarjsSurgicalOverrides = {
564
+ "sonarjs/fixme-tag": "warn",
565
+ "sonarjs/todo-tag": "warn",
566
+ "sonarjs/no-unused-vars": "off",
567
+ "sonarjs/pseudo-random": "warn"
568
+ };
569
+ /**
570
+ * sonarjs rules that misfire specifically on test code: fixtures legitimately
571
+ * contain fake credentials, fake secrets, and literal IPs. This MIRRORS
572
+ * SonarQube Cloud behavior — files under `sonar.tests` get a reduced rule
573
+ * profile there and these main-code rules do not run on them — so local
574
+ * verdicts and Cloud verdicts stay aligned. Everything else in sonarjs stays
575
+ * ON in tests — no-exclusive-tests, no-skipped-tests, and the assertion rules
576
+ * earn their keep there.
577
+ */
578
+ const sonarjsTestContextOverrides = {
579
+ "sonarjs/hardcoded-secret-signatures": "off",
580
+ "sonarjs/no-hardcoded-ip": "off",
581
+ "sonarjs/no-hardcoded-passwords": "off",
582
+ "sonarjs/no-hardcoded-secrets": "off"
583
+ };
550
584
  /** Extensions the import-x node resolver probes when resolving specifiers. */
551
585
  const importXExtensions = [
552
586
  ".js",
@@ -586,6 +620,21 @@ const documentationContexts = [
586
620
  "ExportDefaultDeclaration > ArrowFunctionExpression",
587
621
  "ExportDefaultDeclaration > CallExpression > ArrowFunctionExpression"
588
622
  ];
623
+ /**
624
+ * Surgical severity overrides for react-doctor's recommended set.
625
+ *
626
+ * POLICY — never blanket-downgrade a third-party plugin's ruleset. react-doctor
627
+ * already triages every rule it ships (upstream: 34 error / 198 warn), and that
628
+ * per-rule judgment is exactly the work this package exists to do for consumers.
629
+ * The preset therefore spreads react-doctor's recommended rules AS-IS and
630
+ * overrides ONLY the specific rule IDs that genuinely misfire in this
631
+ * architecture — each entry below is a deliberate decision with a documented
632
+ * reason, not a bulk sweep. The upstream error-set is locked by a drift test
633
+ * (`recommended.test.ts`), and react-doctor is pinned to an exact version, so
634
+ * the set cannot shift silently: a bump that changes it fails CI and forces a
635
+ * re-triage here.
636
+ */
637
+ const reactDoctorSurgicalOverrides = { "react-doctor/react-compiler-no-manual-memoization": "off" };
589
638
  /** Full dlinter rule map at severity "off" — the final reset applied to test files. */
590
639
  const allDlinterRulesOff = {
591
640
  "dlinter/composition-only-delivery": "off",
@@ -614,7 +663,6 @@ function createRecommendedConfig(options = {}) {
614
663
  importPatterns: [...options.infrastructure.importPatterns],
615
664
  runtimeGlobals: [...options.infrastructure.runtimeGlobals]
616
665
  }] : "off";
617
- const reactDoctorWarnRules = downgradeRuleSeverities(reactDoctor.configs.recommended.rules);
618
666
  return [
619
667
  js.configs.recommended,
620
668
  importX.flatConfigs.recommended,
@@ -656,11 +704,8 @@ function createRecommendedConfig(options = {}) {
656
704
  "import-x/no-cycle": ["error", { maxDepth: 1 }],
657
705
  "import-x/no-duplicates": "error",
658
706
  "import-x/no-unresolved": "error",
659
- "sonarjs/cognitive-complexity": ["warn", 15],
660
- "sonarjs/no-all-duplicated-branches": "warn",
661
- "sonarjs/no-identical-functions": "warn",
662
- "sonarjs/no-redundant-boolean": "warn",
663
- "sonarjs/no-small-switch": "warn"
707
+ ...sonarjsRecommendedRules,
708
+ ...sonarjsSurgicalOverrides
664
709
  }
665
710
  },
666
711
  {
@@ -675,6 +720,7 @@ function createRecommendedConfig(options = {}) {
675
720
  files: ["**/*.ts", "**/*.tsx"],
676
721
  plugins: { "@typescript-eslint": typescriptPlugin },
677
722
  rules: {
723
+ ...typescriptRecommendedRules,
678
724
  "max-lines": ["error", {
679
725
  max: 500,
680
726
  skipBlankLines: true,
@@ -689,6 +735,13 @@ function createRecommendedConfig(options = {}) {
689
735
  }]
690
736
  }
691
737
  },
738
+ {
739
+ files: ["src/**/*.{ts,tsx}"],
740
+ ignores: productionTestGlobs,
741
+ languageOptions: { parserOptions: { projectService: true } },
742
+ plugins: { "@typescript-eslint": typescriptPlugin },
743
+ rules: { ...typescriptTypeCheckedOnlyRules }
744
+ },
692
745
  {
693
746
  files: productionTestGlobs,
694
747
  plugins: { "@typescript-eslint": typescriptPlugin },
@@ -716,8 +769,9 @@ function createRecommendedConfig(options = {}) {
716
769
  ignores: productionTestGlobs,
717
770
  plugins: { "react-doctor": reactDoctor },
718
771
  rules: {
719
- ...reactDoctorWarnRules,
720
- "react-doctor/react-compiler-no-manual-memoization": "off"
772
+ ...reactDoctor.configs.recommended.rules,
773
+ ...reactDoctorSurgicalOverrides,
774
+ ...options.reactCompiler ? { "react-doctor/react-compiler-no-manual-memoization": reactDoctor.configs.recommended.rules["react-doctor/react-compiler-no-manual-memoization"] ?? "warn" } : {}
721
775
  }
722
776
  },
723
777
  {
@@ -791,7 +845,7 @@ function createRecommendedConfig(options = {}) {
791
845
  },
792
846
  {
793
847
  files: ["src/**/use-*.ts"],
794
- ignores: productionTestGlobs,
848
+ ignores: governedMainModuleExemptGlobs,
795
849
  plugins: { dlinter: pluginBase },
796
850
  rules: {
797
851
  "dlinter/hook-anatomy": "error",
@@ -822,7 +876,8 @@ function createRecommendedConfig(options = {}) {
822
876
  },
823
877
  rules: {
824
878
  ...allDlinterRulesOff,
825
- "jsdoc/require-jsdoc": "off"
879
+ "jsdoc/require-jsdoc": "off",
880
+ ...sonarjsTestContextOverrides
826
881
  }
827
882
  }
828
883
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dlinter-ts-react",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Deterministic architecture governance for TypeScript + React projects: custom ESLint rules, architecture-concept presets, and pre-commit gate scaffolding.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,6 +35,7 @@
35
35
  "typecheck": "tsc --noEmit",
36
36
  "test": "vitest run",
37
37
  "test:watch": "vitest",
38
+ "test:coverage": "vitest run --coverage",
38
39
  "validate": "bun run typecheck && bun run test",
39
40
  "e2e:pack": "node ./scripts/pack-e2e.mjs",
40
41
  "prepublishOnly": "bun run validate && bun run build"
@@ -56,6 +57,7 @@
56
57
  "devDependencies": {
57
58
  "@types/estree": "^1.0.9",
58
59
  "@types/node": "^26.1.1",
60
+ "@vitest/coverage-v8": "^4.1.10",
59
61
  "eslint": "^10.6.0",
60
62
  "fallow": "^3.3.0",
61
63
  "lefthook": "^2.1.10",
@@ -72,8 +74,8 @@
72
74
  "eslint-plugin-import-x": "^4.17.1",
73
75
  "eslint-plugin-jsdoc": "^63.0.13",
74
76
  "eslint-plugin-react": "^7.37.5",
75
- "eslint-plugin-react-doctor": "^0.7.4",
77
+ "eslint-plugin-react-doctor": "0.7.4",
76
78
  "eslint-plugin-react-hooks": "^7.1.1",
77
- "eslint-plugin-sonarjs": "^4.1.0"
79
+ "eslint-plugin-sonarjs": "4.1.0"
78
80
  }
79
81
  }