@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,513 @@
|
|
|
1
|
+
import { normalize } from "../contract-query/index.js";
|
|
2
|
+
import { colorFunctionLiterals, colorKey, expandHex, namedColorLiterals, spacingLiterals, tokenValueKeys, typographyLiterals } from "./literals.js";
|
|
3
|
+
import { evaluatePatternRules, patternRuleCode, patternRules } from "./pattern-rules.js";
|
|
4
|
+
import { activePolicies, evaluateUse, relationshipRules } from "./policy.js";
|
|
5
|
+
import { scanJsx, scanMarkup } from "./scan.js";
|
|
6
|
+
import { nearest } from "./text.js";
|
|
7
|
+
import { validateUsage } from "./validate-usage.js";
|
|
8
|
+
export const MAX_CODE_CHARS = 200_000;
|
|
9
|
+
export const MAX_FINDINGS = 200;
|
|
10
|
+
export const MAX_EVALUATIONS = 500;
|
|
11
|
+
const NATIVE_EQUIVALENTS = {
|
|
12
|
+
button: ["button"],
|
|
13
|
+
input: ["input", "textfield", "textinput"],
|
|
14
|
+
select: ["select", "dropdown"],
|
|
15
|
+
textarea: ["textarea", "textfield"],
|
|
16
|
+
dialog: ["dialog", "modal"],
|
|
17
|
+
table: ["table", "datatable"],
|
|
18
|
+
a: ["link"],
|
|
19
|
+
img: ["image", "img"],
|
|
20
|
+
label: ["label"],
|
|
21
|
+
progress: ["progress", "progressbar"]
|
|
22
|
+
};
|
|
23
|
+
const HEX = /#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4})(?![0-9A-Za-z_-])/g;
|
|
24
|
+
const CSS_VAR = /var\(\s*(--[A-Za-z0-9_-]+)/g;
|
|
25
|
+
function tagIndex(ds) {
|
|
26
|
+
const map = new Map();
|
|
27
|
+
const add = (key, c) => {
|
|
28
|
+
if (key === "")
|
|
29
|
+
return;
|
|
30
|
+
const list = map.get(key) ?? [];
|
|
31
|
+
if (!list.includes(c))
|
|
32
|
+
list.push(c);
|
|
33
|
+
map.set(key, list);
|
|
34
|
+
};
|
|
35
|
+
for (const c of ds.contract.components) {
|
|
36
|
+
add(normalize(c.name), c);
|
|
37
|
+
for (const i of c.implementations) {
|
|
38
|
+
if (i.exportName)
|
|
39
|
+
add(normalize(i.exportName), c);
|
|
40
|
+
const sel = i.extensions?.[i.framework]?.selector;
|
|
41
|
+
if (typeof sel === "string")
|
|
42
|
+
add(normalize(/^[a-z][a-z0-9-]*/.exec(sel)?.[0] ?? ""), c);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return map;
|
|
46
|
+
}
|
|
47
|
+
function designSystemModules(ds) {
|
|
48
|
+
const modules = new Set();
|
|
49
|
+
for (const c of ds.contract.components)
|
|
50
|
+
for (const i of c.implementations) {
|
|
51
|
+
if (i.packageName)
|
|
52
|
+
modules.add(i.packageName);
|
|
53
|
+
if (i.importPath)
|
|
54
|
+
modules.add(i.importPath);
|
|
55
|
+
}
|
|
56
|
+
return [...modules];
|
|
57
|
+
}
|
|
58
|
+
function cssVariables(ds) {
|
|
59
|
+
const map = new Map();
|
|
60
|
+
for (const t of ds.contract.tokens) {
|
|
61
|
+
const css = t.extensions?.css;
|
|
62
|
+
const names = new Set();
|
|
63
|
+
if (typeof css?.name === "string")
|
|
64
|
+
names.add(css.name);
|
|
65
|
+
names.add(t.name.startsWith("--") ? t.name : `--${t.name.replace(/\./g, "-")}`);
|
|
66
|
+
for (const n of names)
|
|
67
|
+
map.set(n, { token: t, cssName: n });
|
|
68
|
+
}
|
|
69
|
+
return map;
|
|
70
|
+
}
|
|
71
|
+
/** Every literal value a token can take in the default and in each theme. */
|
|
72
|
+
function tokenLiterals(ds, t) {
|
|
73
|
+
return [ds.resolveToken(t).value, ...Object.keys(t.modes ?? {}).map((m) => ds.resolveToken(t, m).value)];
|
|
74
|
+
}
|
|
75
|
+
function tokenColors(ds) {
|
|
76
|
+
const colors = new Map();
|
|
77
|
+
for (const t of ds.contract.tokens)
|
|
78
|
+
for (const v of tokenLiterals(ds, t)) {
|
|
79
|
+
const key = typeof v === "string" ? colorKey(v) : undefined;
|
|
80
|
+
if (key && !colors.has(key))
|
|
81
|
+
colors.set(key, t.id);
|
|
82
|
+
}
|
|
83
|
+
return colors;
|
|
84
|
+
}
|
|
85
|
+
/** Comparable keys of the tokens of one category, to the first token that has the value. */
|
|
86
|
+
function tokenKeysByCategory(ds, category) {
|
|
87
|
+
const keys = new Map();
|
|
88
|
+
for (const t of ds.contract.tokens) {
|
|
89
|
+
if (t.category !== category)
|
|
90
|
+
continue;
|
|
91
|
+
for (const v of tokenLiterals(ds, t))
|
|
92
|
+
for (const k of tokenValueKeys(v))
|
|
93
|
+
if (!keys.has(k))
|
|
94
|
+
keys.set(k, t.id);
|
|
95
|
+
}
|
|
96
|
+
return keys;
|
|
97
|
+
}
|
|
98
|
+
function tokenSource(ds, id) {
|
|
99
|
+
const t = ds.contract.tokens.find((x) => x.id === id);
|
|
100
|
+
const source = t ? (ds.factView(t.facts, "value.value") ?? ds.factView(t.facts, "name"))?.source : undefined;
|
|
101
|
+
return source ? { source } : {};
|
|
102
|
+
}
|
|
103
|
+
/** From the file name, else the contract's only framework, else React. */
|
|
104
|
+
export function defaultFramework(ds, filename) {
|
|
105
|
+
if (filename) {
|
|
106
|
+
if (/\.vue$/i.test(filename))
|
|
107
|
+
return "vue";
|
|
108
|
+
if (/\.component\.html$/i.test(filename))
|
|
109
|
+
return "angular";
|
|
110
|
+
if (/\.html?$/i.test(filename))
|
|
111
|
+
return "html";
|
|
112
|
+
if (/\.[cm]?[jt]sx?$/i.test(filename))
|
|
113
|
+
return "react";
|
|
114
|
+
}
|
|
115
|
+
const known = ds.contract.metadata.frameworks.filter((f) => f === "react" || f === "vue" || f === "angular");
|
|
116
|
+
return known.length === 1 ? known[0] : "react";
|
|
117
|
+
}
|
|
118
|
+
/** What an element is, for deciding whether a scope can be trusted to be complete. */
|
|
119
|
+
function kindOf(r, use) {
|
|
120
|
+
if (r.kind === "component")
|
|
121
|
+
return "component";
|
|
122
|
+
if (r.kind === "ambiguous")
|
|
123
|
+
return "ambiguous";
|
|
124
|
+
if (r.kind === "unknown-ds")
|
|
125
|
+
return "unknown";
|
|
126
|
+
if (r.kind === "foreign")
|
|
127
|
+
return "foreign";
|
|
128
|
+
return /^[a-z][a-z0-9]*$/.test(use.tag) ? "native" : "unknown";
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The elements of a React file with each one resolved to a design-system component where the contract says it is
|
|
132
|
+
* one. Reads syntax only and reports nothing; `validateCode` and pattern discovery both build on it.
|
|
133
|
+
*/
|
|
134
|
+
export function buildTree(ds, code, filename) {
|
|
135
|
+
const index = tagIndex(ds);
|
|
136
|
+
const modules = designSystemModules(ds);
|
|
137
|
+
const isDs = (m) => modules.some((p) => m === p || m.startsWith(`${p}/`));
|
|
138
|
+
const { uses, imports } = scanJsx(code, filename);
|
|
139
|
+
const components = [];
|
|
140
|
+
const kinds = [];
|
|
141
|
+
for (const use of uses) {
|
|
142
|
+
const r = resolveUse(use, index, imports, isDs);
|
|
143
|
+
components.push(r.kind === "component" ? r.component : undefined);
|
|
144
|
+
kinds.push(kindOf(r, use));
|
|
145
|
+
}
|
|
146
|
+
return { uses, components, kinds };
|
|
147
|
+
}
|
|
148
|
+
function resolveUse(use, index, imports, isDs) {
|
|
149
|
+
const parts = use.tag.split(".");
|
|
150
|
+
const base = parts[0];
|
|
151
|
+
const member = parts.slice(1);
|
|
152
|
+
const imp = imports.get(base);
|
|
153
|
+
const pick = (key) => {
|
|
154
|
+
const found = index.get(key) ?? [];
|
|
155
|
+
return found.length === 1
|
|
156
|
+
? { kind: "component", component: found[0] }
|
|
157
|
+
: found.length > 1
|
|
158
|
+
? { kind: "ambiguous", matches: found }
|
|
159
|
+
: { kind: "none" };
|
|
160
|
+
};
|
|
161
|
+
if (imp) {
|
|
162
|
+
if (!isDs(imp.module))
|
|
163
|
+
return { kind: "foreign" };
|
|
164
|
+
const importedName = imp.imported === "*" ? (member[0] ?? "") : imp.imported;
|
|
165
|
+
const rest = imp.imported === "*" ? member.slice(1) : member;
|
|
166
|
+
if (importedName === "" || importedName === "default")
|
|
167
|
+
return pick(normalize(use.tag));
|
|
168
|
+
let r = pick(normalize(importedName + rest.join("")));
|
|
169
|
+
if (r.kind === "ambiguous") {
|
|
170
|
+
// Several packages may export the same name: the import tells which one this file means.
|
|
171
|
+
const fromModule = r.matches.filter((c) => c.implementations.some((i) => [i.packageName, i.importPath].some((m) => m !== undefined && (imp.module === m || imp.module.startsWith(`${m}/`)))));
|
|
172
|
+
if (fromModule.length === 1)
|
|
173
|
+
r = { kind: "component", component: fromModule[0] };
|
|
174
|
+
}
|
|
175
|
+
if (r.kind === "none" && rest.length === 0 && /^[A-Z]/.test(importedName))
|
|
176
|
+
return { kind: "unknown-ds", name: importedName };
|
|
177
|
+
return r;
|
|
178
|
+
}
|
|
179
|
+
// A lowercase tag without a hyphen is a native element in every framework (custom elements need a hyphen).
|
|
180
|
+
if (/^[a-z][a-z0-9]*$/.test(use.tag))
|
|
181
|
+
return { kind: "none" };
|
|
182
|
+
return pick(normalize(use.tag.replace(/^.*:/, "")));
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Checks a code snippet against the contract without running it: uses of known components are validated
|
|
186
|
+
* with their literal props, approved policies are applied, and literal colors and CSS variables are compared
|
|
187
|
+
* with the tokens. Dynamic values are counted, not guessed.
|
|
188
|
+
*/
|
|
189
|
+
function tally(evaluations) {
|
|
190
|
+
const out = { pass: 0, fail: 0, warning: 0, notVerifiable: 0, skipped: 0 };
|
|
191
|
+
for (const e of evaluations) {
|
|
192
|
+
const key = e.status === "not-verifiable" ? "notVerifiable" : e.status;
|
|
193
|
+
out[key]++;
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
export function validateCode(ds, input, options = {}) {
|
|
198
|
+
const framework = input.framework ?? defaultFramework(ds, input.filename);
|
|
199
|
+
const findings = [];
|
|
200
|
+
const limitations = [
|
|
201
|
+
"Static check of the given snippet only: nothing is executed or imported.",
|
|
202
|
+
"Only literal prop values are verified; expressions and bindings are counted as unverified.",
|
|
203
|
+
"A component is recognized by its import from a design-system package (React) or by name or selector. Components from other libraries are not checked."
|
|
204
|
+
];
|
|
205
|
+
const checked = new Set();
|
|
206
|
+
const unknown = new Set();
|
|
207
|
+
let unverified = 0;
|
|
208
|
+
if (input.code.length > MAX_CODE_CHARS) {
|
|
209
|
+
return {
|
|
210
|
+
valid: false,
|
|
211
|
+
framework,
|
|
212
|
+
summary: {
|
|
213
|
+
errors: 1,
|
|
214
|
+
warnings: 0,
|
|
215
|
+
info: 0,
|
|
216
|
+
componentsChecked: [],
|
|
217
|
+
elementsNotInContract: [],
|
|
218
|
+
unverifiedValues: 0,
|
|
219
|
+
patternRules: { pass: 0, fail: 0, warning: 0, notVerifiable: 0, skipped: 0 }
|
|
220
|
+
},
|
|
221
|
+
evaluations: [],
|
|
222
|
+
findings: [
|
|
223
|
+
{
|
|
224
|
+
code: "input-too-large",
|
|
225
|
+
severity: "error",
|
|
226
|
+
message: `The snippet is ${input.code.length} characters; the limit is ${MAX_CODE_CHARS}`,
|
|
227
|
+
basis: "discovered",
|
|
228
|
+
suggestion: "Validate smaller files."
|
|
229
|
+
}
|
|
230
|
+
],
|
|
231
|
+
truncated: false,
|
|
232
|
+
limitations
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const index = tagIndex(ds);
|
|
236
|
+
const modules = designSystemModules(ds);
|
|
237
|
+
const isDs = (m) => modules.some((p) => m === p || m.startsWith(`${p}/`));
|
|
238
|
+
const jsx = framework === "react" ? scanJsx(input.code, input.filename) : undefined;
|
|
239
|
+
const uses = jsx ? jsx.uses : scanMarkup(input.code);
|
|
240
|
+
const imports = jsx?.imports ?? new Map();
|
|
241
|
+
const resolved = [];
|
|
242
|
+
const kinds = [];
|
|
243
|
+
const at = (u) => ({ line: u.line, column: u.column });
|
|
244
|
+
uses.forEach((use) => {
|
|
245
|
+
const r = resolveUse(use, index, imports, isDs);
|
|
246
|
+
kinds.push(kindOf(r, use));
|
|
247
|
+
if (r.kind === "component") {
|
|
248
|
+
resolved.push(r.component);
|
|
249
|
+
const result = validateUsage(ds, {
|
|
250
|
+
component: r.component.id,
|
|
251
|
+
framework: framework === "html" ? undefined : framework,
|
|
252
|
+
props: use.attrs,
|
|
253
|
+
hasChildren: use.hasChildren,
|
|
254
|
+
hasSpread: use.hasSpread
|
|
255
|
+
});
|
|
256
|
+
checked.add(r.component.id);
|
|
257
|
+
unverified += result.unverifiedValues;
|
|
258
|
+
for (const f of result.findings)
|
|
259
|
+
findings.push({ ...f, ...at(use) });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
resolved.push(undefined);
|
|
263
|
+
if (r.kind === "ambiguous") {
|
|
264
|
+
findings.push({
|
|
265
|
+
code: "component-ambiguous",
|
|
266
|
+
severity: "info",
|
|
267
|
+
message: `<${use.tag}> matches ${r.matches.map((m) => m.id).join(", ")}; it was not checked`,
|
|
268
|
+
...at(use),
|
|
269
|
+
basis: "discovered"
|
|
270
|
+
});
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (r.kind === "unknown-ds") {
|
|
274
|
+
const close = nearest(r.name, ds.contract.components.map((c) => c.name));
|
|
275
|
+
findings.push({
|
|
276
|
+
code: "component-unknown",
|
|
277
|
+
severity: "error",
|
|
278
|
+
message: `<${use.tag}> is imported from the design system, but the contract has no component "${r.name}"${close.length > 0 ? `. Candidates: ${close.join(", ")}` : ""}`,
|
|
279
|
+
...at(use),
|
|
280
|
+
basis: "discovered",
|
|
281
|
+
suggestion: close.length > 0
|
|
282
|
+
? `Did you mean ${close.join(" or ")}?`
|
|
283
|
+
: "Check the export name, or find a component with search_design_system."
|
|
284
|
+
});
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (r.kind === "foreign")
|
|
288
|
+
return;
|
|
289
|
+
const equivalents = NATIVE_EQUIVALENTS[use.tag.toLowerCase()];
|
|
290
|
+
const equivalent = equivalents
|
|
291
|
+
? ds.contract.components.find((c) => equivalents.includes(normalize(c.name)))
|
|
292
|
+
: undefined;
|
|
293
|
+
if (equivalent)
|
|
294
|
+
findings.push({
|
|
295
|
+
code: "native-element-has-equivalent",
|
|
296
|
+
severity: "info",
|
|
297
|
+
subject: equivalent.id,
|
|
298
|
+
message: `<${use.tag}> is a native element and the design system has ${equivalent.name}. This is a name match, not a rule`,
|
|
299
|
+
...at(use),
|
|
300
|
+
basis: "heuristic",
|
|
301
|
+
confidence: 0.5,
|
|
302
|
+
suggestion: `Consider ${equivalent.name} if it covers this use.`
|
|
303
|
+
});
|
|
304
|
+
else if (/^[A-Z]/.test(use.tag) || use.tag.includes("-"))
|
|
305
|
+
unknown.add(use.tag);
|
|
306
|
+
});
|
|
307
|
+
const { active, problems } = activePolicies(ds.contract);
|
|
308
|
+
if (problems.length > 0)
|
|
309
|
+
limitations.push(...problems.map((p) => `Not enforced: ${p}.`));
|
|
310
|
+
const rules = [...active, ...relationshipRules(ds)];
|
|
311
|
+
const ctx = { uses, resolved };
|
|
312
|
+
uses.forEach((use, i) => {
|
|
313
|
+
for (const f of evaluateUse(rules, ctx, i))
|
|
314
|
+
findings.push({ ...f, ...at(use) });
|
|
315
|
+
});
|
|
316
|
+
// Structural pattern rules, from approved policies whose constraints are of a pattern kind.
|
|
317
|
+
const off = options.rules ?? {};
|
|
318
|
+
const structural = patternRules(ds);
|
|
319
|
+
if (structural.problems.length > 0)
|
|
320
|
+
limitations.push(...structural.problems.map((p) => `Not enforced: ${p}.`));
|
|
321
|
+
const evaluation = evaluatePatternRules({
|
|
322
|
+
ds,
|
|
323
|
+
rules: structural.rules.filter((r) => off[patternRuleCode(r.kind)] !== "off"),
|
|
324
|
+
tree: { uses, components: resolved, kinds },
|
|
325
|
+
framework,
|
|
326
|
+
filename: input.filename,
|
|
327
|
+
code: input.code
|
|
328
|
+
});
|
|
329
|
+
findings.push(...evaluation.findings);
|
|
330
|
+
if (evaluation.evaluations.length > 0)
|
|
331
|
+
limitations.push("Pattern rules are static: they read the JSX of this file only. A check that depends on content from another file, or on children this file does not show, is reported as not verifiable, never as a pass.");
|
|
332
|
+
const colors = tokenColors(ds);
|
|
333
|
+
const lines = input.code.split("\n");
|
|
334
|
+
if (colors.size > 0) {
|
|
335
|
+
lines.forEach((text, idx) => {
|
|
336
|
+
const found = [];
|
|
337
|
+
for (const m of text.matchAll(HEX)) {
|
|
338
|
+
const before = text.slice(Math.max(0, (m.index ?? 0) - 14), m.index ?? 0);
|
|
339
|
+
if (/(href|to|id|name)\s*=\s*["']?$/i.test(before) || /&$/.test(before))
|
|
340
|
+
continue;
|
|
341
|
+
found.push({ text: m[0], keys: [expandHex(m[0])], column: (m.index ?? 0) + 1 });
|
|
342
|
+
}
|
|
343
|
+
found.push(...colorFunctionLiterals(text), ...namedColorLiterals(text));
|
|
344
|
+
for (const f of found) {
|
|
345
|
+
const token = colors.get(f.keys[0]);
|
|
346
|
+
const where = { line: idx + 1, column: f.column };
|
|
347
|
+
findings.push(token
|
|
348
|
+
? {
|
|
349
|
+
code: "color-matches-token",
|
|
350
|
+
severity: "info",
|
|
351
|
+
subject: token,
|
|
352
|
+
message: `${f.text} equals the value of ${token}`,
|
|
353
|
+
...where,
|
|
354
|
+
basis: "discovered",
|
|
355
|
+
...tokenSource(ds, token),
|
|
356
|
+
suggestion: `Reference ${token} instead of the literal.`
|
|
357
|
+
}
|
|
358
|
+
: {
|
|
359
|
+
code: "color-not-in-tokens",
|
|
360
|
+
severity: "warning",
|
|
361
|
+
message: `${f.text} is a literal color that is not the value of any token`,
|
|
362
|
+
...where,
|
|
363
|
+
basis: "discovered",
|
|
364
|
+
suggestion: "Use a color token from get_design_tokens, or add this color to the design system."
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
else
|
|
370
|
+
limitations.push("No token has a literal color value, so literal colors were not checked.");
|
|
371
|
+
const measured = [
|
|
372
|
+
{ name: "spacing", category: "spacing", scan: spacingLiterals },
|
|
373
|
+
{ name: "typography", category: "typography", scan: typographyLiterals }
|
|
374
|
+
];
|
|
375
|
+
for (const m of measured) {
|
|
376
|
+
const keys = tokenKeysByCategory(ds, m.category);
|
|
377
|
+
if (keys.size === 0) {
|
|
378
|
+
limitations.push(`No ${m.name} token has a literal value, so literal ${m.name} was not checked.`);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
lines.forEach((text, idx) => {
|
|
382
|
+
for (const f of m.scan(text)) {
|
|
383
|
+
const token = f.keys.map((k) => keys.get(k)).find((t) => t !== undefined);
|
|
384
|
+
const where = { line: idx + 1, column: f.column };
|
|
385
|
+
findings.push(token
|
|
386
|
+
? {
|
|
387
|
+
code: `${m.name}-matches-token`,
|
|
388
|
+
severity: "info",
|
|
389
|
+
subject: token,
|
|
390
|
+
message: `${f.property}: ${f.text} equals the value of ${token}`,
|
|
391
|
+
...where,
|
|
392
|
+
basis: "discovered",
|
|
393
|
+
...tokenSource(ds, token),
|
|
394
|
+
suggestion: `Reference ${token} instead of the literal.`
|
|
395
|
+
}
|
|
396
|
+
: {
|
|
397
|
+
code: `${m.name}-not-in-tokens`,
|
|
398
|
+
severity: "warning",
|
|
399
|
+
message: `${f.property}: ${f.text} is a literal ${m.name} value that is not the value of any ${m.name} token`,
|
|
400
|
+
...where,
|
|
401
|
+
basis: "discovered",
|
|
402
|
+
suggestion: `Use a ${m.name} token from get_design_tokens, or add this value to the design system.`
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
if (ds.contract.tokens.length > 0) {
|
|
408
|
+
const vars = cssVariables(ds);
|
|
409
|
+
// A custom property declared in this very file is local to it, not a missing token.
|
|
410
|
+
const declared = new Set([...input.code.matchAll(/(--[A-Za-z0-9_-]+)\s*:/g)].map((m) => m[1]));
|
|
411
|
+
const forbidden = rules.filter((r) => r.rule.kind === "forbid-token");
|
|
412
|
+
lines.forEach((text, idx) => {
|
|
413
|
+
for (const m of text.matchAll(CSS_VAR)) {
|
|
414
|
+
const name = m[1];
|
|
415
|
+
const where = { line: idx + 1, column: (m.index ?? 0) + 1 };
|
|
416
|
+
const known = vars.get(name);
|
|
417
|
+
const banned = forbidden.find((p) => p.rule.kind === "forbid-token" &&
|
|
418
|
+
p.rule.tokens.some((t) => normalize(t.replace(/^token:/, "").replace(/^--/, "")) === normalize(name.replace(/^--/, ""))));
|
|
419
|
+
if (banned && banned.rule.kind === "forbid-token")
|
|
420
|
+
findings.push({
|
|
421
|
+
code: "token-forbidden",
|
|
422
|
+
severity: banned.severity,
|
|
423
|
+
subject: known?.token.id ?? name,
|
|
424
|
+
message: `${name} must not be used (policy ${banned.policyId})`,
|
|
425
|
+
...where,
|
|
426
|
+
basis: "approved",
|
|
427
|
+
policy: banned.policyId,
|
|
428
|
+
...(banned.source ? { source: banned.source } : {}),
|
|
429
|
+
suggestion: banned.rule.replacedBy
|
|
430
|
+
? `Use ${banned.rule.replacedBy} instead.`
|
|
431
|
+
: "Use an approved token."
|
|
432
|
+
});
|
|
433
|
+
if (!known && declared.has(name))
|
|
434
|
+
continue;
|
|
435
|
+
if (!known) {
|
|
436
|
+
const close = nearest(name, [...vars.keys()]);
|
|
437
|
+
findings.push({
|
|
438
|
+
code: "unknown-css-variable",
|
|
439
|
+
severity: "warning",
|
|
440
|
+
message: `${name} is not a token in the contract`,
|
|
441
|
+
...where,
|
|
442
|
+
basis: "discovered",
|
|
443
|
+
suggestion: close.length > 0 ? `Did you mean ${close.join(" or ")}?` : "Use a token from get_design_tokens."
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
else if (known.token.deprecated) {
|
|
447
|
+
const replacement = known.token.deprecated.replacedBy
|
|
448
|
+
? vars.get(`--${known.token.deprecated.replacedBy.slice(6).replace(/\./g, "-")}`)?.cssName
|
|
449
|
+
: undefined;
|
|
450
|
+
findings.push({
|
|
451
|
+
code: "token-deprecated",
|
|
452
|
+
severity: "warning",
|
|
453
|
+
subject: known.token.id,
|
|
454
|
+
message: `${name} is deprecated${known.token.deprecated.reason ? `: ${known.token.deprecated.reason}` : ""}`,
|
|
455
|
+
...where,
|
|
456
|
+
basis: ds.factView(known.token.facts, "deprecated")?.status === "documented"
|
|
457
|
+
? "documented"
|
|
458
|
+
: "discovered",
|
|
459
|
+
suggestion: replacement ? `Use ${replacement}.` : "Migrate to the token that replaces it."
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
else
|
|
466
|
+
limitations.push("The contract has no tokens, so CSS variables were not checked.");
|
|
467
|
+
const overrides = options.rules ?? {};
|
|
468
|
+
const final = [];
|
|
469
|
+
for (const f of findings) {
|
|
470
|
+
const o = overrides[f.code];
|
|
471
|
+
if (o === "off")
|
|
472
|
+
continue;
|
|
473
|
+
final.push({
|
|
474
|
+
...f,
|
|
475
|
+
...(o ? { severity: o } : {}),
|
|
476
|
+
...(input.filename ? { file: input.filename } : {})
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
// Helper findings repeat for every use of the same element; say each once per file.
|
|
480
|
+
const HELPERS = new Set(["component-ambiguous", "native-element-has-equivalent", "unverifiable-prop"]);
|
|
481
|
+
const seen = new Set();
|
|
482
|
+
const deduped = final.filter((f) => {
|
|
483
|
+
if (!HELPERS.has(f.code))
|
|
484
|
+
return true;
|
|
485
|
+
const key = `${f.code}\u0000${f.message}`;
|
|
486
|
+
if (seen.has(key))
|
|
487
|
+
return false;
|
|
488
|
+
seen.add(key);
|
|
489
|
+
return true;
|
|
490
|
+
});
|
|
491
|
+
const sorted = deduped.sort((a, b) => (a.line ?? 0) - (b.line ?? 0) ||
|
|
492
|
+
(a.column ?? 0) - (b.column ?? 0) ||
|
|
493
|
+
(a.code < b.code ? -1 : a.code > b.code ? 1 : 0));
|
|
494
|
+
const count = (s) => sorted.filter((f) => f.severity === s).length;
|
|
495
|
+
return {
|
|
496
|
+
valid: count("error") === 0,
|
|
497
|
+
framework,
|
|
498
|
+
summary: {
|
|
499
|
+
errors: count("error"),
|
|
500
|
+
warnings: count("warning"),
|
|
501
|
+
info: count("info"),
|
|
502
|
+
componentsChecked: [...checked].sort(),
|
|
503
|
+
elementsNotInContract: [...unknown].sort().slice(0, 50),
|
|
504
|
+
unverifiedValues: unverified,
|
|
505
|
+
patternRules: tally(evaluation.evaluations)
|
|
506
|
+
},
|
|
507
|
+
evaluations: evaluation.evaluations.slice(0, MAX_EVALUATIONS),
|
|
508
|
+
findings: sorted.slice(0, MAX_FINDINGS),
|
|
509
|
+
truncated: sorted.length > MAX_FINDINGS,
|
|
510
|
+
limitations
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
//# sourceMappingURL=validate-code.js.map
|