@ethisyscore/eslint-plugin-coreconnect 1.91.0 → 1.92.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ethisyscore/eslint-plugin-coreconnect",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.92.1",
|
|
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",
|
package/src/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import noLocalCompletenessBanner from "./rules/no-local-completeness-banner.js";
|
|
|
5
5
|
import noLocalDataGrid from "./rules/no-local-data-grid.js";
|
|
6
6
|
import noLocalDateInput from "./rules/no-local-date-input.js";
|
|
7
7
|
import noLocalEmptyState from "./rules/no-local-empty-state.js";
|
|
8
|
+
import noLocalInProgressBanner from "./rules/no-local-in-progress-banner.js";
|
|
8
9
|
import noLocalMappingEditor from "./rules/no-local-mapping-editor.js";
|
|
9
10
|
import noLocalSearchInput from "./rules/no-local-search-input.js";
|
|
10
11
|
import noParamInNavPath from "./rules/no-param-in-nav-path.js";
|
|
@@ -17,6 +18,7 @@ const rules = {
|
|
|
17
18
|
"no-local-data-grid": noLocalDataGrid,
|
|
18
19
|
"no-local-date-input": noLocalDateInput,
|
|
19
20
|
"no-local-empty-state": noLocalEmptyState,
|
|
21
|
+
"no-local-in-progress-banner": noLocalInProgressBanner,
|
|
20
22
|
"no-local-mapping-editor": noLocalMappingEditor,
|
|
21
23
|
"no-local-search-input": noLocalSearchInput,
|
|
22
24
|
"no-param-in-nav-path": noParamInNavPath,
|
|
@@ -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({
|
|
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
|
-
|
|
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") {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disallow a plugin declaring its own in-progress / background-job banner.
|
|
3
|
+
*
|
|
4
|
+
* The backstop to a promotion, and the sibling of `no-local-empty-state` - deliberately, because
|
|
5
|
+
* the two components are one decision. A surface with no in-progress state does not stay silent: it
|
|
6
|
+
* renders its EMPTY state instead, which claims the operation finished and produced nothing. That
|
|
7
|
+
* defect shipped twice in one week on a single page (engineering-skills UX-113).
|
|
8
|
+
*
|
|
9
|
+
* Three hand-written copies existed at promotion time - the tech repo-import indicator, the
|
|
10
|
+
* self-healing fix-run banner, and the monolith's own OperationGlobalBanner - with a byte-identical
|
|
11
|
+
* `height: 6, borderRadius: 3` progress bar, and that same pair repeated in ten more places. A rule
|
|
12
|
+
* describing the correct markup did not stop the second occurrence; one implementation can.
|
|
13
|
+
*
|
|
14
|
+
* Fires on a DECLARATION, never a use, so importing `InProgressBanner` from
|
|
15
|
+
* `@ethisyscore/plugin-ui/components/shared` and rendering it is what the rule wants - including
|
|
16
|
+
* wrapping it in a plugin-specific component that feeds it that plugin's own progress data, which
|
|
17
|
+
* the shared helper exempts by recognising the canonical binding.
|
|
18
|
+
*
|
|
19
|
+
* @type {import("eslint").Rule.RuleModule}
|
|
20
|
+
*/
|
|
21
|
+
import { createNoLocalComponentRule } from "./componentDeclarations.js";
|
|
22
|
+
|
|
23
|
+
export default createNoLocalComponentRule({
|
|
24
|
+
// The names this banner keeps being rewritten under. `Indicator` is included because the tech
|
|
25
|
+
// copy was called `TechImportIndicator`, and a suffix match is what catches the plugin-prefixed
|
|
26
|
+
// spellings; the shared helper's hook guard keeps `useImportIndicator` out of it.
|
|
27
|
+
pattern: /(InProgressBanner|ImportIndicator|ProgressBanner|ProgressIndicator|OperationBanner|BusyBanner)$/,
|
|
28
|
+
canonical: { source: "@ethisyscore/plugin-ui/components/shared", name: "InProgressBanner" },
|
|
29
|
+
messageId: "noLocalInProgressBanner",
|
|
30
|
+
description:
|
|
31
|
+
"Disallow a locally-declared in-progress banner - import InProgressBanner from @ethisyscore/plugin-ui/components/shared",
|
|
32
|
+
message:
|
|
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,
|
|
40
|
+
});
|