@geonosis/oxlint-plugin-biological-architecture 3.0.0 → 3.2.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 (239) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +48 -5
  3. package/RULES.md +201 -98
  4. package/corpus/.oxlintrc.json +6 -0
  5. package/corpus/data/bad-dynamic-orm.ts +5 -0
  6. package/corpus/data/bad-type-only-orm.ts +3 -0
  7. package/corpus/e2e/bad-list-response.spec.ts +5 -0
  8. package/corpus/e2e/bad-timeout.spec.ts +5 -0
  9. package/corpus/e2e/good-detail-response.spec.ts +5 -0
  10. package/corpus/e2e/good-state.spec.ts +5 -0
  11. package/corpus/icon-controls/bad-unnamed-icon-button.tsx +8 -0
  12. package/corpus/icon-controls/good-named-icon-button.tsx +19 -0
  13. package/corpus/manifest.json +3 -0
  14. package/corpus/tissues/bad-awaits-data.tsx +8 -0
  15. package/corpus/tissues/bad-fetches-data.tsx +7 -0
  16. package/corpus/tissues/good-takes-products.tsx +5 -0
  17. package/dist/discover-atoms.d.ts +2 -37
  18. package/dist/discover-atoms.js +1 -104
  19. package/dist/index.d.ts +3 -12
  20. package/dist/index.js +19 -174
  21. package/dist/presets.d.ts +11 -19
  22. package/dist/presets.js +89 -3
  23. package/dist/registry.d.ts +2 -0
  24. package/dist/registry.js +172 -0
  25. package/dist/rules/atom-no-deps.d.ts +1 -0
  26. package/dist/rules/atom-no-deps.js +10 -11
  27. package/dist/rules/boundary-time-is-primitive.d.ts +0 -7
  28. package/dist/rules/cell-must-be-stateful.d.ts +2 -2
  29. package/dist/rules/cell-must-be-stateful.js +24 -23
  30. package/dist/rules/cell-must-not-compose-cell.d.ts +1 -0
  31. package/dist/rules/cell-must-not-compose-cell.js +10 -3
  32. package/dist/rules/cell-no-cross-feature-organelles.d.ts +1 -0
  33. package/dist/rules/cell-no-cross-feature-organelles.js +7 -0
  34. package/dist/rules/cell-no-tissues.d.ts +1 -0
  35. package/dist/rules/cell-no-tissues.js +11 -7
  36. package/dist/rules/cells-folder-index-is-barrel.d.ts +12 -10
  37. package/dist/rules/cells-folder-index-is-barrel.js +66 -34
  38. package/dist/rules/compound-must-be-stateless.d.ts +2 -0
  39. package/dist/rules/compound-must-be-stateless.js +24 -26
  40. package/dist/rules/compound-no-stores.d.ts +1 -0
  41. package/dist/rules/compound-no-stores.js +12 -10
  42. package/dist/rules/constants-in-constants-file.d.ts +2 -8
  43. package/dist/rules/constants-in-constants-file.js +5 -6
  44. package/dist/rules/dialect-through-the-seam.d.ts +0 -13
  45. package/dist/rules/durable-body-reads-through-steps.d.ts +0 -14
  46. package/dist/rules/durable-body-reads-through-steps.js +8 -3
  47. package/dist/rules/effect-hook-naming.d.ts +9 -11
  48. package/dist/rules/effect-hook-naming.js +33 -31
  49. package/dist/rules/emit-declares-attempts.d.ts +0 -17
  50. package/dist/rules/emit-declares-attempts.js +5 -10
  51. package/dist/rules/font-roles-only.d.ts +0 -18
  52. package/dist/rules/font-roles-only.js +19 -22
  53. package/dist/rules/hermetic-e2e-no-wall-clock-waits.d.ts +53 -0
  54. package/dist/rules/hermetic-e2e-no-wall-clock-waits.js +100 -0
  55. package/dist/rules/icon-control-has-name.d.ts +53 -0
  56. package/dist/rules/icon-control-has-name.js +71 -0
  57. package/dist/rules/layer-walls.d.ts +0 -25
  58. package/dist/rules/layer-walls.js +7 -13
  59. package/dist/rules/lib/atom-map.d.ts +24 -0
  60. package/dist/rules/lib/atom-map.js +16 -0
  61. package/dist/rules/lib/cells-barrel.d.ts +0 -1
  62. package/dist/rules/lib/colors.d.ts +0 -19
  63. package/dist/rules/lib/colors.js +10 -243
  64. package/dist/rules/lib/comments.d.ts +19 -0
  65. package/dist/rules/lib/comments.js +5 -0
  66. package/dist/rules/lib/context.d.ts +0 -10
  67. package/dist/rules/lib/direction-rule.d.ts +3 -21
  68. package/dist/rules/lib/direction-rule.js +12 -3
  69. package/dist/rules/lib/doors.d.ts +2 -18
  70. package/dist/rules/lib/doors.js +79 -41
  71. package/dist/rules/lib/hooks.d.ts +2 -17
  72. package/dist/rules/lib/hooks.js +14 -9
  73. package/dist/rules/lib/imports.d.ts +2 -23
  74. package/dist/rules/lib/imports.js +7 -2
  75. package/dist/rules/lib/jsx.d.ts +31 -0
  76. package/dist/rules/lib/jsx.js +11 -0
  77. package/dist/rules/lib/landing.d.ts +0 -40
  78. package/dist/rules/lib/literal-call.d.ts +1 -0
  79. package/dist/rules/lib/literal-call.js +2 -0
  80. package/dist/rules/lib/module-text.d.ts +3 -27
  81. package/dist/rules/lib/module-text.js +19 -4
  82. package/dist/rules/lib/mutations.d.ts +0 -14
  83. package/dist/rules/lib/nodes.d.ts +1 -18
  84. package/dist/rules/lib/nodes.js +1 -0
  85. package/dist/rules/lib/objects.d.ts +2 -11
  86. package/dist/rules/lib/objects.js +21 -0
  87. package/dist/rules/lib/options.d.ts +2 -52
  88. package/dist/rules/lib/options.js +44 -18
  89. package/dist/rules/lib/package-imports.d.ts +0 -5
  90. package/dist/rules/lib/package-imports.js +4 -10
  91. package/dist/rules/lib/paths.d.ts +15 -41
  92. package/dist/rules/lib/paths.js +34 -1
  93. package/dist/rules/lib/pattern-match.d.ts +2 -12
  94. package/dist/rules/lib/pattern-match.js +5 -1
  95. package/dist/rules/lib/probe.d.ts +0 -43
  96. package/dist/rules/lib/reexports.d.ts +0 -18
  97. package/dist/rules/lib/render-tracking.d.ts +0 -8
  98. package/dist/rules/lib/shared-features.d.ts +0 -8
  99. package/dist/rules/lib/shared-features.js +2 -9
  100. package/dist/rules/lib/state-sources.d.ts +8 -0
  101. package/dist/rules/lib/state-sources.js +55 -0
  102. package/dist/rules/lib/steps.d.ts +3 -5
  103. package/dist/rules/lib/steps.js +6 -1
  104. package/dist/rules/lib/styling.d.ts +9 -42
  105. package/dist/rules/lib/styling.js +36 -10
  106. package/dist/rules/lib/tiers.d.ts +11 -24
  107. package/dist/rules/lib/tiers.js +41 -1
  108. package/dist/rules/lib/tissue-entrypoint.d.ts +0 -1
  109. package/dist/rules/lib/tissue-entrypoint.js +4 -5
  110. package/dist/rules/lib/tsconfig-paths.d.ts +0 -4
  111. package/dist/rules/lib/tsconfig-paths.js +47 -15
  112. package/dist/rules/lib/type-members.d.ts +6 -0
  113. package/dist/rules/lib/type-members.js +165 -0
  114. package/dist/rules/lib/visitors.d.ts +0 -22
  115. package/dist/rules/lib/workspace-members.d.ts +2 -0
  116. package/dist/rules/lib/workspace-members.js +115 -0
  117. package/dist/rules/list-take-without-order.js +2 -8
  118. package/dist/rules/max-comment-density.d.ts +3 -18
  119. package/dist/rules/max-comment-density.js +3 -3
  120. package/dist/rules/molecule-atoms-only.d.ts +1 -0
  121. package/dist/rules/molecule-atoms-only.js +11 -7
  122. package/dist/rules/molecule-must-compose.d.ts +3 -1
  123. package/dist/rules/molecule-must-compose.js +17 -21
  124. package/dist/rules/mutating-step-declares-retry.d.ts +2 -7
  125. package/dist/rules/mutating-step-declares-retry.js +29 -11
  126. package/dist/rules/named-when-block.d.ts +0 -2
  127. package/dist/rules/named-when-block.js +2 -8
  128. package/dist/rules/next-route-segment-is-thin-delegate.d.ts +4 -19
  129. package/dist/rules/next-route-segment-is-thin-delegate.js +47 -71
  130. package/dist/rules/no-brand-names.d.ts +0 -9
  131. package/dist/rules/no-brand-names.js +1 -1
  132. package/dist/rules/no-card-shaped-div.d.ts +4 -42
  133. package/dist/rules/no-card-shaped-div.js +12 -4
  134. package/dist/rules/no-cross-feature-stores.d.ts +1 -0
  135. package/dist/rules/no-cross-feature-stores.js +9 -5
  136. package/dist/rules/no-d1-transaction.d.ts +2 -5
  137. package/dist/rules/no-d1-transaction.js +7 -0
  138. package/dist/rules/no-date-string-compare.d.ts +2 -0
  139. package/dist/rules/no-date-string-compare.js +14 -8
  140. package/dist/rules/no-duplicate-jsx-patterns.d.ts +2 -0
  141. package/dist/rules/no-duplicate-jsx-patterns.js +16 -1
  142. package/dist/rules/no-generic-utility-package.d.ts +48 -2
  143. package/dist/rules/no-generic-utility-package.js +48 -9
  144. package/dist/rules/no-hook-in-component-disguise.d.ts +27 -13
  145. package/dist/rules/no-hook-in-component-disguise.js +38 -30
  146. package/dist/rules/no-index-count-as-exact.d.ts +0 -3
  147. package/dist/rules/no-inert-hidden-jsx.d.ts +4 -21
  148. package/dist/rules/no-inert-hidden-jsx.js +13 -12
  149. package/dist/rules/no-inline-data-in-jsx.d.ts +2 -0
  150. package/dist/rules/no-inline-data-in-jsx.js +17 -2
  151. package/dist/rules/no-invalid-feature-folders.d.ts +2 -0
  152. package/dist/rules/no-invalid-feature-folders.js +12 -4
  153. package/dist/rules/no-loader-side-effect-in-index-file.d.ts +0 -7
  154. package/dist/rules/no-logic-in-component-files.d.ts +6 -5
  155. package/dist/rules/no-logic-in-component-files.js +52 -31
  156. package/dist/rules/no-mock-db-in-integration.d.ts +0 -15
  157. package/dist/rules/no-mock-db-in-integration.js +3 -4
  158. package/dist/rules/no-orm-outside-db.d.ts +1 -0
  159. package/dist/rules/no-orm-outside-db.js +9 -7
  160. package/dist/rules/no-raw-html-atoms.d.ts +3 -22
  161. package/dist/rules/no-raw-html-atoms.js +5 -17
  162. package/dist/rules/no-raw-sql-outside-allowed.d.ts +2 -0
  163. package/dist/rules/no-raw-sql-outside-allowed.js +10 -15
  164. package/dist/rules/no-react-namespace.d.ts +2 -0
  165. package/dist/rules/no-react-namespace.js +13 -0
  166. package/dist/rules/no-renamed-html-props.d.ts +2 -0
  167. package/dist/rules/no-renamed-html-props.js +11 -2
  168. package/dist/rules/no-render-prop-reader.d.ts +4 -20
  169. package/dist/rules/no-render-prop-reader.js +13 -5
  170. package/dist/rules/no-runtime-dead-affordance.d.ts +3 -26
  171. package/dist/rules/no-runtime-dead-affordance.js +7 -20
  172. package/dist/rules/no-swallowed-catch.js +2 -8
  173. package/dist/rules/no-trivial-wrapper-component.d.ts +4 -19
  174. package/dist/rules/no-trivial-wrapper-component.js +24 -33
  175. package/dist/rules/no-ts-in-bio-folders.d.ts +2 -13
  176. package/dist/rules/no-ts-in-bio-folders.js +9 -7
  177. package/dist/rules/no-type-definitions-in-components.d.ts +2 -0
  178. package/dist/rules/no-type-definitions-in-components.js +12 -1
  179. package/dist/rules/no-unregistered-plugin-dir.d.ts +0 -41
  180. package/dist/rules/no-unregistered-plugin-dir.js +1 -9
  181. package/dist/rules/no-void-port.d.ts +2 -4
  182. package/dist/rules/no-void-port.js +7 -0
  183. package/dist/rules/organelle-dependency.d.ts +1 -0
  184. package/dist/rules/organelle-dependency.js +10 -8
  185. package/dist/rules/organelle-single-source.d.ts +2 -0
  186. package/dist/rules/organelle-single-source.js +58 -9
  187. package/dist/rules/plugin-route-namespaced.d.ts +0 -11
  188. package/dist/rules/plugin-route-namespaced.js +2 -2
  189. package/dist/rules/queries-require-org-scope.d.ts +0 -3
  190. package/dist/rules/queries-require-org-scope.js +3 -10
  191. package/dist/rules/queue-loop-is-the-library.js +3 -2
  192. package/dist/rules/route-no-inline-mutation.d.ts +1 -25
  193. package/dist/rules/route-no-inline-mutation.js +2 -7
  194. package/dist/rules/router-schema-parity.d.ts +0 -9
  195. package/dist/rules/router-schema-parity.js +5 -9
  196. package/dist/rules/service-name-not-reserved.d.ts +0 -3
  197. package/dist/rules/service-name-not-reserved.js +1 -34
  198. package/dist/rules/ssot-no-inline-facts.js +2 -5
  199. package/dist/rules/ssot-no-process-env.js +4 -12
  200. package/dist/rules/step-declares-compensation-or-none.d.ts +2 -1
  201. package/dist/rules/step-declares-compensation-or-none.js +17 -1
  202. package/dist/rules/step-opens-its-own-cell.d.ts +2 -25
  203. package/dist/rules/step-opens-its-own-cell.js +8 -13
  204. package/dist/rules/store-route-scopes-tenant-data.js +8 -26
  205. package/dist/rules/subpath-over-barrel.d.ts +2 -10
  206. package/dist/rules/subpath-over-barrel.js +48 -9
  207. package/dist/rules/subscriber-declares-id.d.ts +0 -2
  208. package/dist/rules/subscriber-declares-id.js +2 -8
  209. package/dist/rules/tables-declare-their-plane.d.ts +0 -2
  210. package/dist/rules/tables-declare-their-plane.js +4 -10
  211. package/dist/rules/tenant-tables-carry-org-id.d.ts +1 -4
  212. package/dist/rules/tenant-tables-carry-org-id.js +5 -12
  213. package/dist/rules/test-no-clock-polling-for-promise-flips.d.ts +2 -0
  214. package/dist/rules/test-no-clock-polling-for-promise-flips.js +18 -1
  215. package/dist/rules/time-through-the-door.d.ts +0 -24
  216. package/dist/rules/time-through-the-door.js +5 -5
  217. package/dist/rules/tissue-must-compose.d.ts +4 -2
  218. package/dist/rules/tissue-must-compose.js +25 -20
  219. package/dist/rules/tissue-no-data-props.d.ts +56 -0
  220. package/dist/rules/tissue-no-data-props.js +114 -32
  221. package/dist/rules/tissue-no-fetch.d.ts +26 -0
  222. package/dist/rules/tissue-no-fetch.js +57 -0
  223. package/dist/rules/tissue-no-hooks.d.ts +20 -22
  224. package/dist/rules/tissue-no-hooks.js +22 -4
  225. package/dist/rules/tissue-no-organelles.d.ts +1 -0
  226. package/dist/rules/tissue-no-organelles.js +9 -1
  227. package/dist/rules/tissue-no-stores.d.ts +1 -0
  228. package/dist/rules/tissue-no-stores.js +9 -1
  229. package/dist/rules/todo-names-its-plan.d.ts +1 -28
  230. package/dist/rules/todo-names-its-plan.js +2 -2
  231. package/dist/rules/tokens-only-colors.d.ts +2 -37
  232. package/dist/rules/tokens-only-colors.js +33 -18
  233. package/dist/rules/unique-query-step-name.d.ts +0 -7
  234. package/dist/rules/unique-query-step-name.js +6 -10
  235. package/dist/rules/worker-handles-are-scoped.d.ts +11 -8
  236. package/dist/rules/worker-handles-are-scoped.js +12 -5
  237. package/dist/rules/zustand-v5-best-practices.d.ts +8 -18
  238. package/dist/rules/zustand-v5-best-practices.js +32 -56
  239. package/package.json +2 -2
@@ -1,5 +1,10 @@
1
- import { type RuleContext } from './lib/context.js';
1
+ import { type RuleContextWithOptions } from './lib/context.js';
2
+ import { type ModuleNode } from './lib/imports.js';
2
3
  import { type AnyNode } from './lib/nodes.js';
4
+ import { type Probe } from './lib/probe.js';
5
+ interface Options {
6
+ clientBoundary?: boolean | string[];
7
+ }
3
8
  interface JsxChild {
4
9
  expression?: {
5
10
  name?: string;
@@ -40,22 +45,13 @@ interface VariableDeclaratorNode {
40
45
  } | null;
41
46
  type: 'VariableDeclarator';
42
47
  }
43
- interface ImportNode {
44
- importKind?: string;
45
- loc?: unknown;
46
- source: {
47
- value: string;
48
- };
49
- type: string;
50
- }
51
48
  interface ProgramNode {
52
49
  body?: {
53
50
  directive?: string;
54
51
  }[];
55
52
  }
56
53
  declare const noHookInComponentDisguise: {
57
- create(context: RuleContext): {
58
- Program?: undefined;
54
+ create(context: RuleContextWithOptions<Options>): {
59
55
  CallExpression?: undefined;
60
56
  ImportDeclaration?: undefined;
61
57
  ArrowFunctionExpression?: undefined;
@@ -64,6 +60,7 @@ declare const noHookInComponentDisguise: {
64
60
  'FunctionDeclaration:exit'?: undefined;
65
61
  FunctionExpression?: undefined;
66
62
  'FunctionExpression:exit'?: undefined;
63
+ Program?: undefined;
67
64
  ReturnStatement?: undefined;
68
65
  VariableDeclarator?: undefined;
69
66
  } | {
@@ -74,7 +71,7 @@ declare const noHookInComponentDisguise: {
74
71
  'FunctionDeclaration:exit'(): void;
75
72
  FunctionExpression(node: FunctionLikeNode): void;
76
73
  'FunctionExpression:exit'(): void;
77
- ImportDeclaration(node: ImportNode): void;
74
+ ImportDeclaration(node: ModuleNode): void;
78
75
  Program(node: ProgramNode): void;
79
76
  ReturnStatement(node: ReturnStatementNode): void;
80
77
  VariableDeclarator(node: VariableDeclaratorNode): void;
@@ -86,11 +83,28 @@ declare const noHookInComponentDisguise: {
86
83
  };
87
84
  messages: {
88
85
  hookInDisguise: string;
86
+ hookInDisguiseNoBoundary: string;
89
87
  passthroughComponent: string;
90
88
  passthroughLayout: string;
91
89
  };
92
- schema: never[];
90
+ schema: {
91
+ additionalProperties: boolean;
92
+ properties: {
93
+ clientBoundary: {
94
+ anyOf: ({
95
+ items: {
96
+ type: 'string';
97
+ };
98
+ type: 'array';
99
+ } | {
100
+ type: 'boolean';
101
+ })[];
102
+ };
103
+ };
104
+ type: 'object';
105
+ }[];
93
106
  type: 'problem';
94
107
  };
108
+ probe(): Probe;
95
109
  };
96
110
  export default noHookInComponentDisguise;
@@ -1,6 +1,15 @@
1
1
  // src/rules/no-hook-in-component-disguise.ts
2
2
  import { hookCalledBy } from "./lib/hooks.js";
3
- import { isTestFile, normalise } from "./lib/paths.js";
3
+ import { isR3fImport } from "./lib/imports.js";
4
+ import { isCapitalised, meaningfulChildren } from "./lib/jsx.js";
5
+ import {
6
+ isTestFile,
7
+ LAYOUT_BASENAMES,
8
+ normalise,
9
+ ROUTE_FALLBACK_BASENAMES,
10
+ fileOf
11
+ } from "./lib/paths.js";
12
+ import { optionsBag, STRINGS } from "./lib/schema.js";
4
13
  var EXPLICIT_SIDE_EFFECT_HOOKS = new Set([
5
14
  "useEffect",
6
15
  "useInsertionEffect",
@@ -18,18 +27,6 @@ function isSideEffectHook(name) {
18
27
  return true;
19
28
  return false;
20
29
  }
21
- var ROUTE_FALLBACK_BASENAMES = new Set([
22
- "default.tsx",
23
- "error.tsx",
24
- "global-error.tsx",
25
- "loading.tsx",
26
- "not-found.tsx"
27
- ]);
28
- var LAYOUT_BASENAMES = new Set(["layout.tsx", "template.tsx"]);
29
- var R3F_PACKAGES = new Set(["@react-three/drei", "@react-three/fiber"]);
30
- function isPascalCase(name) {
31
- return /^[A-Z]/.test(name);
32
- }
33
30
  function isNullLiteral(n) {
34
31
  if (!n)
35
32
  return true;
@@ -43,14 +40,7 @@ function isIdentityFragment(n) {
43
40
  return false;
44
41
  if (n.type !== "JSXFragment")
45
42
  return false;
46
- const children = n.children ?? [];
47
- const meaningful = children.filter((c) => {
48
- if (c.type === "JSXText") {
49
- const text = c.value ?? c.name ?? "";
50
- return text.trim().length > 0;
51
- }
52
- return true;
53
- });
43
+ const meaningful = meaningfulChildren(n.children);
54
44
  if (meaningful.length === 0)
55
45
  return true;
56
46
  if (meaningful.length === 1) {
@@ -90,11 +80,13 @@ function suggestedHookName(componentName) {
90
80
  var noHookInComponentDisguise = {
91
81
  create(context) {
92
82
  const normalized = normalise(context.filename);
83
+ const boundary = context.options?.[0]?.clientBoundary ?? true;
84
+ const boundaryHonoured = typeof boundary === "boolean" ? boundary : boundary.some((one) => new RegExp(one).test(normalized));
93
85
  if (isTestFile(normalized))
94
86
  return {};
95
87
  if (!normalized.endsWith(".tsx"))
96
88
  return {};
97
- const basename = normalized.split("/").pop() ?? "";
89
+ const basename = fileOf(normalized);
98
90
  if (ROUTE_FALLBACK_BASENAMES.has(basename))
99
91
  return {};
100
92
  let fileUsesR3F = false;
@@ -121,7 +113,7 @@ var noHookInComponentDisguise = {
121
113
  return;
122
114
  if (fileUsesR3F)
123
115
  return;
124
- if (!isPascalCase(scope.name))
116
+ if (!isCapitalised(scope.name))
125
117
  return;
126
118
  if (scope.hasNonIdentityReturn)
127
119
  return;
@@ -129,8 +121,9 @@ var noHookInComponentDisguise = {
129
121
  return;
130
122
  if (basename === "page.tsx" && scope.calledHooks.size === 0)
131
123
  return;
132
- if (fileIsClientBoundary && scope.calledAnyHook && !scope.hasPassthroughReturn)
124
+ if (boundaryHonoured && fileIsClientBoundary && scope.calledAnyHook && !scope.hasPassthroughReturn) {
133
125
  return;
126
+ }
134
127
  const hasHooks = scope.calledHooks.size > 0;
135
128
  const passthrough = LAYOUT_BASENAMES.has(basename) ? "passthroughLayout" : "passthroughComponent";
136
129
  context.report({
@@ -139,7 +132,7 @@ var noHookInComponentDisguise = {
139
132
  name: scope.name,
140
133
  suggested: suggestedHookName(scope.name)
141
134
  },
142
- messageId: hasHooks ? "hookInDisguise" : passthrough,
135
+ messageId: hasHooks ? boundaryHonoured ? "hookInDisguise" : "hookInDisguiseNoBoundary" : passthrough,
143
136
  node: scope.node
144
137
  });
145
138
  }
@@ -181,9 +174,8 @@ var noHookInComponentDisguise = {
181
174
  exitFunction();
182
175
  },
183
176
  ImportDeclaration(node) {
184
- if (R3F_PACKAGES.has(node.source.value)) {
177
+ if (isR3fImport(node))
185
178
  fileUsesR3F = true;
186
- }
187
179
  },
188
180
  Program(node) {
189
181
  for (const statement of node.body ?? []) {
@@ -209,18 +201,34 @@ A component returns UI; a hook returns state. The one exception is the client-bo
209
201
  parent that cannot call hooks — a Server Component, or a tissue: \`'use client'\` as the file's first
210
202
  statement, a hook call, and \`return null\`. Render it beside the children, never around them — a
211
203
  component that returns its \`children\` is still a disguise. A component that calls no hook and
212
- returns nothing is not a beacon either; delete it.`,
204
+ returns nothing is not a beacon either; delete it. A repo whose directive is inert (no server components)
205
+ sets \`clientBoundary: false\`, and a repo whose server components live in one tree lists the paths:
206
+ \`clientBoundary: ['/src/app/']\`. Outside them the beacon is a hook in disguise like any other.`,
213
207
  meta: {
214
208
  docs: {
215
- description: "Forbid components whose body calls side-effect hooks (useEffect/useLayoutEffect/useInsertionEffect/useRef) but whose every return is identity (null, bare `children`, `<></>`, `<>{children}</>`). Such a component renders no real UI — it's a hook masquerading as a component. Move to organelles/use-*-effect.ts and call the hook from a real component. Exempt: route fallback files (default/not-found/loading/error/global-error.tsx), files importing from @react-three/fiber or @react-three/drei, and the client-boundary beacon — a 'use client' file whose component calls a hook and returns only null."
209
+ description: "Forbid components whose body calls side-effect hooks (useEffect/useLayoutEffect/useInsertionEffect/useRef) but whose every return is identity (null, bare `children`, `<></>`, `<>{children}</>`). Such a component renders no real UI — it's a hook masquerading as a component. Move to organelles/use-*-effect.ts and call the hook from a real component. Exempt: route fallback files (default/not-found/loading/error/global-error.tsx), files importing from @react-three/fiber or @react-three/drei, and the client-boundary beacon — a 'use client' file whose component calls a hook and returns only null, unless the `clientBoundary` option withdraws it (false) or confines it to listed paths."
216
210
  },
217
211
  messages: {
218
212
  hookInDisguise: "Component '{{name}}' has side-effect hooks ({{hooks}}) but renders only identity/null. It's a hook in disguise — move to organelles/use-{{suggested}}-effect.ts and call the hook from a real component. If its parent cannot call hooks (a Server Component, a tissue), make it the client boundary instead: 'use client' first, and return null — rendered beside the children, not around them.",
213
+ hookInDisguiseNoBoundary: "Component '{{name}}' has side-effect hooks ({{hooks}}) but renders only identity/null. It's a hook in disguise — move to organelles/use-{{suggested}}-effect.ts and call the hook from a real component.",
219
214
  passthroughComponent: "Component '{{name}}' renders only identity/null and calls no hook. It serves no architectural purpose: delete it and render its children where it is used, or give it real content.",
220
215
  passthroughLayout: "Component '{{name}}' renders only identity/null with no effects. It serves no architectural purpose. If this is a metadata-only layout, move `metadata`/`generateMetadata` to the sibling page.tsx and delete this file. If it's structural, give it real content."
221
216
  },
222
- schema: [],
217
+ schema: optionsBag({ clientBoundary: { anyOf: [{ type: "boolean" }, STRINGS] } }),
223
218
  type: "problem"
219
+ },
220
+ probe() {
221
+ return {
222
+ path: "src/cells/geonosis-probe.tsx",
223
+ source: `import { useEffect } from 'react'
224
+
225
+ export function GeonosisProbe() {
226
+ useEffect(() => undefined, [])
227
+
228
+ return null
229
+ }
230
+ `
231
+ };
224
232
  }
225
233
  };
226
234
  var no_hook_in_component_disguise_default = noHookInComponentDisguise;
@@ -2,9 +2,7 @@ import { type RuleContextWithOptions } from './lib/context.js';
2
2
  import { type AnyNode } from './lib/nodes.js';
3
3
  import { type Probe } from './lib/probe.js';
4
4
  interface Options {
5
- /** The fields whose value is an estimate. Default: both spellings the index engine uses. */
6
5
  estimateFields?: string[];
7
- /** The property that admits it. Default: the key one consumer already ships. */
8
6
  labelKey?: string;
9
7
  }
10
8
  declare const noIndexCountAsExact: {
@@ -36,7 +34,6 @@ declare const noIndexCountAsExact: {
36
34
  }[];
37
35
  type: 'problem';
38
36
  };
39
- /** One payload reading an estimate field, with no label beside it. */
40
37
  probe(options: readonly unknown[]): Probe;
41
38
  };
42
39
  export default noIndexCountAsExact;
@@ -1,29 +1,11 @@
1
1
  import { type RuleContext } from './lib/context.js';
2
- interface JSXAttribute {
3
- name?: {
4
- name?: string;
5
- type: string;
6
- };
7
- type: string;
8
- value?: {
9
- type: string;
10
- value?: unknown;
11
- };
12
- }
13
- interface JSXOpeningElement {
14
- attributes: JSXAttribute[];
15
- loc?: unknown;
16
- name: {
17
- name?: string;
18
- type: string;
19
- };
20
- type: 'JSXOpeningElement';
21
- }
2
+ import { type JsxOpeningElement } from './lib/jsx.js';
3
+ import { type Probe } from './lib/probe.js';
22
4
  declare const noInertHiddenJsx: {
23
5
  create(context: RuleContext): {
24
6
  JSXOpeningElement?: undefined;
25
7
  } | {
26
- JSXOpeningElement(node: JSXOpeningElement): void;
8
+ JSXOpeningElement(node: JsxOpeningElement): void;
27
9
  };
28
10
  fixShape: string;
29
11
  meta: {
@@ -36,5 +18,6 @@ declare const noInertHiddenJsx: {
36
18
  schema: never[];
37
19
  type: 'problem';
38
20
  };
21
+ probe(): Probe;
39
22
  };
40
23
  export default noInertHiddenJsx;
@@ -1,5 +1,7 @@
1
1
  // src/rules/no-inert-hidden-jsx.ts
2
+ import { tagNameOf } from "./lib/jsx.js";
2
3
  import { isTestFile, normalise } from "./lib/paths.js";
4
+ import { staticClassNameOf } from "./lib/styling.js";
3
5
  var PURELY_PRESENTATIONAL_ATTRS = new Set([
4
6
  "className",
5
7
  "data-testid",
@@ -21,21 +23,11 @@ var noInertHiddenJsx = {
21
23
  return {};
22
24
  return {
23
25
  JSXOpeningElement(node) {
24
- if (node.name?.type !== "JSXIdentifier")
25
- return;
26
- const componentName = node.name.name;
26
+ const componentName = tagNameOf(node);
27
27
  if (!componentName)
28
28
  return;
29
29
  const attributes = node.attributes ?? [];
30
- const classNameAttr = attributes.find((a) => getAttrName(a) === "className");
31
- if (!classNameAttr || !classNameAttr.value)
32
- return;
33
- if (classNameAttr.value.type !== "Literal")
34
- return;
35
- const raw = classNameAttr.value.value;
36
- if (typeof raw !== "string")
37
- return;
38
- if (raw.trim() !== "hidden")
30
+ if (staticClassNameOf(node)?.trim() !== "hidden")
39
31
  return;
40
32
  const hasFunctionalAttr = attributes.some((attr) => {
41
33
  if (attr.type === "JSXSpreadAttribute")
@@ -67,6 +59,15 @@ satisfied.`,
67
59
  },
68
60
  schema: [],
69
61
  type: "problem"
62
+ },
63
+ probe() {
64
+ return {
65
+ path: "src/cells/geonosis-probe.tsx",
66
+ source: `export function GeonosisProbe() {
67
+ return <span className="hidden" aria-hidden="true" />
68
+ }
69
+ `
70
+ };
70
71
  }
71
72
  };
72
73
  var no_inert_hidden_jsx_default = noInertHiddenJsx;
@@ -1,5 +1,6 @@
1
1
  import { type RuleContext } from './lib/context.js';
2
2
  import { type AnyNode } from './lib/nodes.js';
3
+ import { type Probe } from './lib/probe.js';
3
4
  declare const noInlineDataInJsx: {
4
5
  create(context: RuleContext): {
5
6
  JSXAttribute?: undefined;
@@ -20,5 +21,6 @@ declare const noInlineDataInJsx: {
20
21
  schema: never[];
21
22
  type: 'problem';
22
23
  };
24
+ probe(): Probe;
23
25
  };
24
26
  export default noInlineDataInJsx;
@@ -1,7 +1,7 @@
1
1
  // src/rules/no-inline-data-in-jsx.ts
2
2
  import { withoutTypes } from "./lib/objects.js";
3
3
  import { normalise } from "./lib/paths.js";
4
- var COMPONENT_FILE_PATTERN = /(?:^|\/)(?:features\/[^/]+\/)?(compounds|organelles|cells|tissues|organs)\/.+\.tsx$/;
4
+ import { COMPONENT_FILE_PATTERN, featureOf } from "./lib/tiers.js";
5
5
  var MIN_OBJECTS = 3;
6
6
  var objectsIn = (node) => {
7
7
  const at = withoutTypes(node);
@@ -14,7 +14,7 @@ var noInlineDataInJsx = {
14
14
  const normalized = normalise(context.filename);
15
15
  if (!COMPONENT_FILE_PATTERN.test(normalized))
16
16
  return {};
17
- const feature = normalized.match(/features\/([^/]+)\//)?.[1];
17
+ const feature = featureOf(normalized);
18
18
  const home = feature === undefined ? "a lib/ module beside this component" : `features/${feature}/lib/`;
19
19
  return {
20
20
  JSXAttribute(node) {
@@ -63,6 +63,21 @@ lives in one place.`,
63
63
  },
64
64
  schema: [],
65
65
  type: "problem"
66
+ },
67
+ probe() {
68
+ return {
69
+ path: "src/features/geonosis/cells/geonosis-probe.tsx",
70
+ source: `export function GeonosisProbe() {
71
+ return (
72
+ <ul>
73
+ {[{ id: 'a' }, { id: 'b' }, { id: 'c' }].map((one) => (
74
+ <li key={one.id}>{one.id}</li>
75
+ ))}
76
+ </ul>
77
+ )
78
+ }
79
+ `
80
+ };
66
81
  }
67
82
  };
68
83
  var no_inline_data_in_jsx_default = noInlineDataInJsx;
@@ -1,4 +1,5 @@
1
1
  import { type RuleContextWithOptions } from './lib/context.js';
2
+ import { type Probe } from './lib/probe.js';
2
3
  type Options = {
3
4
  roots?: string[];
4
5
  };
@@ -32,5 +33,6 @@ declare const noInvalidFeatureFolders: {
32
33
  }[];
33
34
  type: 'problem';
34
35
  };
36
+ probe(): Probe;
35
37
  };
36
38
  export default noInvalidFeatureFolders;
@@ -1,5 +1,6 @@
1
1
  // src/rules/no-invalid-feature-folders.ts
2
- import { normalise } from "./lib/paths.js";
2
+ import { HOOK_NAME } from "./lib/hooks.js";
3
+ import { fileOf, normalise, trimSlashes } from "./lib/paths.js";
3
4
  import { optionsBag, STRINGS } from "./lib/schema.js";
4
5
  import { TIERS } from "./lib/tiers.js";
5
6
  var FEATURE_NEIGHBOURS = ["lib", "stores"];
@@ -9,7 +10,7 @@ var FEATURE_PATH_PATTERN = /features\/([^/]+)\/([^/]+)\//;
9
10
  var folderUnderRoot = (normalized, roots) => {
10
11
  const path = `/${normalized}`;
11
12
  for (const declared of roots) {
12
- const root = declared.replaceAll(/^\/+|\/+$/g, "");
13
+ const root = trimSlashes(declared);
13
14
  const at = path.indexOf(`/${root}/`);
14
15
  if (root === "" || at === -1)
15
16
  continue;
@@ -45,8 +46,8 @@ var noInvalidFeatureFolders = {
45
46
  };
46
47
  }
47
48
  if (folder === "lib") {
48
- const fileName = normalized.split("/").pop() || "";
49
- if (fileName.startsWith("use-") || /^use[A-Z]/.test(fileName)) {
49
+ const fileName = fileOf(normalized);
50
+ if (fileName.startsWith("use-") || HOOK_NAME.test(fileName)) {
50
51
  return {
51
52
  Program(node) {
52
53
  context.report({
@@ -80,6 +81,13 @@ tier would not accept it.`,
80
81
  roots: STRINGS
81
82
  }),
82
83
  type: "problem"
84
+ },
85
+ probe() {
86
+ return {
87
+ path: "src/features/geonosis/components/geonosis-probe.tsx",
88
+ source: `export const GeonosisProbe = 1
89
+ `
90
+ };
83
91
  }
84
92
  };
85
93
  var no_invalid_feature_folders_default = noInvalidFeatureFolders;
@@ -2,9 +2,7 @@ import { type RuleContextWithOptions } from './lib/context.js';
2
2
  import { type AnyNode } from './lib/nodes.js';
3
3
  import { type Probe } from './lib/probe.js';
4
4
  interface Options {
5
- /** Regex sources for the loader directories still skipped. Default: subscribers and jobs. */
6
5
  directories?: string[];
7
- /** Calls that register a workflow resource by running. Default: the composer's three. */
8
6
  resourceFactories?: string[];
9
7
  }
10
8
  declare const noLoaderSideEffectInIndexFile: {
@@ -45,11 +43,6 @@ declare const noLoaderSideEffectInIndexFile: {
45
43
  }[];
46
44
  type: 'problem';
47
45
  };
48
- /**
49
- * A default export in a file named `index`, inside a directory the loader still skips. The
50
- * directory is a regex option, so the path is built back out of it and refused if it does not
51
- * match — a probe placed outside the tree fires nothing however right its source is.
52
- */
53
46
  probe(options: readonly unknown[]): Probe;
54
47
  };
55
48
  export default noLoaderSideEffectInIndexFile;
@@ -1,4 +1,5 @@
1
1
  import { type RuleContext } from './lib/context.js';
2
+ import { type Probe } from './lib/probe.js';
2
3
  interface ASTNode {
3
4
  argument?: ASTNode | null;
4
5
  body?: ASTNode | ASTNode[] | null;
@@ -7,10 +8,7 @@ interface ASTNode {
7
8
  id?: {
8
9
  name: string;
9
10
  } | null;
10
- init?: {
11
- body?: ASTNode | null;
12
- type: string;
13
- } | null;
11
+ init?: ASTNode | null;
14
12
  }[];
15
13
  id?: {
16
14
  name: string;
@@ -26,11 +24,13 @@ interface ASTNode {
26
24
  }
27
25
  declare const noLogicInComponentFiles: {
28
26
  create(context: RuleContext): {
27
+ 'Program:exit'?: undefined;
29
28
  ExportNamedDeclaration?: undefined;
29
+ ExportDefaultDeclaration?: undefined;
30
30
  Program?: undefined;
31
- 'Program:exit'?: undefined;
32
31
  } | {
33
32
  ExportNamedDeclaration(node: ASTNode): void;
33
+ ExportDefaultDeclaration(node: ASTNode): void;
34
34
  Program(node: unknown): void;
35
35
  'Program:exit'(): void;
36
36
  };
@@ -45,5 +45,6 @@ declare const noLogicInComponentFiles: {
45
45
  schema: never[];
46
46
  type: 'problem';
47
47
  };
48
+ probe(): Probe;
48
49
  };
49
50
  export default noLogicInComponentFiles;
@@ -1,9 +1,24 @@
1
1
  // src/rules/no-logic-in-component-files.ts
2
+ import { isCapitalised } from "./lib/jsx.js";
2
3
  import { normalise } from "./lib/paths.js";
3
- var COMPONENT_FILE_PATTERN = /(?:^|\/)(?:features\/[^/]+\/)?(compounds|organelles|cells|tissues|organs)\/.+\.tsx$/;
4
- var PRIVATE_OR_NON_ENTRYPOINT = /(\.(parts|sections|summary|layouts|icons|types|hooks|utils|context|store|sidebar|data|config|stories|spec|test)\.tsx$|\/index\.tsx$)/;
5
- function isReactComponent(name) {
6
- return /^[A-Z][a-zA-Z0-9]*$/.test(name) && !/^[A-Z0-9_]+$/.test(name);
4
+ import { COMPONENT_FILE_PATTERN, PRIVATE_SIBLING } from "./lib/tiers.js";
5
+ var PASCAL_CASE = /^[A-Z][a-zA-Z0-9]*$/;
6
+ var SHOUTING = /^[A-Z0-9]+$/;
7
+ var rendersJsx = (node) => node?.type === "JSXElement" || node?.type === "JSXFragment";
8
+ function returnsJsx(fn) {
9
+ const body = fn.body;
10
+ if (!body || Array.isArray(body))
11
+ return false;
12
+ if (body.type !== "BlockStatement")
13
+ return rendersJsx(body);
14
+ return Array.isArray(body.body) && body.body.some((one) => one.type === "ReturnStatement" && rendersJsx(one.argument));
15
+ }
16
+ function isReactComponent(name, fn) {
17
+ if (!PASCAL_CASE.test(name))
18
+ return false;
19
+ if (!SHOUTING.test(name))
20
+ return true;
21
+ return fn.type !== "CallExpression" && returnsJsx(fn);
7
22
  }
8
23
  function jsxRootIsComponent(node) {
9
24
  if (!node || node.type !== "JSXElement")
@@ -13,31 +28,19 @@ function jsxRootIsComponent(node) {
13
28
  return false;
14
29
  if (name.type === "JSXMemberExpression")
15
30
  return true;
16
- return name.type === "JSXIdentifier" && /^[A-Z]/.test(name.name || "");
31
+ return name.type === "JSXIdentifier" && isCapitalised(name.name || "");
17
32
  }
18
- function isThinWrapper(decl) {
19
- if (!decl)
33
+ function isThinWrapper(fn) {
34
+ const body = fn?.body;
35
+ if (!body || Array.isArray(body))
20
36
  return false;
21
- try {
22
- let body = decl.body;
23
- if (!body && decl.declarations?.[0]?.init) {
24
- body = decl.declarations[0].init.body;
25
- }
26
- if (!body || Array.isArray(body))
37
+ if (body.type === "JSXElement")
38
+ return jsxRootIsComponent(body);
39
+ if (body.type === "BlockStatement" && Array.isArray(body.body)) {
40
+ const only = body.body[0];
41
+ if (body.body.length !== 1 || only?.type !== "ReturnStatement")
27
42
  return false;
28
- if (body.type === "JSXElement")
29
- return jsxRootIsComponent(body);
30
- if (body.type === "BlockStatement" && Array.isArray(body.body)) {
31
- const stmts = body.body;
32
- if (stmts.length !== 1)
33
- return false;
34
- const only = stmts[0];
35
- if (only?.type !== "ReturnStatement")
36
- return false;
37
- return jsxRootIsComponent(only.argument);
38
- }
39
- } catch {
40
- return false;
43
+ return jsxRootIsComponent(only.argument);
41
44
  }
42
45
  return false;
43
46
  }
@@ -46,7 +49,7 @@ var noLogicInComponentFiles = {
46
49
  const normalized = normalise(context.filename);
47
50
  if (!COMPONENT_FILE_PATTERN.test(normalized))
48
51
  return {};
49
- if (PRIVATE_OR_NON_ENTRYPOINT.test(normalized))
52
+ if (PRIVATE_SIBLING.test(normalized) || normalized.endsWith("/index.tsx"))
50
53
  return {};
51
54
  const exportedComponents = [];
52
55
  let programNode = null;
@@ -55,19 +58,25 @@ var noLogicInComponentFiles = {
55
58
  const decl = node.declaration;
56
59
  if (!decl)
57
60
  return;
58
- if (decl.type === "FunctionDeclaration" && decl.id?.name && isReactComponent(decl.id.name) && !isThinWrapper(decl)) {
61
+ if (decl.type === "FunctionDeclaration" && decl.id?.name && isReactComponent(decl.id.name, decl) && !isThinWrapper(decl)) {
59
62
  exportedComponents.push(decl.id.name);
60
63
  }
61
64
  if (decl.type === "VariableDeclaration" && decl.declarations) {
62
65
  for (const d of decl.declarations) {
63
- const initType = d.init?.type;
64
- const isComponentInit = initType === "ArrowFunctionExpression" || initType === "FunctionExpression" || initType === "CallExpression";
65
- if (d.id?.name && isReactComponent(d.id.name) && isComponentInit && !isThinWrapper(decl)) {
66
+ const init = d.init;
67
+ const isComponentInit = init?.type === "ArrowFunctionExpression" || init?.type === "FunctionExpression" || init?.type === "CallExpression";
68
+ if (d.id?.name && init && isComponentInit && isReactComponent(d.id.name, init) && !isThinWrapper(init)) {
66
69
  exportedComponents.push(d.id.name);
67
70
  }
68
71
  }
69
72
  }
70
73
  },
74
+ ExportDefaultDeclaration(node) {
75
+ const decl = node.declaration;
76
+ if (decl?.type === "FunctionDeclaration" && decl.id?.name && isReactComponent(decl.id.name, decl) && !isThinWrapper(decl)) {
77
+ exportedComponents.push(decl.id.name);
78
+ }
79
+ },
71
80
  Program(node) {
72
81
  programNode = node;
73
82
  },
@@ -97,6 +106,18 @@ the same component. Thin preset wrappers over one imported component do not coun
97
106
  },
98
107
  schema: [],
99
108
  type: "problem"
109
+ },
110
+ probe() {
111
+ return {
112
+ path: "src/cells/geonosis-probe.tsx",
113
+ source: `export function GeonosisProbe() {
114
+ return <div data-testid="1"><span>a</span><span>b</span></div>
115
+ }
116
+ export function GeonosisSecond() {
117
+ return <section data-testid="2"><p>x</p><p>y</p></section>
118
+ }
119
+ `
120
+ };
100
121
  }
101
122
  };
102
123
  var no_logic_in_component_files_default = noLogicInComponentFiles;
@@ -1,19 +1,8 @@
1
1
  import { type RuleContextWithOptions } from './lib/context.js';
2
2
  import { type Probe } from './lib/probe.js';
3
3
  interface Options {
4
- /**
5
- * Module specifiers that ARE the database. Matched exactly, as a subpath (`@repo/db/client`) or
6
- * as the last segment of a relative path (`../db`) — never by containment, because `@repo/dbt` is
7
- * a different package.
8
- */
9
4
  dbModules?: string[];
10
- /** Path fragments whose files are integration suites. A whole path segment, never a prefix. */
11
5
  integrationDirs?: string[];
12
- /**
13
- * Identifiers a repo has named as mocks of its database, for the shape no mock call can be seen
14
- * in: a handle built in a setup file and threaded through a context helper. Empty by default —
15
- * a rule guessing at identifier names in a repo that has no such handle is a rule they turn off.
16
- */
17
6
  mockNames?: string[];
18
7
  }
19
8
  type Node = {
@@ -79,10 +68,6 @@ declare const noMockDbInIntegration: {
79
68
  }[];
80
69
  type: 'problem';
81
70
  };
82
- /**
83
- * A suite under the configured integration tree, mocking the module that IS the database —
84
- * written in bun's spelling, which is what `BUN_MOCK` above is for and not a preference.
85
- */
86
71
  probe(options: readonly unknown[]): Probe;
87
72
  };
88
73
  export default noMockDbInIntegration;