dlinter-ts-react 0.3.1 → 0.4.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.
- package/README.md +20 -3
- package/dist/cli/index.mjs +6 -3
- package/dist/index.d.mts +8 -0
- package/dist/index.mjs +81 -26
- package/package.json +5 -3
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`
|
|
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
|
-
|
|
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
|
|
package/dist/cli/index.mjs
CHANGED
|
@@ -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
|
-
|
|
56
|
-
|
|
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:
|
|
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
|
-
|
|
660
|
-
|
|
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
|
-
...
|
|
720
|
-
|
|
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
|
{
|
|
@@ -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
|
+
"version": "0.4.0",
|
|
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": "
|
|
77
|
+
"eslint-plugin-react-doctor": "0.7.4",
|
|
76
78
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
77
|
-
"eslint-plugin-sonarjs": "
|
|
79
|
+
"eslint-plugin-sonarjs": "4.1.0"
|
|
78
80
|
}
|
|
79
81
|
}
|