@invariant-ui/cli 0.0.0-stage → 0.1.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/LICENSE +105 -0
- package/README.md +248 -2
- package/dist/agent-kit/apply.js +232 -0
- package/dist/agent-kit/guidance.js +236 -0
- package/dist/agent-kit/index.js +5 -0
- package/dist/agent-kit/integrations.js +121 -0
- package/dist/agent-kit/plugin.js +115 -0
- package/dist/artifact-generator/index.js +75 -0
- package/dist/browser-verify/driver.js +2 -0
- package/dist/browser-verify/index.js +4 -0
- package/dist/browser-verify/playwright.js +82 -0
- package/dist/browser-verify/runner.js +175 -0
- package/dist/browser-verify/spec.js +103 -0
- package/dist/cli/args.js +77 -0
- package/dist/cli/baseline.js +64 -0
- package/dist/cli/cli.js +121 -0
- package/dist/cli/commands-browser.js +104 -0
- package/dist/cli/commands-discover.js +312 -0
- package/dist/cli/commands-enforcement.js +35 -0
- package/dist/cli/commands-governance.js +179 -0
- package/dist/cli/commands-hook.js +269 -0
- package/dist/cli/commands-patterns.js +324 -0
- package/dist/cli/commands-setup.js +485 -0
- package/dist/cli/commands-validate.js +157 -0
- package/dist/cli/commands.js +517 -0
- package/dist/cli/config.js +197 -0
- package/dist/cli/diff.js +66 -0
- package/dist/cli/enforcement-log.js +83 -0
- package/dist/cli/enforcement-report.js +96 -0
- package/dist/cli/errors.js +10 -0
- package/dist/cli/format.js +22 -0
- package/dist/cli/index.js +5 -0
- package/dist/cli/io.js +2 -0
- package/dist/cli/main.js +31 -0
- package/dist/cli/pipeline.js +262 -0
- package/dist/cli/publish.js +77 -0
- package/dist/cli/report.js +127 -0
- package/dist/cli/starter-rules.js +57 -0
- package/dist/cli/store.js +60 -0
- package/dist/cli/version.js +5 -0
- package/dist/cli/watch.js +68 -0
- package/dist/cli/workflow.js +52 -0
- package/dist/compiler-angular/adapter.js +115 -0
- package/dist/compiler-angular/analyze.js +222 -0
- package/dist/compiler-angular/classes.js +446 -0
- package/dist/compiler-angular/declared.js +285 -0
- package/dist/compiler-angular/emit.js +183 -0
- package/dist/compiler-angular/index.js +4 -0
- package/dist/compiler-angular/template.js +142 -0
- package/dist/compiler-core/adapter.js +2 -0
- package/dist/compiler-core/adapters/inventory.js +312 -0
- package/dist/compiler-core/compile.js +148 -0
- package/dist/compiler-core/component-facts.js +123 -0
- package/dist/compiler-core/discovery/docs.js +66 -0
- package/dist/compiler-core/discovery/entries.js +270 -0
- package/dist/compiler-core/discovery/exports.js +207 -0
- package/dist/compiler-core/discovery/graph.js +59 -0
- package/dist/compiler-core/discovery/index.js +103 -0
- package/dist/compiler-core/discovery/packages.js +148 -0
- package/dist/compiler-core/discovery/schema.js +144 -0
- package/dist/compiler-core/discovery/storybook.js +232 -0
- package/dist/compiler-core/discovery/support.js +82 -0
- package/dist/compiler-core/discovery/tokens.js +186 -0
- package/dist/compiler-core/discovery/tooling.js +146 -0
- package/dist/compiler-core/fs.js +89 -0
- package/dist/compiler-core/glob.js +3 -0
- package/dist/compiler-core/index.js +16 -0
- package/dist/compiler-core/ir-builder.js +68 -0
- package/dist/compiler-core/json-schema.js +11 -0
- package/dist/compiler-core/testing/index.js +81 -0
- package/dist/compiler-core/ts/ast.js +166 -0
- package/dist/compiler-core/ts/members.js +163 -0
- package/dist/compiler-core/ts/program.js +81 -0
- package/dist/compiler-core/ts/static-eval.js +145 -0
- package/dist/compiler-core/ts/tsconfig-paths.js +60 -0
- package/dist/compiler-core/walk.js +46 -0
- package/dist/compiler-ir/index.js +4 -0
- package/dist/compiler-ir/ir.js +67 -0
- package/dist/compiler-ir/json-schema.js +6 -0
- package/dist/compiler-ir/validate.js +116 -0
- package/dist/compiler-react/adapter.js +147 -0
- package/dist/compiler-react/analyze.js +199 -0
- package/dist/compiler-react/ast.js +105 -0
- package/dist/compiler-react/behavior.js +75 -0
- package/dist/compiler-react/detect.js +558 -0
- package/dist/compiler-react/emit.js +188 -0
- package/dist/compiler-react/index.js +2 -0
- package/dist/compiler-react/program.js +58 -0
- package/dist/compiler-react/props.js +157 -0
- package/dist/compiler-react/styling.js +138 -0
- package/dist/compiler-react/usage.js +116 -0
- package/dist/compiler-react/variants.js +111 -0
- package/dist/compiler-storybook/adapter.js +253 -0
- package/dist/compiler-storybook/csf.js +521 -0
- package/dist/compiler-storybook/emit.js +202 -0
- package/dist/compiler-storybook/index-json.js +41 -0
- package/dist/compiler-storybook/index.js +5 -0
- package/dist/compiler-storybook/mdx.js +120 -0
- package/dist/compiler-storybook/static-eval.js +145 -0
- package/dist/compiler-tokens/adapter.js +158 -0
- package/dist/compiler-tokens/css.js +59 -0
- package/dist/compiler-tokens/index.js +5 -0
- package/dist/compiler-tokens/json-lines.js +106 -0
- package/dist/compiler-tokens/json-tokens.js +52 -0
- package/dist/compiler-vue/adapter.js +107 -0
- package/dist/compiler-vue/analyze.js +344 -0
- package/dist/compiler-vue/declared.js +343 -0
- package/dist/compiler-vue/emit.js +129 -0
- package/dist/compiler-vue/index.js +5 -0
- package/dist/compiler-vue/script.js +472 -0
- package/dist/compiler-vue/sfc.js +44 -0
- package/dist/compiler-vue/template.js +73 -0
- package/dist/contract/archetype-checks.js +153 -0
- package/dist/contract/components.js +102 -0
- package/dist/contract/conflicts.js +39 -0
- package/dist/contract/contract.js +46 -0
- package/dist/contract/evidence.js +84 -0
- package/dist/contract/fact-path.js +39 -0
- package/dist/contract/facts.js +62 -0
- package/dist/contract/index.js +16 -0
- package/dist/contract/integrity.js +21 -0
- package/dist/contract/json-schema.js +11 -0
- package/dist/contract/knowledge.js +95 -0
- package/dist/contract/pattern-checks.js +254 -0
- package/dist/contract/patterns.js +225 -0
- package/dist/contract/primitives.js +81 -0
- package/dist/contract/sources.js +30 -0
- package/dist/contract/tokens.js +41 -0
- package/dist/contract/validate.js +253 -0
- package/dist/contract-query/archetype.js +188 -0
- package/dist/contract-query/index.js +6 -0
- package/dist/contract-query/patterns.js +298 -0
- package/dist/contract-query/scan.js +164 -0
- package/dist/contract-query/search.js +132 -0
- package/dist/contract-query/system.js +148 -0
- package/dist/contract-query/validate-code.js +239 -0
- package/dist/contract-query/validate-usage.js +150 -0
- package/dist/contract-query/views.js +288 -0
- package/dist/governance/decisions.js +229 -0
- package/dist/governance/index.js +5 -0
- package/dist/governance/patterns-file.js +156 -0
- package/dist/governance/patterns.js +510 -0
- package/dist/governance/policies.js +148 -0
- package/dist/mcp/index.js +7 -0
- package/dist/mcp/loader.js +70 -0
- package/dist/mcp/protocol.js +27 -0
- package/dist/mcp/resources.js +138 -0
- package/dist/mcp/server.js +177 -0
- package/dist/mcp/stdio.js +28 -0
- package/dist/mcp/tools.js +267 -0
- package/dist/reconciliation/code.js +62 -0
- package/dist/reconciliation/components.js +401 -0
- package/dist/reconciliation/conflicts.js +126 -0
- package/dist/reconciliation/facts.js +41 -0
- package/dist/reconciliation/index.js +7 -0
- package/dist/reconciliation/reconcile.js +134 -0
- package/dist/reconciliation/resolve.js +158 -0
- package/dist/reconciliation/review.js +81 -0
- package/dist/reconciliation/storybook.js +370 -0
- package/dist/reconciliation/tokens.js +179 -0
- package/dist/reconciliation/util.js +55 -0
- package/dist/semantic-engine/additive.js +51 -0
- package/dist/semantic-engine/compounds.js +91 -0
- package/dist/semantic-engine/documents.js +157 -0
- package/dist/semantic-engine/enrich.js +42 -0
- package/dist/semantic-engine/index.js +7 -0
- package/dist/semantic-engine/infer.js +38 -0
- package/dist/semantic-engine/tokens.js +120 -0
- package/dist/shared/canonical.js +65 -0
- package/dist/shared/diagnostic.js +78 -0
- package/dist/shared/glob.js +98 -0
- package/dist/shared/index.js +4 -0
- package/dist/shared/json.js +2 -0
- package/dist/validation/card.js +50 -0
- package/dist/validation/discovery/arrangements.js +162 -0
- package/dist/validation/discovery/discover.js +70 -0
- package/dist/validation/discovery/index.js +8 -0
- package/dist/validation/discovery/model.js +127 -0
- package/dist/validation/discovery/promote.js +24 -0
- package/dist/validation/discovery/reviews.js +63 -0
- package/dist/validation/discovery/shapes.js +47 -0
- package/dist/validation/discovery/similar-files.js +146 -0
- package/dist/validation/discovery/util.js +47 -0
- package/dist/validation/index.js +11 -0
- package/dist/validation/interaction-facts.js +341 -0
- package/dist/validation/interaction-workflows.js +633 -0
- package/dist/validation/literals.js +251 -0
- package/dist/validation/pattern-rules.js +608 -0
- package/dist/validation/policy.js +241 -0
- package/dist/validation/report.js +163 -0
- package/dist/validation/rules.js +269 -0
- package/dist/validation/scan.js +295 -0
- package/dist/validation/text.js +34 -0
- package/dist/validation/validate-code.js +513 -0
- package/dist/validation/validate-usage.js +196 -0
- package/package.json +38 -4
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
export const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
2
|
+
export const normalize = (text) => text.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
3
|
+
/**
|
|
4
|
+
* Read model over one compiled contract and its evidence. Pure and deterministic: it never touches the
|
|
5
|
+
* file system, a network or a model, and everything it returns is plain JSON.
|
|
6
|
+
*/
|
|
7
|
+
export class DesignSystem {
|
|
8
|
+
contract;
|
|
9
|
+
contentHash;
|
|
10
|
+
evidence = new Map();
|
|
11
|
+
byId = new Map();
|
|
12
|
+
patternsById = new Map();
|
|
13
|
+
containers = new Map();
|
|
14
|
+
constructor(contract, evidence) {
|
|
15
|
+
this.contract = contract;
|
|
16
|
+
this.contentHash = contract.integrity.contentHash;
|
|
17
|
+
for (const e of evidence?.evidence ?? [])
|
|
18
|
+
this.evidence.set(e.id, e);
|
|
19
|
+
for (const c of contract.components)
|
|
20
|
+
this.byId.set(c.id, c);
|
|
21
|
+
for (const p of contract.patterns)
|
|
22
|
+
this.patternsById.set(p.id, p);
|
|
23
|
+
for (const c of contract.components)
|
|
24
|
+
c.composition.forEach((r, i) => {
|
|
25
|
+
if (r.type !== "contains")
|
|
26
|
+
return;
|
|
27
|
+
const list = this.containers.get(r.target) ?? [];
|
|
28
|
+
const fact = c.facts[`composition.${i}`];
|
|
29
|
+
list.push({ id: c.id, ...(fact ? { fact } : {}) });
|
|
30
|
+
this.containers.set(r.target, list);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
component(id) {
|
|
34
|
+
return this.byId.get(id);
|
|
35
|
+
}
|
|
36
|
+
/** Resolves an id, an id without its prefix, or a name (ignoring case and separators). */
|
|
37
|
+
findComponent(query) {
|
|
38
|
+
const q = query.trim();
|
|
39
|
+
const exact = this.byId.get(q) ?? this.byId.get(`component:${q.toLowerCase()}`);
|
|
40
|
+
if (exact)
|
|
41
|
+
return { match: exact, candidates: [exact] };
|
|
42
|
+
const n = normalize(q);
|
|
43
|
+
const named = this.contract.components.filter((c) => normalize(c.name) === n || normalize(c.id.slice(10)) === n);
|
|
44
|
+
if (named.length === 1)
|
|
45
|
+
return { match: named[0], candidates: named };
|
|
46
|
+
if (named.length > 1)
|
|
47
|
+
return { candidates: named };
|
|
48
|
+
const near = this.contract.components
|
|
49
|
+
.filter((c) => n.length >= 2 && (normalize(c.name).includes(n) || n.includes(normalize(c.name))))
|
|
50
|
+
.slice(0, 8);
|
|
51
|
+
return { candidates: near };
|
|
52
|
+
}
|
|
53
|
+
pattern(id) {
|
|
54
|
+
return this.patternsById.get(id);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolves a pattern by id, by id without its prefix (`page.data-management`), by the part after the
|
|
58
|
+
* category (`data-management`), or by name, ignoring case and separators. Several matches yield candidates only.
|
|
59
|
+
*/
|
|
60
|
+
findPattern(query) {
|
|
61
|
+
const q = query.trim();
|
|
62
|
+
const exact = this.patternsById.get(q) ?? this.patternsById.get(`pattern:${q.toLowerCase()}`);
|
|
63
|
+
if (exact)
|
|
64
|
+
return { match: exact, candidates: [exact] };
|
|
65
|
+
const n = normalize(q);
|
|
66
|
+
const named = this.contract.patterns.filter((p) => {
|
|
67
|
+
const body = p.id.slice("pattern:".length);
|
|
68
|
+
const afterCategory = body.includes(".") ? body.slice(body.indexOf(".") + 1) : body;
|
|
69
|
+
return normalize(p.name) === n || normalize(body) === n || normalize(afterCategory) === n;
|
|
70
|
+
});
|
|
71
|
+
if (named.length === 1)
|
|
72
|
+
return { match: named[0], candidates: named };
|
|
73
|
+
if (named.length > 1)
|
|
74
|
+
return { candidates: named };
|
|
75
|
+
const near = this.contract.patterns
|
|
76
|
+
.filter((p) => n.length >= 2 && (normalize(p.name).includes(n) || n.includes(normalize(p.name))))
|
|
77
|
+
.slice(0, 8);
|
|
78
|
+
return { candidates: near };
|
|
79
|
+
}
|
|
80
|
+
/** The evidence entries for these ids, in the given order. Ids missing from the evidence file are skipped. */
|
|
81
|
+
evidenceEntries(ids) {
|
|
82
|
+
return ids.flatMap((id) => {
|
|
83
|
+
const e = this.evidence.get(id);
|
|
84
|
+
return e ? [e] : [];
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
containedBy(id) {
|
|
88
|
+
return this.containers.get(id) ?? [];
|
|
89
|
+
}
|
|
90
|
+
location(ids) {
|
|
91
|
+
for (const id of ids) {
|
|
92
|
+
const e = this.evidence.get(id);
|
|
93
|
+
if (!e)
|
|
94
|
+
continue;
|
|
95
|
+
if (e.location.type === "file")
|
|
96
|
+
return `${e.location.path}${e.location.line ? `:${e.location.line}` : ""}`;
|
|
97
|
+
return e.sourceRef;
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
factView(facts, path) {
|
|
102
|
+
const f = facts[path];
|
|
103
|
+
if (!f)
|
|
104
|
+
return undefined;
|
|
105
|
+
const source = this.location(f.evidenceIds);
|
|
106
|
+
return {
|
|
107
|
+
status: f.status,
|
|
108
|
+
...(f.confidence === undefined ? {} : { confidence: f.confidence }),
|
|
109
|
+
...(f.derivation ? { rule: f.derivation.rule } : {}),
|
|
110
|
+
...(source ? { source } : {})
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** True when the implementation's props are fully enumerated, so an absent prop really is absent. */
|
|
114
|
+
propsComplete(c, framework) {
|
|
115
|
+
const impls = c.implementations.filter((i) => !framework || i.framework === framework);
|
|
116
|
+
if (impls.length === 0)
|
|
117
|
+
return false;
|
|
118
|
+
return impls.every((i) => i.extensions?.[i.framework]?.propsComplete === true);
|
|
119
|
+
}
|
|
120
|
+
conflictsFor(subject) {
|
|
121
|
+
return this.contract.conflicts.filter((c) => c.subject === subject);
|
|
122
|
+
}
|
|
123
|
+
/** Follows aliases to a literal. A cycle or a missing target yields no value rather than a guess. */
|
|
124
|
+
resolveToken(token, theme) {
|
|
125
|
+
const chain = [token.id];
|
|
126
|
+
let current = token;
|
|
127
|
+
for (let i = 0; i < 20 && current; i++) {
|
|
128
|
+
const form = theme
|
|
129
|
+
? (current.modes?.[theme] ?? current.value)
|
|
130
|
+
: current.value;
|
|
131
|
+
if (form.aliasOf === undefined)
|
|
132
|
+
return { value: form.value, chain };
|
|
133
|
+
const next = this.contract.tokens.find((t) => t.id === form.aliasOf);
|
|
134
|
+
if (!next)
|
|
135
|
+
return {
|
|
136
|
+
chain: [...chain, form.aliasOf],
|
|
137
|
+
note: `alias target ${form.aliasOf} is not in the contract`
|
|
138
|
+
};
|
|
139
|
+
if (chain.includes(next.id))
|
|
140
|
+
return { chain: [...chain, next.id], note: "alias cycle" };
|
|
141
|
+
chain.push(next.id);
|
|
142
|
+
current = next;
|
|
143
|
+
}
|
|
144
|
+
return { chain, note: "alias chain too long" };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export const sanitizeName = (text) => text.replace(/[^A-Za-z0-9 ._:/@-]/g, "").slice(0, 80);
|
|
148
|
+
//# sourceMappingURL=system.js.map
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { scanJsx, scanMarkup } from "./scan.js";
|
|
2
|
+
import { normalize } from "./system.js";
|
|
3
|
+
import { validateUsage } from "./validate-usage.js";
|
|
4
|
+
export const MAX_CODE_CHARS = 200_000;
|
|
5
|
+
export const MAX_FINDINGS = 200;
|
|
6
|
+
const NATIVE_EQUIVALENTS = {
|
|
7
|
+
button: ["button"],
|
|
8
|
+
input: ["input", "textfield", "textinput"],
|
|
9
|
+
select: ["select", "dropdown"],
|
|
10
|
+
textarea: ["textarea", "textfield"],
|
|
11
|
+
dialog: ["dialog", "modal"],
|
|
12
|
+
table: ["table", "datatable"],
|
|
13
|
+
a: ["link"],
|
|
14
|
+
img: ["image", "img"],
|
|
15
|
+
label: ["label"],
|
|
16
|
+
progress: ["progress", "progressbar"]
|
|
17
|
+
};
|
|
18
|
+
const HEX = /#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4})(?![0-9A-Za-z_-])/g;
|
|
19
|
+
const CSS_VAR = /var\(\s*(--[A-Za-z0-9_-]+)/g;
|
|
20
|
+
function expandHex(hex) {
|
|
21
|
+
const h = hex.slice(1).toLowerCase();
|
|
22
|
+
return `#${h.length <= 4 ? [...h].map((c) => c + c).join("") : h}`;
|
|
23
|
+
}
|
|
24
|
+
function tagIndex(ds) {
|
|
25
|
+
const map = new Map();
|
|
26
|
+
const add = (key, c) => {
|
|
27
|
+
if (key === "")
|
|
28
|
+
return;
|
|
29
|
+
const list = map.get(key) ?? [];
|
|
30
|
+
if (!list.includes(c))
|
|
31
|
+
list.push(c);
|
|
32
|
+
map.set(key, list);
|
|
33
|
+
};
|
|
34
|
+
for (const c of ds.contract.components) {
|
|
35
|
+
add(normalize(c.name), c);
|
|
36
|
+
for (const i of c.implementations) {
|
|
37
|
+
if (i.exportName)
|
|
38
|
+
add(normalize(i.exportName), c);
|
|
39
|
+
const sel = i.extensions?.[i.framework]?.selector;
|
|
40
|
+
if (typeof sel === "string")
|
|
41
|
+
add(normalize(/^[a-z][a-z0-9-]*/.exec(sel)?.[0] ?? ""), c);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return map;
|
|
45
|
+
}
|
|
46
|
+
function tokenColors(ds) {
|
|
47
|
+
const colors = new Map();
|
|
48
|
+
for (const t of ds.contract.tokens) {
|
|
49
|
+
const values = [
|
|
50
|
+
ds.resolveToken(t).value,
|
|
51
|
+
...Object.keys(t.modes ?? {}).map((m) => ds.resolveToken(t, m).value)
|
|
52
|
+
];
|
|
53
|
+
for (const v of values)
|
|
54
|
+
if (typeof v === "string" && /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(v.trim())) {
|
|
55
|
+
const key = expandHex(v.trim());
|
|
56
|
+
if (!colors.has(key))
|
|
57
|
+
colors.set(key, t.id);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return colors;
|
|
61
|
+
}
|
|
62
|
+
function cssVariableNames(ds) {
|
|
63
|
+
const names = new Set();
|
|
64
|
+
for (const t of ds.contract.tokens) {
|
|
65
|
+
const css = t.extensions?.css;
|
|
66
|
+
if (typeof css?.name === "string")
|
|
67
|
+
names.add(css.name);
|
|
68
|
+
names.add(t.name.startsWith("--") ? t.name : `--${t.name.replace(/\./g, "-")}`);
|
|
69
|
+
}
|
|
70
|
+
return names;
|
|
71
|
+
}
|
|
72
|
+
/** From the file name, else the contract's only framework, else React. */
|
|
73
|
+
export function defaultFramework(ds, filename) {
|
|
74
|
+
if (filename) {
|
|
75
|
+
if (/\.vue$/i.test(filename))
|
|
76
|
+
return "vue";
|
|
77
|
+
if (/\.component\.html$/i.test(filename))
|
|
78
|
+
return "angular";
|
|
79
|
+
if (/\.html?$/i.test(filename))
|
|
80
|
+
return "html";
|
|
81
|
+
if (/\.[cm]?[jt]sx?$/i.test(filename))
|
|
82
|
+
return "react";
|
|
83
|
+
}
|
|
84
|
+
const known = ds.contract.metadata.frameworks.filter((f) => f === "react" || f === "vue" || f === "angular");
|
|
85
|
+
return known.length === 1 ? known[0] : "react";
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Checks a code snippet against the contract without running it: uses of known components are validated
|
|
89
|
+
* with their literal props, literal colors and CSS variables are compared with the tokens. Dynamic values
|
|
90
|
+
* are counted, not guessed.
|
|
91
|
+
*/
|
|
92
|
+
export function validateCode(ds, input) {
|
|
93
|
+
const framework = input.framework ?? defaultFramework(ds, input.filename);
|
|
94
|
+
const findings = [];
|
|
95
|
+
const limitations = [
|
|
96
|
+
"Static check of the given snippet only: nothing is executed or imported.",
|
|
97
|
+
"Only literal prop values are verified; expressions and bindings are counted as unverified.",
|
|
98
|
+
"A component is recognized by name or selector. Components from other libraries are not checked."
|
|
99
|
+
];
|
|
100
|
+
const checked = new Set();
|
|
101
|
+
const unknown = new Set();
|
|
102
|
+
let unverified = 0;
|
|
103
|
+
if (input.code.length > MAX_CODE_CHARS) {
|
|
104
|
+
return {
|
|
105
|
+
valid: false,
|
|
106
|
+
framework,
|
|
107
|
+
summary: {
|
|
108
|
+
errors: 1,
|
|
109
|
+
warnings: 0,
|
|
110
|
+
info: 0,
|
|
111
|
+
componentsChecked: [],
|
|
112
|
+
elementsNotInContract: [],
|
|
113
|
+
unverifiedValues: 0
|
|
114
|
+
},
|
|
115
|
+
findings: [
|
|
116
|
+
{
|
|
117
|
+
code: "input-too-large",
|
|
118
|
+
severity: "error",
|
|
119
|
+
message: `The snippet is ${input.code.length} characters; the limit is ${MAX_CODE_CHARS}`,
|
|
120
|
+
basis: "discovered"
|
|
121
|
+
}
|
|
122
|
+
],
|
|
123
|
+
truncated: false,
|
|
124
|
+
limitations
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const index = tagIndex(ds);
|
|
128
|
+
const uses = framework === "react" ? scanJsx(input.code, input.filename) : scanMarkup(input.code);
|
|
129
|
+
for (const use of uses) {
|
|
130
|
+
const key = normalize(use.tag.replace(/^.*:/, ""));
|
|
131
|
+
// A lowercase tag without a hyphen is a native element in every framework (custom elements need a hyphen).
|
|
132
|
+
const nativeTag = /^[a-z][a-z0-9]*$/.test(use.tag);
|
|
133
|
+
const matches = nativeTag ? [] : (index.get(key) ?? []);
|
|
134
|
+
if (matches.length === 1) {
|
|
135
|
+
const result = validateUsage(ds, {
|
|
136
|
+
component: matches[0].id,
|
|
137
|
+
framework: framework === "html" ? undefined : framework,
|
|
138
|
+
props: use.attrs,
|
|
139
|
+
hasChildren: use.hasChildren,
|
|
140
|
+
hasSpread: use.hasSpread
|
|
141
|
+
});
|
|
142
|
+
checked.add(matches[0].id);
|
|
143
|
+
unverified += result.unverifiedValues;
|
|
144
|
+
for (const f of result.findings)
|
|
145
|
+
findings.push({ ...f, line: use.line });
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (matches.length > 1) {
|
|
149
|
+
findings.push({
|
|
150
|
+
code: "component-ambiguous",
|
|
151
|
+
severity: "info",
|
|
152
|
+
message: `<${use.tag}> matches ${matches.map((m) => m.id).join(", ")}; it was not checked`,
|
|
153
|
+
line: use.line,
|
|
154
|
+
basis: "discovered"
|
|
155
|
+
});
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const native = NATIVE_EQUIVALENTS[use.tag.toLowerCase()];
|
|
159
|
+
const equivalent = native
|
|
160
|
+
? ds.contract.components.find((c) => native.includes(normalize(c.name)))
|
|
161
|
+
: undefined;
|
|
162
|
+
if (equivalent)
|
|
163
|
+
findings.push({
|
|
164
|
+
code: "native-element-has-equivalent",
|
|
165
|
+
severity: "info",
|
|
166
|
+
message: `<${use.tag}> is a native element and the design system has ${equivalent.name}. This is a name match, not a rule`,
|
|
167
|
+
line: use.line,
|
|
168
|
+
basis: "heuristic",
|
|
169
|
+
confidence: 0.5
|
|
170
|
+
});
|
|
171
|
+
else if (/^[A-Z]/.test(use.tag) || use.tag.includes("-"))
|
|
172
|
+
unknown.add(use.tag);
|
|
173
|
+
}
|
|
174
|
+
const colors = tokenColors(ds);
|
|
175
|
+
if (colors.size > 0) {
|
|
176
|
+
const lines = input.code.split("\n");
|
|
177
|
+
lines.forEach((text, idx) => {
|
|
178
|
+
for (const m of text.matchAll(HEX)) {
|
|
179
|
+
const before = text.slice(Math.max(0, (m.index ?? 0) - 14), m.index ?? 0);
|
|
180
|
+
if (/(href|to|id|name)\s*=\s*["']?$/i.test(before) || /&$/.test(before))
|
|
181
|
+
continue;
|
|
182
|
+
const token = colors.get(expandHex(m[0]));
|
|
183
|
+
findings.push(token
|
|
184
|
+
? {
|
|
185
|
+
code: "color-matches-token",
|
|
186
|
+
severity: "info",
|
|
187
|
+
message: `${m[0]} equals the value of ${token}; reference the token instead`,
|
|
188
|
+
line: idx + 1,
|
|
189
|
+
basis: "discovered"
|
|
190
|
+
}
|
|
191
|
+
: {
|
|
192
|
+
code: "color-not-in-tokens",
|
|
193
|
+
severity: "warning",
|
|
194
|
+
message: `${m[0]} is a literal color that is not the value of any token`,
|
|
195
|
+
line: idx + 1,
|
|
196
|
+
basis: "discovered"
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
else
|
|
202
|
+
limitations.push("No token has a literal color value, so literal colors were not checked.");
|
|
203
|
+
if (ds.contract.tokens.length > 0) {
|
|
204
|
+
const known = cssVariableNames(ds);
|
|
205
|
+
input.code.split("\n").forEach((text, idx) => {
|
|
206
|
+
for (const m of text.matchAll(CSS_VAR)) {
|
|
207
|
+
const name = m[1];
|
|
208
|
+
if (!known.has(name))
|
|
209
|
+
findings.push({
|
|
210
|
+
code: "unknown-css-variable",
|
|
211
|
+
severity: "warning",
|
|
212
|
+
message: `${name} is not a token in the contract`,
|
|
213
|
+
line: idx + 1,
|
|
214
|
+
basis: "discovered"
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
else
|
|
220
|
+
limitations.push("The contract has no tokens, so CSS variables were not checked.");
|
|
221
|
+
findings.sort((a, b) => (a.line ?? 0) - (b.line ?? 0) || (a.code < b.code ? -1 : a.code > b.code ? 1 : 0));
|
|
222
|
+
const count = (s) => findings.filter((f) => f.severity === s).length;
|
|
223
|
+
return {
|
|
224
|
+
valid: count("error") === 0,
|
|
225
|
+
framework,
|
|
226
|
+
summary: {
|
|
227
|
+
errors: count("error"),
|
|
228
|
+
warnings: count("warning"),
|
|
229
|
+
info: count("info"),
|
|
230
|
+
componentsChecked: [...checked].sort(),
|
|
231
|
+
elementsNotInContract: [...unknown].sort().slice(0, 50),
|
|
232
|
+
unverifiedValues: unverified
|
|
233
|
+
},
|
|
234
|
+
findings: findings.slice(0, MAX_FINDINGS),
|
|
235
|
+
truncated: findings.length > MAX_FINDINGS,
|
|
236
|
+
limitations
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
//# sourceMappingURL=validate-code.js.map
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { normalize } from "./system.js";
|
|
2
|
+
export const isDynamic = (v) => typeof v === "object" &&
|
|
3
|
+
v !== null &&
|
|
4
|
+
!Array.isArray(v) &&
|
|
5
|
+
Object.keys(v).length === 1 &&
|
|
6
|
+
typeof v.expression === "string";
|
|
7
|
+
const PASSTHROUGH = /^(data-|aria-)|^(key|ref|class|className|style|id|slot|role|tabindex|title|hidden)$/i;
|
|
8
|
+
function sameValue(allowed, v) {
|
|
9
|
+
if (allowed === v)
|
|
10
|
+
return true;
|
|
11
|
+
return typeof v === "string" && typeof allowed !== "string" && String(allowed) === v;
|
|
12
|
+
}
|
|
13
|
+
export function validateUsage(ds, input) {
|
|
14
|
+
const findings = [];
|
|
15
|
+
const found = ds.findComponent(input.component);
|
|
16
|
+
if (!found.match) {
|
|
17
|
+
const names = found.candidates.map((c) => c.name);
|
|
18
|
+
findings.push({
|
|
19
|
+
code: found.candidates.length > 1 && names.some((n) => normalize(n) === normalize(input.component))
|
|
20
|
+
? "component-ambiguous"
|
|
21
|
+
: "component-unknown",
|
|
22
|
+
severity: "error",
|
|
23
|
+
message: `No single component "${input.component}" in the contract${names.length > 0 ? `. Candidates: ${names.join(", ")}` : ""}`,
|
|
24
|
+
basis: "discovered"
|
|
25
|
+
});
|
|
26
|
+
return { findings, unverifiedValues: 0 };
|
|
27
|
+
}
|
|
28
|
+
const c = found.match;
|
|
29
|
+
const framework = input.framework;
|
|
30
|
+
if (framework && !c.implementations.some((i) => i.framework === framework))
|
|
31
|
+
findings.push({
|
|
32
|
+
code: "framework-not-implemented",
|
|
33
|
+
severity: "error",
|
|
34
|
+
message: `${c.name} has no ${framework} implementation (it has: ${c.implementations.map((i) => i.framework).join(", ")})`,
|
|
35
|
+
basis: "discovered"
|
|
36
|
+
});
|
|
37
|
+
if (c.lifecycle === "deprecated")
|
|
38
|
+
findings.push({
|
|
39
|
+
code: "component-deprecated",
|
|
40
|
+
severity: "warning",
|
|
41
|
+
message: `${c.name} is deprecated`,
|
|
42
|
+
basis: ds.factView(c.facts, "lifecycle")?.status === "documented" ? "documented" : "discovered"
|
|
43
|
+
});
|
|
44
|
+
const complete = ds.propsComplete(c, framework);
|
|
45
|
+
const props = input.props ?? {};
|
|
46
|
+
let unverified = 0;
|
|
47
|
+
const basisOf = (path) => {
|
|
48
|
+
const s = ds.factView(c.facts, path)?.status;
|
|
49
|
+
return s === "documented" || s === "inferred" ? s : "discovered";
|
|
50
|
+
};
|
|
51
|
+
const where = (path) => {
|
|
52
|
+
const s = ds.factView(c.facts, path)?.source;
|
|
53
|
+
return s ? { source: s } : {};
|
|
54
|
+
};
|
|
55
|
+
const eventNames = new Set(c.events.map((e) => normalize(e.name)));
|
|
56
|
+
const slotNames = new Set(c.slots.map((s) => normalize(s.name)));
|
|
57
|
+
for (const [name, value] of Object.entries(props)) {
|
|
58
|
+
const prop = c.props.find((p) => p.name === name);
|
|
59
|
+
if (!prop) {
|
|
60
|
+
if (PASSTHROUGH.test(name) ||
|
|
61
|
+
eventNames.has(normalize(name.replace(/^on(?=[A-Z])/, ""))) ||
|
|
62
|
+
slotNames.has(normalize(name)))
|
|
63
|
+
continue;
|
|
64
|
+
findings.push(complete
|
|
65
|
+
? {
|
|
66
|
+
code: "unknown-prop",
|
|
67
|
+
severity: "warning",
|
|
68
|
+
message: `${c.name} does not declare a prop "${name}"`,
|
|
69
|
+
path: `props.${name}`,
|
|
70
|
+
basis: "discovered"
|
|
71
|
+
}
|
|
72
|
+
: {
|
|
73
|
+
code: "unverifiable-prop",
|
|
74
|
+
severity: "info",
|
|
75
|
+
message: `${c.name}'s prop list is incomplete, so "${name}" could not be verified`,
|
|
76
|
+
path: `props.${name}`,
|
|
77
|
+
basis: "discovered"
|
|
78
|
+
});
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const base = `props.${name}`;
|
|
82
|
+
if (prop.deprecated)
|
|
83
|
+
findings.push({
|
|
84
|
+
code: "prop-deprecated",
|
|
85
|
+
severity: "warning",
|
|
86
|
+
message: `${c.name}.${name} is deprecated${prop.deprecated.reason ? `: ${prop.deprecated.reason}` : ""}`,
|
|
87
|
+
path: base,
|
|
88
|
+
basis: basisOf(`${base}.deprecated`),
|
|
89
|
+
...where(`${base}.deprecated`)
|
|
90
|
+
});
|
|
91
|
+
if (isDynamic(value)) {
|
|
92
|
+
unverified++;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (prop.allowedValues &&
|
|
96
|
+
(typeof value === "string" || typeof value === "number" || typeof value === "boolean")) {
|
|
97
|
+
if (!prop.allowedValues.some((a) => sameValue(a, value)))
|
|
98
|
+
findings.push({
|
|
99
|
+
code: "prop-value-not-allowed",
|
|
100
|
+
severity: "error",
|
|
101
|
+
message: `${c.name}.${name} is ${JSON.stringify(value)}, which is not one of ${prop.allowedValues.map((a) => JSON.stringify(a)).join(", ")}`,
|
|
102
|
+
path: `${base}.allowedValues`,
|
|
103
|
+
basis: basisOf(`${base}.allowedValues`),
|
|
104
|
+
...where(`${base}.allowedValues`)
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
const kind = prop.type.kind;
|
|
109
|
+
const bad = (kind === "boolean" && typeof value !== "boolean") ||
|
|
110
|
+
(kind === "number" && typeof value !== "number") ||
|
|
111
|
+
(kind === "string" && typeof value !== "string");
|
|
112
|
+
if (bad)
|
|
113
|
+
findings.push({
|
|
114
|
+
code: "prop-type-mismatch",
|
|
115
|
+
severity: "warning",
|
|
116
|
+
message: `${c.name}.${name} expects ${kind} (${prop.type.text}) but got ${JSON.stringify(value)}`,
|
|
117
|
+
path: `${base}.type`,
|
|
118
|
+
basis: basisOf(`${base}.type.text`),
|
|
119
|
+
...where(`${base}.type.text`)
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!input.hasSpread)
|
|
124
|
+
for (const p of c.props) {
|
|
125
|
+
if (!p.required || p.name in props)
|
|
126
|
+
continue;
|
|
127
|
+
if (p.name === "children") {
|
|
128
|
+
if (input.hasChildren === false)
|
|
129
|
+
findings.push({
|
|
130
|
+
code: "required-prop-missing",
|
|
131
|
+
severity: "error",
|
|
132
|
+
message: `${c.name} requires children`,
|
|
133
|
+
path: "props.children",
|
|
134
|
+
basis: basisOf("props.children.required"),
|
|
135
|
+
...where("props.children.required")
|
|
136
|
+
});
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
findings.push({
|
|
140
|
+
code: "required-prop-missing",
|
|
141
|
+
severity: "error",
|
|
142
|
+
message: `${c.name} requires the prop "${p.name}"`,
|
|
143
|
+
path: `props.${p.name}`,
|
|
144
|
+
basis: basisOf(`props.${p.name}.required`),
|
|
145
|
+
...where(`props.${p.name}.required`)
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return { component: c.id, findings, unverifiedValues: unverified };
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=validate-usage.js.map
|