@ethisyscore/eslint-plugin-coreconnect 1.92.0 → 1.93.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ethisyscore/eslint-plugin-coreconnect",
3
- "version": "1.92.0",
3
+ "version": "1.93.0",
4
4
  "description": "ESLint rules enforcing EthisysCore plugin frontend conventions. Published so a new rule reaches every plugin on a version bump, rather than being copied into each scaffold and drifting.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -87,9 +87,32 @@ export function looksLikeComponent(init) {
87
87
  * @param {string} options.message
88
88
  * @param {{ source: string, name: string }} options.canonical The promoted component this rule
89
89
  * points people at. A local declaration that COMPOSES it is exempt - see below.
90
+ * @param {boolean} [options.fileScopedExemption] Exempt EVERY matching declaration in a file
91
+ * that composes the canonical component anywhere, instead of only the declaration whose own
92
+ * body reaches it.
93
+ *
94
+ * <p>Off by default, because range containment is what keeps these rules narrow. Opt in when
95
+ * the pattern deliberately matches FEATURE-SURFACE names rather than component names - the
96
+ * in-progress-banner rule matches `TechImportIndicator`, whose legitimate shape is a top-level
97
+ * component that renders the shared banner through a SIBLING helper. Range containment cannot
98
+ * express that: the helper composes the canonical component, the outer name is what matches the
99
+ * pattern, and the two are different declarations. Without this the rule fires on correctly
100
+ * adopted code - which it did, on the first real adoption, because the test asserting the
101
+ * exemption used the easy shape where the wrapper renders the canonical component directly.</p>
102
+ *
103
+ * <p>The cost, stated: a file that composes the canonical component AND hand-rolls a second
104
+ * banner beside it goes unreported. Accepted for that rule only - a false positive on every
105
+ * correct adoption is the worse failure, because it trains people to switch the rule off.</p>
90
106
  * @returns {import("eslint").Rule.RuleModule}
91
107
  */
92
- export function createNoLocalComponentRule({ pattern, messageId, description, message, canonical }) {
108
+ export function createNoLocalComponentRule({
109
+ pattern,
110
+ messageId,
111
+ description,
112
+ message,
113
+ canonical,
114
+ fileScopedExemption = false,
115
+ }) {
93
116
  // Fail at construction, not on the first source file that happens to contain an import. Without
94
117
  // this a fifth rule added without a `canonical` line throws a TypeError from inside the
95
118
  // ImportDeclaration visitor, which surfaces as an opaque lint crash on an arbitrary file.
@@ -145,7 +168,9 @@ export function createNoLocalComponentRule({ pattern, messageId, description, me
145
168
  * one next door still reports the second.
146
169
  */
147
170
  const composesCanonical = (node) =>
148
- canonicalUses.some((at) => at >= node.range[0] && at <= node.range[1]);
171
+ fileScopedExemption
172
+ ? canonicalUses.length > 0
173
+ : canonicalUses.some((at) => at >= node.range[0] && at <= node.range[1]);
149
174
 
150
175
  const check = (idNode, declarationNode) => {
151
176
  if (!idNode || idNode.type !== "Identifier") {
@@ -31,4 +31,10 @@ export default createNoLocalComponentRule({
31
31
  "Disallow a locally-declared in-progress banner - import InProgressBanner from @ethisyscore/plugin-ui/components/shared",
32
32
  message:
33
33
  "'{{name}}' looks like a local in-progress banner. Import { InProgressBanner } from '@ethisyscore/plugin-ui/components/shared' instead, so a background job looks the same wherever it appears. It takes title, children, value (omit for an indeterminate bar), action and sx.",
34
+ // FILE-SCOPED, unlike its four siblings, because this pattern matches feature-surface names.
35
+ // `TechImportIndicator` is a top-level component that renders the shared banner through a
36
+ // SIBLING helper (`ImportingGroup`), so the declaration matching the pattern and the one
37
+ // composing the canonical component are different nodes and range containment cannot exempt it.
38
+ // Without this the rule fired on the very first correct adoption.
39
+ fileScopedExemption: true,
34
40
  });