@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.
Files changed (2) hide show
  1. package/package.json +2 -1
  2. 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.0",
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 };