@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,15 +1,15 @@
1
1
  // src/rules/next-route-segment-is-thin-delegate.ts
2
- import { walkOwnFunctionBody } from "./lib/nodes.js";
3
- import { isTestFile, normalise } from "./lib/paths.js";
4
- var DELEGATE_BASENAMES = new Set(["layout.tsx", "page.tsx", "template.tsx"]);
5
- var FUNCTIONS = new Set(["ArrowFunctionExpression", "FunctionDeclaration", "FunctionExpression"]);
6
- var FALLBACK_BASENAMES = new Set([
7
- "default.tsx",
8
- "error.tsx",
9
- "global-error.tsx",
10
- "loading.tsx",
11
- "not-found.tsx"
12
- ]);
2
+ import { sourceOf } from "./lib/imports.js";
3
+ import { isCapitalised, meaningfulChildren } from "./lib/jsx.js";
4
+ import { FUNCTION_BOUNDARY_TYPES, walkOwnFunctionBody } from "./lib/nodes.js";
5
+ import {
6
+ fileOf,
7
+ isTestFile,
8
+ normalise,
9
+ ROUTE_BODY_BASENAMES,
10
+ ROUTE_FALLBACK_BASENAMES
11
+ } from "./lib/paths.js";
12
+ import { APP_FOLDER, folderOf, TIERS } from "./lib/tiers.js";
13
13
  function collectReturnStatements(body) {
14
14
  const out = [];
15
15
  walkOwnFunctionBody(body, (node) => {
@@ -41,47 +41,42 @@ function describeReturn(arg) {
41
41
  return a.type ?? "unknown";
42
42
  }
43
43
  function bioFolderOf(source) {
44
- const m = source.match(/\/(atoms|molecules|compounds|organelles|cells|tissues|lib|stores)(?:\/|$)/);
45
- return m?.[1] ?? "unknown";
44
+ return folderOf(source, [...TIERS, "lib", "stores"]) ?? "unknown";
46
45
  }
47
46
  function isTissueSource(source) {
48
47
  if (!source)
49
48
  return false;
50
- return /\/tissues(?:\/|$)/.test(source);
49
+ return folderOf(source, ["tissues"]) !== undefined;
51
50
  }
52
51
  var nextRouteSegmentIsThinDelegate = {
53
52
  create(context) {
54
53
  const normalized = normalise(context.filename);
55
54
  if (isTestFile(normalized))
56
55
  return {};
57
- if (!/(?:^|\/)app\//.test(normalized))
56
+ if (!APP_FOLDER.test(normalized))
58
57
  return {};
59
- const basename = normalized.split("/").pop() ?? "";
60
- if (!DELEGATE_BASENAMES.has(basename))
58
+ const basename = fileOf(normalized);
59
+ if (!ROUTE_BODY_BASENAMES.has(basename))
61
60
  return {};
62
- if (FALLBACK_BASENAMES.has(basename))
61
+ if (ROUTE_FALLBACK_BASENAMES.has(basename))
63
62
  return {};
64
63
  const importSources = new Map;
65
- function isImportedPascalCase(tag) {
66
- if (!tag)
67
- return false;
68
- if (!/^[A-Z]/.test(tag))
69
- return false;
70
- return importSources.has(tag);
71
- }
72
- function reportNonTissueDelegate(tag, ret) {
73
- const source = importSources.get(tag) ?? "";
74
- context.report({
75
- data: {
76
- bio: bioFolderOf(source),
77
- file: basename,
78
- source,
79
- tag
80
- },
81
- messageId: "delegateMustBeTissue",
82
- node: ret
83
- });
84
- }
64
+ const delegate = (tag, found, ret) => {
65
+ const source = tag === null ? undefined : importSources.get(tag);
66
+ if (tag === null || source === undefined || !isCapitalised(tag)) {
67
+ context.report({
68
+ data: { file: basename, found },
69
+ messageId: "mustBeThinDelegate",
70
+ node: ret
71
+ });
72
+ } else if (!isTissueSource(source)) {
73
+ context.report({
74
+ data: { bio: bioFolderOf(source), file: basename, source, tag },
75
+ messageId: "delegateMustBeTissue",
76
+ node: ret
77
+ });
78
+ }
79
+ };
85
80
  function checkReturn(arg, ret) {
86
81
  if (!arg || typeof arg !== "object")
87
82
  return;
@@ -89,28 +84,11 @@ var nextRouteSegmentIsThinDelegate = {
89
84
  if (a.type === "Literal" && a.value === null)
90
85
  return;
91
86
  if (a.type === "JSXElement") {
92
- const tag = tagOf(arg);
93
- if (!isImportedPascalCase(tag)) {
94
- context.report({
95
- data: { file: basename, found: describeReturn(arg) },
96
- messageId: "mustBeThinDelegate",
97
- node: ret
98
- });
99
- return;
100
- }
101
- const source = importSources.get(tag);
102
- if (!isTissueSource(source)) {
103
- reportNonTissueDelegate(tag, ret);
104
- }
87
+ delegate(tagOf(arg), describeReturn(arg), ret);
105
88
  return;
106
89
  }
107
90
  if (a.type === "JSXFragment") {
108
- const kids = arg.children ?? [];
109
- const meaningful = kids.filter((c) => {
110
- if (c.type === "JSXText")
111
- return (c.value ?? "").trim().length > 0;
112
- return true;
113
- });
91
+ const meaningful = meaningfulChildren(arg.children);
114
92
  if (meaningful.length !== 1) {
115
93
  context.report({
116
94
  data: { count: String(meaningful.length), file: basename },
@@ -129,18 +107,7 @@ var nextRouteSegmentIsThinDelegate = {
129
107
  return;
130
108
  }
131
109
  const tagName = child.openingElement?.name?.type === "JSXIdentifier" ? child.openingElement.name.name ?? null : null;
132
- if (!isImportedPascalCase(tagName)) {
133
- context.report({
134
- data: { file: basename, found: `fragment><${tagName ?? "?"}/></fragment` },
135
- messageId: "mustBeThinDelegate",
136
- node: ret
137
- });
138
- return;
139
- }
140
- const source = importSources.get(tagName);
141
- if (!isTissueSource(source)) {
142
- reportNonTissueDelegate(tagName, ret);
143
- }
110
+ delegate(tagName, `fragment><${tagName ?? "?"}/></fragment`, ret);
144
111
  return;
145
112
  }
146
113
  context.report({
@@ -152,7 +119,7 @@ var nextRouteSegmentIsThinDelegate = {
152
119
  return {
153
120
  ExportDefaultDeclaration(node) {
154
121
  const decl = node.declaration;
155
- if (!decl || !FUNCTIONS.has(decl.type))
122
+ if (!decl || !FUNCTION_BOUNDARY_TYPES.has(decl.type))
156
123
  return;
157
124
  if (decl.body !== undefined && decl.body.type !== "BlockStatement") {
158
125
  checkReturn(decl.body, decl.body);
@@ -162,7 +129,7 @@ var nextRouteSegmentIsThinDelegate = {
162
129
  checkReturn(ret.argument, ret);
163
130
  },
164
131
  ImportDeclaration(node) {
165
- const source = node.source?.value ?? "";
132
+ const source = sourceOf(node) ?? "";
166
133
  for (const spec of node.specifiers ?? []) {
167
134
  const local = spec.local?.name;
168
135
  if (local)
@@ -186,6 +153,15 @@ returns is a tissue — a route that renders a cell directly wraps it in a tissu
186
153
  },
187
154
  schema: [],
188
155
  type: "problem"
156
+ },
157
+ probe() {
158
+ return {
159
+ path: "src/app/geonosis-probe/page.tsx",
160
+ source: `export default function Page() {
161
+ return <div>inline markup</div>
162
+ }
163
+ `
164
+ };
189
165
  }
190
166
  };
191
167
  var next_route_segment_is_thin_delegate_default = nextRouteSegmentIsThinDelegate;
@@ -1,8 +1,6 @@
1
1
  import { type Probe } from './lib/probe.js';
2
2
  interface Options {
3
- /** Substrings stripped before the scan — a real demo-asset host, so a CDN URL never trips. */
4
3
  allowedSubstrings?: string[];
5
- /** Brand names, matched case-insensitively on a word boundary. */
6
4
  brands?: string[];
7
5
  }
8
6
  declare const noBrandNames: {
@@ -52,13 +50,6 @@ declare const noBrandNames: {
52
50
  }[];
53
51
  type: 'problem';
54
52
  };
55
- /**
56
- * The brand list is a Medusa storefront's and the corpus says `acme`, so this rule reads as
57
- * firing nowhere in the repo whose brands it was configured with — one of the two findings that
58
- * opened this. The brand goes on a line of its own so no `allowedSubstrings` entry can strip it
59
- * away: a CDN host is stripped before the scan, and a probe hiding inside one would prove the
60
- * opposite of what it claims.
61
- */
62
53
  probe(options: readonly unknown[]): Probe;
63
54
  };
64
55
  export default noBrandNames;
@@ -2,10 +2,10 @@
2
2
  import * as fs from "node:fs";
3
3
  import { requireOption } from "./lib/options.js";
4
4
  import { normalise } from "./lib/paths.js";
5
+ import { escapeForRegex } from "./lib/pattern-match.js";
5
6
  import { bagOf } from "./lib/probe.js";
6
7
  import { optionsBag, STRINGS } from "./lib/schema.js";
7
8
  var EXEMPT_PATH = /\/examples\//;
8
- var escapeForRegex = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
9
9
  var noBrandNames = {
10
10
  create(context) {
11
11
  const { allowedSubstrings = [], brands = [] } = context.options?.[0] ?? {};
@@ -1,44 +1,5 @@
1
- interface BinaryExpression {
2
- left: ClassNameExpression | null;
3
- operator: string;
4
- right: ClassNameExpression | null;
5
- type: 'BinaryExpression';
6
- }
7
- type ClassNameExpression = {
8
- type: string;
9
- } | BinaryExpression | JSXExpressionContainer | Literal | TemplateLiteral;
10
- interface JSXAttribute {
11
- name?: {
12
- name?: string;
13
- type: string;
14
- };
15
- type: string;
16
- value?: ClassNameExpression | null;
17
- }
18
- interface JSXExpressionContainer {
19
- expression: ClassNameExpression | null;
20
- type: 'JSXExpressionContainer';
21
- }
22
- interface JSXOpeningElement {
23
- attributes: JSXAttribute[];
24
- name: {
25
- name?: string;
26
- type: string;
27
- };
28
- type: 'JSXOpeningElement';
29
- }
30
- interface Literal {
31
- type: 'Literal';
32
- value: unknown;
33
- }
34
- interface TemplateLiteral {
35
- quasis: {
36
- value?: {
37
- raw?: string;
38
- };
39
- }[];
40
- type: 'TemplateLiteral';
41
- }
1
+ import { type JsxOpeningElement } from './lib/jsx.js';
2
+ import { type Probe } from './lib/probe.js';
42
3
  declare const noCardShapedDiv: {
43
4
  create(context: {
44
5
  filename: string;
@@ -50,7 +11,7 @@ declare const noCardShapedDiv: {
50
11
  }): {
51
12
  JSXOpeningElement?: undefined;
52
13
  } | {
53
- JSXOpeningElement(node: JSXOpeningElement): void;
14
+ JSXOpeningElement(node: JsxOpeningElement): void;
54
15
  };
55
16
  fixShape: string;
56
17
  meta: {
@@ -63,5 +24,6 @@ declare const noCardShapedDiv: {
63
24
  schema: never[];
64
25
  type: 'problem';
65
26
  };
27
+ probe(): Probe;
66
28
  };
67
29
  export default noCardShapedDiv;
@@ -1,6 +1,7 @@
1
1
  // src/rules/no-card-shaped-div.ts
2
+ import { attributeNamed, tagNameOf } from "./lib/jsx.js";
2
3
  import { normalise } from "./lib/paths.js";
3
- var biologicalHierarchyPattern = /\/(molecules|compounds|organelles|cells|layouts)\//;
4
+ import { ABOVE_ATOMS_FOLDER } from "./lib/tiers.js";
4
5
  var MODIFIER_PREFIX = "(?:(?:dark|print|sm|md|lg|xl|2xl|first|last|odd|even):)?";
5
6
  var BORDER_PATTERN = new RegExp(`(?:^|\\s)${MODIFIER_PREFIX}border(?:\\s|$|-[1-9]\\d*(?:\\s|$))`);
6
7
  var BG_PATTERN = new RegExp(`(?:^|\\s)${MODIFIER_PREFIX}bg-(?:white|black|container|page|neutral-\\d+|gray-\\d+|zinc-\\d+|stone-\\d+|slate-\\d+)`);
@@ -35,13 +36,13 @@ function isCardLikeClassName(className) {
35
36
  var noCardShapedDiv = {
36
37
  create(context) {
37
38
  const filename = normalise(context.filename);
38
- if (!biologicalHierarchyPattern.test(filename))
39
+ if (!ABOVE_ATOMS_FOLDER.test(filename))
39
40
  return {};
40
41
  return {
41
42
  JSXOpeningElement(node) {
42
- if (node?.name?.type !== "JSXIdentifier" || node.name.name !== "div")
43
+ if (tagNameOf(node) !== "div")
43
44
  return;
44
- const classAttr = node.attributes?.find((a) => a?.type === "JSXAttribute" && a?.name?.type === "JSXIdentifier" && a?.name?.name === "className");
45
+ const classAttr = attributeNamed(node, "className");
45
46
  if (!classAttr)
46
47
  return;
47
48
  const cls = extractClassNameString(classAttr.value ?? null);
@@ -65,6 +66,13 @@ styling into an atom that owns it.`,
65
66
  },
66
67
  schema: [],
67
68
  type: "problem"
69
+ },
70
+ probe() {
71
+ return {
72
+ path: "src/cells/geonosis-probe.tsx",
73
+ source: `export const GeonosisProbe = () => <div className="border bg-white" />
74
+ `
75
+ };
68
76
  }
69
77
  };
70
78
  var no_card_shaped_div_default = noCardShapedDiv;
@@ -9,6 +9,7 @@ declare const noCrossFeatureStores: {
9
9
  }) => void;
10
10
  }): import('./lib/visitors.js').Visitor;
11
11
  fixShape: string;
12
+ probe?: ((options: readonly unknown[]) => import('./lib/probe.js').Probe | import('./lib/probe.js').ProbeTree) | undefined;
12
13
  meta: {
13
14
  docs: {
14
15
  description: string;
@@ -1,8 +1,8 @@
1
1
  // src/rules/no-cross-feature-stores.ts
2
2
  import { directionRule } from "./lib/direction-rule.js";
3
+ import { crossesFeatureBoundary } from "./lib/shared-features.js";
3
4
  var BIO_FILE_PATTERN = /(?:^|\/)features\/([^/]+)\/(cells|organelles)\/.*\.(tsx?|ts)$/;
4
5
  var STORE_IMPORT_PATTERN = /(?:^|\/|@\/)features\/([^/]+)\/stores\//;
5
- var SHARED_FEATURE = "shared";
6
6
  var storeFeatureOf = (source) => STORE_IMPORT_PATTERN.exec(source)?.[1];
7
7
  var noCrossFeatureStores = directionRule({
8
8
  data: (target, importer) => ({
@@ -15,14 +15,18 @@ var noCrossFeatureStores = directionRule({
15
15
  fixShape: `A cell or organelle in \`features/X/\` reads only \`features/X/stores/*\` and \`features/shared/stores/*\`.
16
16
  Another feature's store is a hidden coupling: let the data flow through the parent tissue as props
17
17
  instead. Type-only imports are exempt; \`export … from\` is not.`,
18
- forbidden: (target, importer) => {
19
- const importedFeature = storeFeatureOf(target.path);
20
- return importedFeature !== undefined && importedFeature !== importer.feature && importedFeature !== SHARED_FEATURE;
21
- },
18
+ forbidden: (target, importer) => crossesFeatureBoundary(storeFeatureOf(target.path), importer.feature, {}),
22
19
  messages: {
23
20
  crossFeatureStore: `"{{file}}" in feature "{{ownFeature}}" imports from another feature's store: "{{source}}" (feature: "{{importedFeature}}"). Cross-feature data must flow through the parent as props. Only features/{{ownFeature}}/stores/* and features/shared/stores/* are allowed here.`
24
21
  },
25
22
  messageId: "crossFeatureStore",
23
+ probe: () => ({
24
+ path: "src/features/geonosis/cells/geonosis-probe.tsx",
25
+ source: `import { other } from '@/features/geonosis-neighbour/stores/other'
26
+
27
+ export const GeonosisProbe = other
28
+ `
29
+ }),
26
30
  scope: (importer) => BIO_FILE_PATTERN.test(importer.path)
27
31
  });
28
32
  var no_cross_feature_stores_default = noCrossFeatureStores;
@@ -1,11 +1,7 @@
1
1
  import { type RuleContextWithOptions } from './lib/context.js';
2
+ import type { Probe } from './lib/probe.js';
2
3
  interface Options {
3
- /**
4
- * Regex sources over the normalised filename: files inside `transacts` whose handle is still D1
5
- * — the control plane's own modules, its schema.
6
- */
7
4
  except?: string | string[];
8
- /** Regex sources over the normalised filename: the files whose database transacts (a Postgres cell). */
9
5
  transacts?: string | string[];
10
6
  }
11
7
  type Node = {
@@ -65,5 +61,6 @@ declare const noD1Transaction: {
65
61
  }[];
66
62
  type: 'problem';
67
63
  };
64
+ probe(): Probe;
68
65
  };
69
66
  export default noD1Transaction;
@@ -82,6 +82,13 @@ judged until the repo says which of its handles may transact.`,
82
82
  transacts: PATTERNS
83
83
  }),
84
84
  type: "problem"
85
+ },
86
+ probe() {
87
+ return {
88
+ path: "geonosis-probe-d1.ts",
89
+ source: `export const geonosisProbe = (db) => db.transaction(async () => 1)
90
+ `
91
+ };
85
92
  }
86
93
  };
87
94
  var no_d1_transaction_default = noD1Transaction;
@@ -1,5 +1,6 @@
1
1
  import { type RuleContextWithRawOptions } 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 noDateStringCompare: {
4
5
  create(context: RuleContextWithRawOptions): {
5
6
  BinaryExpression(node: AnyNode): void;
@@ -24,5 +25,6 @@ declare const noDateStringCompare: {
24
25
  }[];
25
26
  type: 'problem';
26
27
  };
28
+ probe(options: readonly unknown[]): Probe;
27
29
  };
28
30
  export default noDateStringCompare;
@@ -1,7 +1,7 @@
1
1
  // src/rules/no-date-string-compare.ts
2
2
  import { calleeNameOf, propertyNameOf } from "./lib/nodes.js";
3
3
  import { bagOf } from "./lib/probe.js";
4
- import { STRING } from "./lib/schema.js";
4
+ import { optionsBag, STRING } from "./lib/schema.js";
5
5
  var FIELD_PATTERN = "(?:_at|[a-z]At)$";
6
6
  var RELATIONAL = new Set(["<", "<=", ">", ">="]);
7
7
  var COMPARE = "localeCompare";
@@ -84,14 +84,20 @@ and a template with copy in it are both left alone. Which names hold a timestamp
84
84
  messages: {
85
85
  textOrder: 'Ordering "{{field}}" by {{ordering}} on a string compares the TEXT of a timestamp: a Date stringifies to "Fri Aug 30 2026 …", so this sorts by weekday name. Compare `.getTime()` or `Date.parse()`, or order on the ISO string the API returned.'
86
86
  },
87
- schema: [
88
- {
89
- additionalProperties: false,
90
- properties: { fieldPattern: STRING },
91
- type: "object"
92
- }
93
- ],
87
+ schema: optionsBag({ fieldPattern: STRING }),
94
88
  type: "problem"
89
+ },
90
+ probe(options) {
91
+ const field = "created_at";
92
+ const pattern = bagOf(options).fieldPattern ?? FIELD_PATTERN;
93
+ if (!new RegExp(pattern).test(field)) {
94
+ throw new Error(`no probe can be built: \`${field}\` is not a field the pattern ${pattern} holds a time in`);
95
+ }
96
+ return {
97
+ path: "geonosis-probe-order.ts",
98
+ source: `export const geonosisProbe = (left, right) => String(left.${field}).localeCompare(String(right.${field}))
99
+ `
100
+ };
95
101
  }
96
102
  };
97
103
  var no_date_string_compare_default = noDateStringCompare;
@@ -1,4 +1,5 @@
1
1
  import { type RuleContext } from './lib/context.js';
2
+ import { type Probe } from './lib/probe.js';
2
3
  interface JSXNode {
3
4
  attributes: {
4
5
  name?: {
@@ -51,5 +52,6 @@ declare const noDuplicateJsxPatterns: {
51
52
  schema: never[];
52
53
  type: 'problem';
53
54
  };
55
+ probe(): Probe;
54
56
  };
55
57
  export default noDuplicateJsxPatterns;
@@ -1,6 +1,6 @@
1
1
  // src/rules/no-duplicate-jsx-patterns.ts
2
2
  import { normalise } from "./lib/paths.js";
3
- var COMPONENT_FILE_PATTERN = /(?:^|\/)(?:features\/[^/]+\/)?(compounds|organelles|cells|tissues|organs)\/.+\.tsx$/;
3
+ import { COMPONENT_FILE_PATTERN } from "./lib/tiers.js";
4
4
  var MIN_CLASS_LENGTH = 30;
5
5
  var MIN_DUPLICATES = 3;
6
6
  var ITERATORS = new Set(["map", "flatMap", "forEach", "filter", "reduce"]);
@@ -95,6 +95,21 @@ their text are one component with a prop.`,
95
95
  },
96
96
  schema: [],
97
97
  type: "problem"
98
+ },
99
+ probe() {
100
+ return {
101
+ path: "src/cells/geonosis-probe.tsx",
102
+ source: `export function GeonosisProbe() {
103
+ return (
104
+ <div>
105
+ <p className="mb-[4px] text-[12px] leading-[16px] font-bold">a</p>
106
+ <p className="mb-[4px] text-[12px] leading-[16px] font-bold">b</p>
107
+ <p className="mb-[4px] text-[12px] leading-[16px] font-bold">c</p>
108
+ </div>
109
+ )
110
+ }
111
+ `
112
+ };
98
113
  }
99
114
  };
100
115
  var no_duplicate_jsx_patterns_default = noDuplicateJsxPatterns;
@@ -1,8 +1,35 @@
1
1
  import { type RuleContextWithRawOptions } from './lib/context.js';
2
2
  import { type ProbeTree } from './lib/probe.js';
3
+ interface Declared {
4
+ declarations?: {
5
+ id?: {
6
+ name?: string;
7
+ };
8
+ }[];
9
+ id?: {
10
+ name?: string;
11
+ };
12
+ }
13
+ interface Statement {
14
+ declaration?: Declared | null;
15
+ exported?: {
16
+ name?: string;
17
+ value?: string;
18
+ } | null;
19
+ source?: unknown;
20
+ specifiers?: {
21
+ exported?: {
22
+ name?: string;
23
+ value?: string;
24
+ };
25
+ }[];
26
+ type?: string;
27
+ }
3
28
  declare const noGenericUtilityPackage: {
4
29
  create(context: RuleContextWithRawOptions): {
5
- Program(node: unknown): void;
30
+ Program(node: {
31
+ body?: Statement[];
32
+ }): void;
6
33
  };
7
34
  fixShape: string;
8
35
  meta: {
@@ -10,11 +37,31 @@ declare const noGenericUtilityPackage: {
10
37
  description: string;
11
38
  };
12
39
  messages: {
40
+ beyondTheDoor: string;
13
41
  generic: string;
14
42
  };
15
43
  schema: {
16
44
  additionalProperties: boolean;
17
45
  properties: {
46
+ allow: {
47
+ items: {
48
+ additionalProperties: boolean;
49
+ properties: {
50
+ exports: {
51
+ items: {
52
+ type: 'string';
53
+ };
54
+ type: 'array';
55
+ };
56
+ package: {
57
+ type: 'string';
58
+ };
59
+ };
60
+ required: string[];
61
+ type: 'object';
62
+ };
63
+ type: 'array';
64
+ };
18
65
  entries: {
19
66
  items: {
20
67
  type: 'string';
@@ -32,7 +79,6 @@ declare const noGenericUtilityPackage: {
32
79
  }[];
33
80
  type: 'problem';
34
81
  };
35
- /** The manifest AND the entry: the name is in the manifest, and the finding lands on the entry. */
36
82
  probe(options: readonly unknown[]): ProbeTree;
37
83
  };
38
84
  export default noGenericUtilityPackage;
@@ -1,8 +1,8 @@
1
1
  // src/rules/no-generic-utility-package.ts
2
- import { readFileSync } from "node:fs";
2
+ import { readManifest } from "./lib/module-text.js";
3
3
  import { normalise } from "./lib/paths.js";
4
4
  import { bagOf } from "./lib/probe.js";
5
- import { optionsBag, STRINGS } from "./lib/schema.js";
5
+ import { optionsBag, STRING, STRINGS } from "./lib/schema.js";
6
6
  var NAMES = ["common", "helpers", "misc", "shared", "utils"];
7
7
  var ENTRIES = [
8
8
  "index.ts",
@@ -15,19 +15,41 @@ var ENTRIES = [
15
15
  "src/index.jsx"
16
16
  ];
17
17
  var MANIFEST = "package.json";
18
- var nameOf = (root) => {
19
- try {
20
- const manifest = JSON.parse(readFileSync(`${root}/${MANIFEST}`, "utf8"));
21
- return typeof manifest.name === "string" ? manifest.name : null;
22
- } catch {
23
- return null;
18
+ var exportedNames = (body) => body.flatMap((statement) => {
19
+ if (statement.type === "ExportDefaultDeclaration")
20
+ return ["default"];
21
+ if (statement.type === "ExportAllDeclaration") {
22
+ return [statement.exported?.name ?? statement.exported?.value ?? "*"];
24
23
  }
24
+ if (statement.type !== "ExportNamedDeclaration")
25
+ return [];
26
+ const declared = statement.declaration;
27
+ const fromDeclaration = [
28
+ declared?.id?.name,
29
+ ...(declared?.declarations ?? []).map((one) => one.id?.name)
30
+ ];
31
+ const fromSpecifiers = (statement.specifiers ?? []).map((one) => one.exported?.name ?? one.exported?.value);
32
+ return [...fromDeclaration, ...fromSpecifiers].filter((one) => one !== undefined);
33
+ });
34
+ var doorSchema = {
35
+ items: {
36
+ additionalProperties: false,
37
+ properties: { exports: STRINGS, package: STRING },
38
+ required: ["package", "exports"],
39
+ type: "object"
40
+ },
41
+ type: "array"
42
+ };
43
+ var nameOf = (root) => {
44
+ const name = readManifest(`${root}/${MANIFEST}`)?.name;
45
+ return typeof name === "string" ? name : null;
25
46
  };
26
47
  var noGenericUtilityPackage = {
27
48
  create(context) {
28
49
  const options = bagOf(context.options);
29
50
  const names = new Set(options.names ?? NAMES);
30
51
  const entries = options.entries ?? ENTRIES;
52
+ const allowed = options.allow ?? [];
31
53
  const filename = normalise(context.filename);
32
54
  return {
33
55
  Program(node) {
@@ -40,6 +62,18 @@ var noGenericUtilityPackage = {
40
62
  const last = name.split("/").at(-1) ?? "";
41
63
  if (!names.has(last))
42
64
  return;
65
+ const door = allowed.find((one) => one.package === name);
66
+ if (door !== undefined) {
67
+ const extra = exportedNames(node.body ?? []).filter((one) => !door.exports.includes(one));
68
+ if (extra.length === 0)
69
+ return;
70
+ context.report({
71
+ data: { extra: extra.join(", "), name, allowed: door.exports.join(", ") },
72
+ messageId: "beyondTheDoor",
73
+ node
74
+ });
75
+ return;
76
+ }
43
77
  context.report({ data: { generic: last, name }, messageId: "generic", node });
44
78
  }
45
79
  };
@@ -49,15 +83,20 @@ about what is inside, so nothing can be refused entry and everything arrives: we
49
83
  object, a time door. Split it by what each export is FOR and give each half a name that would make a
50
84
  wrong import obviously wrong — the formatting goes to the feature's own lib, the constants go beside
51
85
  their owner, a door gets its own package named after the thing it is a door to. Delete the rest. The
52
- anti-names are the \`names\` option and the default is the universal list, never a repo's vocabulary.`,
86
+ anti-names are the \`names\` option and the default is the universal list, never a repo's vocabulary.
87
+ One package may keep a generic name for one reason: \`allow: [{ package, exports }]\` names the door it
88
+ survives to hold, and the package is clean while its entry exports nothing else — a second export is
89
+ the finding. A star re-export cannot be vouched for and counts as one.`,
53
90
  meta: {
54
91
  docs: {
55
92
  description: "A workspace package named `utils` / `common` / `helpers` / `shared` / `misc` is named for nothing, so it accretes everything. Reported once, at the package entry, from the name in its own manifest."
56
93
  },
57
94
  messages: {
95
+ beyondTheDoor: 'The package "{{name}}" is allowed to hold {{allowed}} and nothing else, and it also exports {{extra}}. Move each of those to the package named for what it does.',
58
96
  generic: 'The package "{{name}}" is named for nothing: "{{generic}}" says what is inside to no one, so nothing can be refused entry and everything arrives. Split it by what each export is for and name each half after the thing it does.'
59
97
  },
60
98
  schema: optionsBag({
99
+ allow: doorSchema,
61
100
  entries: STRINGS,
62
101
  names: STRINGS
63
102
  }),