dlinter-ts-react 0.2.0 → 0.3.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
@@ -132,13 +132,21 @@ bun run validate # typecheck + test
132
132
 
133
133
  The repo self-hosts its own gate (lefthook: fallow audit + typecheck + test). Contributions follow strict TDD — a rule without a failing test proving it fires is a disabled rule — and conventional commits.
134
134
 
135
- ### Releasing
135
+ ### Self-governance
136
+
137
+ This package enforces its own rules on itself. A permanent test (`src/__tests__/self-governance.test.ts`, part of `bun run validate`) lints `src/**` with the universal subset of the shipped rules — strict colocation, folder ownership, pure barrels, exported-variable JSDoc, `max-lines` — plus a layer contract on import direction:
136
138
 
137
- 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 with provenance — after the tarball end-to-end gate passes.
139
+ ```
140
+ src/index.ts → src/configs/ → src/plugin.ts → src/rules/ (src/cli/ is a standalone bin)
141
+ ```
142
+
143
+ Every commit and release must pass the same architecture this package sells. Two documented exceptions: rule modules are exported const objects (the ESLint plugin contract requires it), and the package entrypoints (`main`, `bin`) are composition modules, not barrels.
144
+
145
+ ### Releasing
138
146
 
139
- Manual escape hatch: pushing a `v*` tag triggers the same publish pipeline directly.
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.
140
148
 
141
- Requires the `NPM_TOKEN` secret in the repository settings.
149
+ 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).
142
150
 
143
151
  ## Roadmap
144
152
 
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, writeFileSync } from "node:fs";
3
3
  import path from "node:path";
4
- //#region src/cli/init.ts
4
+ //#region src/cli/init/init.constants.ts
5
+ /** Default lefthook pre-commit gate scaffolded into consumer projects. */
5
6
  const lefthookTemplate = `pre-commit:
6
7
  parallel: false
7
8
  jobs:
@@ -14,6 +15,8 @@ const lefthookTemplate = `pre-commit:
14
15
  - name: test
15
16
  run: npm run test
16
17
  `;
18
+ //#endregion
19
+ //#region src/cli/init/init.ts
17
20
  /**
18
21
  * Scaffolds the dlinter pre-commit gate into a consumer project. Existing
19
22
  * files are never overwritten — a hand-tuned gate always wins over the template.
package/dist/index.d.mts CHANGED
@@ -1106,7 +1106,7 @@ interface Directive {
1106
1106
  justification?: string;
1107
1107
  }
1108
1108
  //#endregion
1109
- //#region src/configs/recommended.types.d.ts
1109
+ //#region src/configs/recommended/recommended.types.d.ts
1110
1110
  /**
1111
1111
  * Consumer-defined infrastructure edge: which import specifiers and runtime
1112
1112
  * globals count as "infrastructure" that views must never touch directly.
@@ -1130,7 +1130,7 @@ interface RecommendedConfigOptions {
1130
1130
  readonly tsconfigPath?: string;
1131
1131
  }
1132
1132
  //#endregion
1133
- //#region src/configs/recommended.d.ts
1133
+ //#region src/configs/recommended/recommended.d.ts
1134
1134
  /**
1135
1135
  * Builds the full governance preset: bundled third-party plugin stack plus
1136
1136
  * every dlinter architecture rule, composed with the glob topology proven in
package/dist/index.mjs CHANGED
@@ -1,6 +1,5 @@
1
1
  import tsParser from "@typescript-eslint/parser";
2
2
  import js from "@eslint/js";
3
- import tsPlugin from "@typescript-eslint/eslint-plugin";
4
3
  import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript";
5
4
  import checkFilePlugin from "eslint-plugin-check-file";
6
5
  import { createNodeResolver, importX } from "eslint-plugin-import-x";
@@ -11,6 +10,7 @@ import reactHooksPlugin from "eslint-plugin-react-hooks";
11
10
  import sonarjsPlugin from "eslint-plugin-sonarjs";
12
11
  import { existsSync } from "node:fs";
13
12
  import path from "node:path";
13
+ import tsPlugin from "@typescript-eslint/eslint-plugin";
14
14
  //#region src/configs/dumb-ui.ts
15
15
  /**
16
16
  * Dumb UI preset: every `.tsx` file is presentational. Side effects live in
@@ -33,9 +33,13 @@ function createDumbUiConfig(plugin) {
33
33
  }];
34
34
  }
35
35
  //#endregion
36
- //#region src/rules/composition-only-delivery.ts
36
+ //#region src/rules/composition-only-delivery/composition-only-delivery.constants.ts
37
+ /** Matches every built-in React hook name, including bare `use`. */
37
38
  const reactHookNamePattern = "^use(State|Reducer|Effect|LayoutEffect|InsertionEffect|SyncExternalStore|Memo|Callback|Ref|Context|Transition|DeferredValue|ImperativeHandle|DebugValue|Id|Optimistic|ActionState)?$";
39
+ /** Matches module specifiers that resolve to a custom `use-*` hook file. */
38
40
  const customHookModulePattern = /(^|\/)use-[^/]+$/u;
41
+ //#endregion
42
+ //#region src/rules/composition-only-delivery/composition-only-delivery.ts
39
43
  /**
40
44
  * Delivery Layer Rule: delivery files (App.tsx, app/**) compose feature
41
45
  * entrypoints — they never orchestrate. No React hooks, no custom hook
@@ -76,13 +80,16 @@ const compositionOnlyDelivery = {
76
80
  }
77
81
  };
78
82
  //#endregion
79
- //#region src/rules/folder-ownership.ts
83
+ //#region src/rules/folder-ownership/folder-ownership.constants.ts
84
+ /** Role-file suffixes a split main module may own next to its entrypoint. */
80
85
  const roleSuffixes = [
81
86
  ".constants.ts",
82
87
  ".helpers.ts",
83
88
  ".schema.ts",
84
89
  ".types.ts"
85
90
  ];
91
+ //#endregion
92
+ //#region src/rules/folder-ownership/folder-ownership.ts
86
93
  /**
87
94
  * Split Module Ownership Rule: when a main module splits into role files
88
95
  * (`*.helpers.ts`, `*.types.ts`, `*.constants.ts`, `*.schema.ts`), the whole
@@ -124,8 +131,11 @@ const folderOwnership = {
124
131
  }
125
132
  };
126
133
  //#endregion
127
- //#region src/rules/hook-anatomy.ts
134
+ //#region src/rules/hook-anatomy/hook-anatomy.constants.ts
135
+ /** Selector prefix targeting the body of every exported `use*` hook declaration. */
128
136
  const hookBlock = "ExportNamedDeclaration > FunctionDeclaration[id.name=/^use[A-Z0-9]/] > BlockStatement";
137
+ //#endregion
138
+ //#region src/rules/hook-anatomy/hook-anatomy.ts
129
139
  /**
130
140
  * Hook Anatomy Rule: exported hooks follow one ordering — derived state
131
141
  * (useMemo) before callbacks (useCallback) before effects (useEffect) — and
@@ -243,8 +253,11 @@ const noInfrastructureInView = {
243
253
  }
244
254
  };
245
255
  //#endregion
246
- //#region src/rules/no-view-effects.ts
256
+ //#region src/rules/no-view-effects/no-view-effects.constants.ts
257
+ /** Matches the React effect hooks that are forbidden inside view components. */
247
258
  const effectHookPattern = /^use(?:Effect|LayoutEffect)$/u;
259
+ //#endregion
260
+ //#region src/rules/no-view-effects/no-view-effects.ts
248
261
  /**
249
262
  * Dumb UI Rule: view components (.tsx) must stay presentational. Side effects
250
263
  * belong in the colocated `use-*.ts` hook, never in the view itself.
@@ -299,8 +312,11 @@ const pureIndexBarrel = {
299
312
  }
300
313
  };
301
314
  //#endregion
302
- //#region src/rules/readonly-props.ts
315
+ //#region src/rules/readonly-props/readonly-props.constants.ts
316
+ /** Selector fragment matching parameters annotated with a bare `*Props` type reference. */
303
317
  const propsBoundary = "[typeAnnotation.typeAnnotation.type=\"TSTypeReference\"][typeAnnotation.typeAnnotation.typeName.name=/Props$/]";
318
+ //#endregion
319
+ //#region src/rules/readonly-props/readonly-props.ts
304
320
  /**
305
321
  * Type Contract Rule: component props are immutable. Parameters use
306
322
  * `Readonly<Props>` at the function boundary, and every field of a `*Props`
@@ -366,7 +382,11 @@ const requireExportedVariableJsdoc = {
366
382
  }
367
383
  };
368
384
  //#endregion
369
- //#region src/rules/strict-colocation.ts
385
+ //#region src/rules/strict-colocation/strict-colocation.constants.ts
386
+ /**
387
+ * Every check id the rule can enforce; the `checks` option narrows this set
388
+ * for role files that legitimately own a subset of declaration kinds.
389
+ */
370
390
  const allChecks = [
371
391
  "root-variable",
372
392
  "root-helper-function",
@@ -376,13 +396,86 @@ const allChecks = [
376
396
  "inline-type-alias",
377
397
  "zod-import"
378
398
  ];
399
+ /** Matches `zod` itself and any `zod/*` subpath import. */
379
400
  const zodImportPattern = /^zod(?:\/.*)?$/u;
380
401
  //#endregion
402
+ //#region src/rules/strict-colocation/strict-colocation.helpers.ts
403
+ /**
404
+ * Collects every identifier a module exports through specifier statements —
405
+ * `export default App` and `export { App }` — so root function declarations
406
+ * exported that way are recognized as main modules rather than helpers.
407
+ */
408
+ function collectSpecifierExportedNames(program) {
409
+ const names = /* @__PURE__ */ new Set();
410
+ for (const statement of program.body) {
411
+ if (statement.type === "ExportDefaultDeclaration" && statement.declaration.type === "Identifier") names.add(statement.declaration.name);
412
+ if (statement.type === "ExportNamedDeclaration" && statement.declaration === null) {
413
+ for (const specifier of statement.specifiers) if (specifier.local.type === "Identifier") names.add(specifier.local.name);
414
+ }
415
+ }
416
+ return names;
417
+ }
418
+ /**
419
+ * Builds the visitor fragment owned by each check id; the rule merges the
420
+ * fragments for whichever checks its options enable.
421
+ */
422
+ function createCheckVisitors(context) {
423
+ return {
424
+ "root-variable": { "Program > VariableDeclaration": (node) => {
425
+ context.report({
426
+ node,
427
+ messageId: "rootVariable"
428
+ });
429
+ } },
430
+ "root-helper-function": { "Program > FunctionDeclaration": (node) => {
431
+ if (node.type === "FunctionDeclaration" && node.parent.type === "Program") {
432
+ const exportedNames = collectSpecifierExportedNames(node.parent);
433
+ if (node.id !== null && exportedNames.has(node.id.name)) return;
434
+ }
435
+ context.report({
436
+ node,
437
+ messageId: "rootHelperFunction"
438
+ });
439
+ } },
440
+ "exported-const": { "Program > ExportNamedDeclaration > VariableDeclaration": (node) => {
441
+ context.report({
442
+ node,
443
+ messageId: "exportedConst"
444
+ });
445
+ } },
446
+ "default-arrow-export": { "Program > ExportDefaultDeclaration > ArrowFunctionExpression": (node) => {
447
+ context.report({
448
+ node,
449
+ messageId: "defaultArrowExport"
450
+ });
451
+ } },
452
+ "inline-interface": { TSInterfaceDeclaration: (node) => {
453
+ context.report({
454
+ node,
455
+ messageId: "inlineInterface"
456
+ });
457
+ } },
458
+ "inline-type-alias": { TSTypeAliasDeclaration: (node) => {
459
+ context.report({
460
+ node,
461
+ messageId: "inlineTypeAlias"
462
+ });
463
+ } },
464
+ "zod-import": { ImportDeclaration: (node) => {
465
+ if (node.type === "ImportDeclaration" && zodImportPattern.test(String(node.source.value))) context.report({
466
+ node,
467
+ messageId: "zodImport"
468
+ });
469
+ } }
470
+ };
471
+ }
472
+ //#endregion
381
473
  //#region src/plugin.ts
382
474
  /**
383
- * Plugin core shared by every preset: metadata plus the dlinter rule registry.
384
- * Presets receive this object so config factories never import the package
385
- * entrypoint back (which would create a module cycle).
475
+ * Plugin core shared by every preset: metadata plus the dlinter rule registry
476
+ * keyed by published rule name. Presets receive this object so config
477
+ * factories never import the package entrypoint back (which would create a
478
+ * module cycle).
386
479
  */
387
480
  const pluginBase = {
388
481
  meta: {
@@ -423,49 +516,9 @@ const pluginBase = {
423
516
  create(context) {
424
517
  const configuredChecks = context.options[0]?.checks ?? allChecks;
425
518
  const enabled = new Set(configuredChecks);
519
+ const checkVisitors = createCheckVisitors(context);
426
520
  const visitor = {};
427
- if (enabled.has("root-variable")) visitor["Program > VariableDeclaration"] = (node) => {
428
- context.report({
429
- node,
430
- messageId: "rootVariable"
431
- });
432
- };
433
- if (enabled.has("root-helper-function")) visitor["Program > FunctionDeclaration"] = (node) => {
434
- context.report({
435
- node,
436
- messageId: "rootHelperFunction"
437
- });
438
- };
439
- if (enabled.has("exported-const")) visitor["Program > ExportNamedDeclaration > VariableDeclaration"] = (node) => {
440
- context.report({
441
- node,
442
- messageId: "exportedConst"
443
- });
444
- };
445
- if (enabled.has("default-arrow-export")) visitor["Program > ExportDefaultDeclaration > ArrowFunctionExpression"] = (node) => {
446
- context.report({
447
- node,
448
- messageId: "defaultArrowExport"
449
- });
450
- };
451
- if (enabled.has("inline-interface")) visitor["TSInterfaceDeclaration"] = (node) => {
452
- context.report({
453
- node,
454
- messageId: "inlineInterface"
455
- });
456
- };
457
- if (enabled.has("inline-type-alias")) visitor["TSTypeAliasDeclaration"] = (node) => {
458
- context.report({
459
- node,
460
- messageId: "inlineTypeAlias"
461
- });
462
- };
463
- if (enabled.has("zod-import")) visitor["ImportDeclaration"] = (node) => {
464
- if (node.type === "ImportDeclaration" && zodImportPattern.test(String(node.source.value))) context.report({
465
- node,
466
- messageId: "zodImport"
467
- });
468
- };
521
+ for (const check of allChecks) if (enabled.has(check)) Object.assign(visitor, checkVisitors[check]);
469
522
  return visitor;
470
523
  }
471
524
  }
@@ -488,8 +541,13 @@ function downgradeRuleSeverities(rules) {
488
541
  }));
489
542
  }
490
543
  //#endregion
491
- //#region src/configs/recommended.ts
544
+ //#region src/configs/recommended/recommended.constants.ts
545
+ /**
546
+ * The @typescript-eslint plugin object types its rules more richly than core
547
+ * ESLint's Plugin contract; the runtime shape is compatible.
548
+ */
492
549
  const typescriptPlugin = tsPlugin;
550
+ /** Extensions the import-x node resolver probes when resolving specifiers. */
493
551
  const importXExtensions = [
494
552
  ".js",
495
553
  ".jsx",
@@ -497,15 +555,20 @@ const importXExtensions = [
497
555
  ".tsx",
498
556
  ".d.ts"
499
557
  ];
558
+ /** Globs identifying test files, which stay outside the architecture contract. */
500
559
  const productionTestGlobs = [
501
560
  "**/__tests__/**/*.{ts,tsx}",
502
561
  "**/*.test.{ts,tsx}",
503
562
  "src/test/**/*.{ts,tsx}",
504
563
  "test/**/*.{ts,tsx}"
505
564
  ];
565
+ /** Globs for repository automation scripts that run under Node. */
506
566
  const nodeScriptGlobs = ["scripts/**/*.{js,mjs,cjs}"];
567
+ /** Globs for application source that runs in the browser. */
507
568
  const sourceBrowserGlobs = ["src/**/*.{ts,tsx,js,jsx}"];
569
+ /** Files exempt from the public-documentation contract: tests and documented-invalid fixtures. */
508
570
  const documentationExemptGlobs = [...productionTestGlobs, "**/*.invalid.{ts,tsx,js,jsx}"];
571
+ /** Files that are not governed main modules: exempt files plus barrels and role files. */
509
572
  const governedMainModuleExemptGlobs = [
510
573
  ...documentationExemptGlobs,
511
574
  "**/index.ts",
@@ -514,6 +577,7 @@ const governedMainModuleExemptGlobs = [
514
577
  "**/*.schema.ts",
515
578
  "**/*.types.ts"
516
579
  ];
580
+ /** AST selector contexts that `jsdoc/require-jsdoc` treats as public documentation surface. */
517
581
  const documentationContexts = [
518
582
  "ExportNamedDeclaration > FunctionDeclaration",
519
583
  "ExportNamedDeclaration > TSInterfaceDeclaration",
@@ -522,6 +586,7 @@ const documentationContexts = [
522
586
  "ExportDefaultDeclaration > ArrowFunctionExpression",
523
587
  "ExportDefaultDeclaration > CallExpression > ArrowFunctionExpression"
524
588
  ];
589
+ /** Full dlinter rule map at severity "off" — the final reset applied to test files. */
525
590
  const allDlinterRulesOff = {
526
591
  "dlinter/composition-only-delivery": "off",
527
592
  "dlinter/folder-ownership": "off",
@@ -533,6 +598,8 @@ const allDlinterRulesOff = {
533
598
  "dlinter/require-exported-variable-jsdoc": "off",
534
599
  "dlinter/strict-colocation": "off"
535
600
  };
601
+ //#endregion
602
+ //#region src/configs/recommended/recommended.ts
536
603
  /**
537
604
  * Builds the full governance preset: bundled third-party plugin stack plus
538
605
  * every dlinter architecture rule, composed with the glob topology proven in
@@ -741,7 +808,11 @@ function createRecommendedConfig(options = {}) {
741
808
  files: ["src/**/*.helpers.ts"],
742
809
  ignores: productionTestGlobs,
743
810
  plugins: { dlinter: pluginBase },
744
- rules: { "dlinter/strict-colocation": ["error", { checks: ["inline-interface", "inline-type-alias"] }] }
811
+ rules: { "dlinter/strict-colocation": ["error", { checks: [
812
+ "root-variable",
813
+ "inline-interface",
814
+ "inline-type-alias"
815
+ ] }] }
745
816
  },
746
817
  {
747
818
  files: productionTestGlobs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dlinter-ts-react",
3
- "version": "0.2.0",
3
+ "version": "0.3.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": {
@@ -25,7 +25,7 @@
25
25
  }
26
26
  },
27
27
  "bin": {
28
- "dlinter": "./dist/cli/index.mjs"
28
+ "dlinter": "dist/cli/index.mjs"
29
29
  },
30
30
  "files": [
31
31
  "dist"
@@ -54,6 +54,7 @@
54
54
  "typescript": ">=5.0.0 <6.1.0"
55
55
  },
56
56
  "devDependencies": {
57
+ "@types/estree": "^1.0.9",
57
58
  "@types/node": "^26.1.1",
58
59
  "eslint": "^10.6.0",
59
60
  "fallow": "^3.3.0",