@md-code/react-component-uniqueness 0.1.0 → 0.1.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 +2 -1
- package/suggestions.js +103 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@md-code/react-component-uniqueness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "ESLint rule that enforces one canonical location per React component: duplicate declarations, raw HTML outside the canonical packages, hand-rolled components by behavior, and styled.* created in the app.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "index.js",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"resolve.js",
|
|
19
19
|
"dom.js",
|
|
20
20
|
"report.js",
|
|
21
|
+
"suggestions.js",
|
|
21
22
|
"errorTypes/",
|
|
22
23
|
"scanner/",
|
|
23
24
|
"scanner/filters/",
|
package/suggestions.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* suggestions.js
|
|
5
|
+
*
|
|
6
|
+
* Deterministic, table-driven suggestions for every finding. Given the
|
|
7
|
+
* behavior string (`what`) or the raw tag+type, resolve the canonical
|
|
8
|
+
* component name + path the developer should use instead. No LLM — pure
|
|
9
|
+
* data from the component registry (reports/component-registry.json) and the
|
|
10
|
+
* component catalog (reports/component-catalog.json).
|
|
11
|
+
*
|
|
12
|
+
* Every resolver returns { component, path } — always defined, so the report
|
|
13
|
+
* message can always say "use `X` from `path`".
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { kebabToPascal } = require("./scanner/matching");
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Strip the "(a hand-rolled control|button)" suffix and punctuation so a
|
|
20
|
+
* verbose behavior string yields clean words.
|
|
21
|
+
*/
|
|
22
|
+
function clean(what) {
|
|
23
|
+
return String(what).replace(/\s*\(a hand-rolled (?:control|button)\)/, "").trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Candidate component names derived from a behavior string: the PascalCase of
|
|
28
|
+
* each meaningful word, longest first. No component names are enumerated — the
|
|
29
|
+
* registry / catalog decides which candidate actually exists.
|
|
30
|
+
*/
|
|
31
|
+
function candidatesFromWords(text) {
|
|
32
|
+
const words = clean(text)
|
|
33
|
+
.toLowerCase()
|
|
34
|
+
.split(/[^a-z0-9]+/)
|
|
35
|
+
.filter((w) => w && w !== "a" && w !== "hand" && w !== "rolled" && w !== "element" && w !== "indicator" && w !== "region" && w !== "control" && w !== "the" && w !== "and");
|
|
36
|
+
const out = [];
|
|
37
|
+
for (const w of words) out.push(kebabToPascal(w));
|
|
38
|
+
if (words.length >= 2) {
|
|
39
|
+
for (let i = 0; i < words.length - 1; i++) out.push(kebabToPascal(words.slice(i, i + 2).join("-")));
|
|
40
|
+
}
|
|
41
|
+
return [...new Set(out)];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* First candidate that exists in the registry (by exported name) or the
|
|
46
|
+
* catalog (by component name). Returns { component, path } or null.
|
|
47
|
+
*/
|
|
48
|
+
function resolve(candidates, registry, catalogComponents) {
|
|
49
|
+
for (const name of candidates) {
|
|
50
|
+
const paths = registry && registry.names && registry.names[name];
|
|
51
|
+
if (paths && paths.length) return { component: name, path: paths[0] };
|
|
52
|
+
}
|
|
53
|
+
for (const name of candidates) {
|
|
54
|
+
const hit = (catalogComponents || []).find((c) => c.name === name);
|
|
55
|
+
if (hit) return { component: name, path: hit.path };
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Fallback path when neither the registry nor the catalog knows the
|
|
62
|
+
* suggested component: the first canonical component folder passed by the
|
|
63
|
+
* consumer, or the generic phrase when none was configured.
|
|
64
|
+
*/
|
|
65
|
+
function fallbackPath(componentsFolder) {
|
|
66
|
+
return componentsFolder && componentsFolder.length ? componentsFolder[0] : "the canonical component packages";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Deterministic suggestion for a behavior finding (layoutPrimitive).
|
|
71
|
+
*
|
|
72
|
+
* @param {string} what the behavior string from behaviorOf / behaviorForRaw
|
|
73
|
+
* @param {object} registry the component registry { names: {Name:[paths]} }
|
|
74
|
+
* @param {Array} catalogComponents the catalog component signatures
|
|
75
|
+
* @param {string[]} [componentsFolder] canonical folders (fallback path source)
|
|
76
|
+
* @returns {{component:string, path:string}}
|
|
77
|
+
*/
|
|
78
|
+
function forBehavior(what, registry, catalogComponents, componentsFolder) {
|
|
79
|
+
const candidates = candidatesFromWords(what);
|
|
80
|
+
return resolve(candidates, registry, catalogComponents) || { component: candidates[0] || clean(what), path: fallbackPath(componentsFolder) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Deterministic suggestion for a rawHtml finding.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} tag the raw tag (button, input, select, ...)
|
|
87
|
+
* @param {string|undefined} type the input type (only for tag=input)
|
|
88
|
+
* @param {object} registry the component registry
|
|
89
|
+
* @param {Array} catalogComponents the catalog component signatures
|
|
90
|
+
* @param {string[]} [componentsFolder] canonical folders (fallback path source)
|
|
91
|
+
* @returns {{component:string, path:string}}
|
|
92
|
+
*/
|
|
93
|
+
function forRaw(tag, type, registry, catalogComponents, componentsFolder) {
|
|
94
|
+
let candidates;
|
|
95
|
+
if (tag === "input" && type) {
|
|
96
|
+
candidates = [...new Set([kebabToPascal(`${type}-input`), kebabToPascal(type), "Input"])];
|
|
97
|
+
} else {
|
|
98
|
+
candidates = [kebabToPascal(tag)];
|
|
99
|
+
}
|
|
100
|
+
return resolve(candidates, registry, catalogComponents) || { component: candidates[0], path: fallbackPath(componentsFolder) };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = { forBehavior, forRaw };
|