@usefragments/core 1.0.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 +21 -0
- package/dist/chunk-243QYRUF.js +2145 -0
- package/dist/chunk-243QYRUF.js.map +1 -0
- package/dist/chunk-3LLRNCPX.js +111 -0
- package/dist/chunk-3LLRNCPX.js.map +1 -0
- package/dist/chunk-EIYNNS77.js +48 -0
- package/dist/chunk-EIYNNS77.js.map +1 -0
- package/dist/chunk-JNBFJ34I.js +326 -0
- package/dist/chunk-JNBFJ34I.js.map +1 -0
- package/dist/chunk-R3X7UBHU.js +228 -0
- package/dist/chunk-R3X7UBHU.js.map +1 -0
- package/dist/chunk-SH4KPIYH.js +393 -0
- package/dist/chunk-SH4KPIYH.js.map +1 -0
- package/dist/chunk-V4VQB57N.js +56 -0
- package/dist/chunk-V4VQB57N.js.map +1 -0
- package/dist/chunk-X34IA4LR.js +319 -0
- package/dist/chunk-X34IA4LR.js.map +1 -0
- package/dist/chunk-YF65VYRY.js +493 -0
- package/dist/chunk-YF65VYRY.js.map +1 -0
- package/dist/codes/index.d.ts +40 -0
- package/dist/codes/index.js +23 -0
- package/dist/codes/index.js.map +1 -0
- package/dist/compiled-types/index.d.ts +5 -0
- package/dist/compiled-types/index.js +1 -0
- package/dist/compiled-types/index.js.map +1 -0
- package/dist/generate/index.d.ts +47 -0
- package/dist/generate/index.js +11 -0
- package/dist/generate/index.js.map +1 -0
- package/dist/governance-BOa3KyiJ.d.ts +3279 -0
- package/dist/graph/index.d.ts +66 -0
- package/dist/graph/index.js +17 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/index.d.ts +11388 -0
- package/dist/index.js +8711 -0
- package/dist/index.js.map +1 -0
- package/dist/package-identity.d.ts +42 -0
- package/dist/package-identity.js +23 -0
- package/dist/package-identity.js.map +1 -0
- package/dist/preview/index.d.ts +484 -0
- package/dist/preview/index.js +1129 -0
- package/dist/preview/index.js.map +1 -0
- package/dist/react-types.d.ts +22 -0
- package/dist/react-types.js +1 -0
- package/dist/react-types.js.map +1 -0
- package/dist/registry.d.ts +2367 -0
- package/dist/registry.js +62 -0
- package/dist/registry.js.map +1 -0
- package/dist/schemas/index.d.ts +2584 -0
- package/dist/schemas/index.js +55 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/severity.d.ts +18 -0
- package/dist/severity.js +27 -0
- package/dist/severity.js.map +1 -0
- package/dist/test-utils.d.ts +14 -0
- package/dist/test-utils.js +63 -0
- package/dist/test-utils.js.map +1 -0
- package/dist/topology/index.d.ts +48 -0
- package/dist/topology/index.js +7 -0
- package/dist/topology/index.js.map +1 -0
- package/dist/types-xJ2xyp_G.d.ts +119 -0
- package/package.json +120 -0
- package/src/__tests__/contract-parser.test.ts +614 -0
- package/src/__tests__/dtcg-generators.test.ts +439 -0
- package/src/__tests__/dtcg-parser.test.ts +884 -0
- package/src/__tests__/preview-runtime.test.tsx +111 -0
- package/src/__tests__/schema.test.ts +31 -0
- package/src/__tests__/severity.test.ts +34 -0
- package/src/__tests__/style-comparison.test.ts +662 -0
- package/src/__tests__/tokens-compat.test.ts +51 -0
- package/src/agent-format.test.ts +105 -0
- package/src/agent-format.ts +141 -0
- package/src/bundle.ts +149 -0
- package/src/codes/__tests__/codes.test.ts +73 -0
- package/src/codes/codes.ts +463 -0
- package/src/codes/index.ts +7 -0
- package/src/codes/source-finding-bridge.ts +113 -0
- package/src/compiled-types/index.ts +293 -0
- package/src/component-contract.ts +231 -0
- package/src/component-discovery.ts +80 -0
- package/src/composition.test.ts +262 -0
- package/src/composition.ts +318 -0
- package/src/config-paths.test.ts +78 -0
- package/src/config-paths.ts +47 -0
- package/src/config.ts +18 -0
- package/src/conform-prove.test.ts +274 -0
- package/src/conform-prove.ts +326 -0
- package/src/conform.ts +160 -0
- package/src/constants.ts +114 -0
- package/src/context.ts +2 -0
- package/src/contract/fixture.ts +110 -0
- package/src/contract/hash.test.ts +86 -0
- package/src/contract/hash.ts +195 -0
- package/src/contract/index.ts +43 -0
- package/src/contract/preimage.test.ts +254 -0
- package/src/contract/preimage.ts +365 -0
- package/src/contract/stamp.test.ts +100 -0
- package/src/contract/stamp.ts +105 -0
- package/src/contract-parser.ts +141 -0
- package/src/defineFragment.test.ts +257 -0
- package/src/defineFragment.ts +293 -0
- package/src/dtcg-generators.ts +324 -0
- package/src/dtcg-parser.ts +1 -0
- package/src/dtcg.ts +237 -0
- package/src/facts/__snapshots__/facts.test.ts.snap +124 -0
- package/src/facts/builders.ts +835 -0
- package/src/facts/compile.ts +422 -0
- package/src/facts/fact-index.ts +394 -0
- package/src/facts/facts.test.ts +494 -0
- package/src/facts/ids.ts +125 -0
- package/src/facts/index.ts +119 -0
- package/src/facts/types.ts +614 -0
- package/src/figma.ts +263 -0
- package/src/fragment-types.ts +214 -0
- package/src/generate/generate.test.ts +594 -0
- package/src/generate/index.ts +438 -0
- package/src/governance-integrity.test.ts +186 -0
- package/src/governance-integrity.ts +282 -0
- package/src/governance.test.ts +300 -0
- package/src/governance.ts +605 -0
- package/src/governance.type-test.ts +75 -0
- package/src/graph/engine.ts +446 -0
- package/src/graph/graph.test.ts +746 -0
- package/src/graph/index.ts +32 -0
- package/src/graph/serialization.ts +55 -0
- package/src/graph/types.ts +178 -0
- package/src/index.ts +891 -0
- package/src/local-canonical.ts +102 -0
- package/src/mcp-snapshot.ts +222 -0
- package/src/package-identity-match.ts +33 -0
- package/src/package-identity.generated.ts +233 -0
- package/src/package-identity.test.ts +99 -0
- package/src/package-identity.ts +157 -0
- package/src/performance-presets.ts +142 -0
- package/src/preview/comparison-artifact.test.ts +228 -0
- package/src/preview/comparison-artifact.ts +181 -0
- package/src/preview/comparison-context.ts +93 -0
- package/src/preview/comparison-evidence.ts +112 -0
- package/src/preview/comparison-format.ts +21 -0
- package/src/preview/comparison-presentation.ts +187 -0
- package/src/preview/comparison-references.test.ts +430 -0
- package/src/preview/comparison-references.ts +72 -0
- package/src/preview/comparison-score.ts +59 -0
- package/src/preview/comparison-styles.ts +197 -0
- package/src/preview/comparison.test.ts +559 -0
- package/src/preview/comparison.ts +488 -0
- package/src/preview/index.ts +10 -0
- package/src/preview/types.ts +309 -0
- package/src/preview/validation.test.ts +290 -0
- package/src/preview/validation.ts +356 -0
- package/src/preview-runtime.tsx +144 -0
- package/src/raw-html-canonical.test.ts +41 -0
- package/src/raw-html-canonical.ts +179 -0
- package/src/react-types.ts +21 -0
- package/src/registry-install-plan.ts +344 -0
- package/src/registry.test.ts +245 -0
- package/src/registry.ts +369 -0
- package/src/rules/a11y-required-accessible-name.ts +93 -0
- package/src/rules/color-math.test.ts +72 -0
- package/src/rules/color-math.ts +192 -0
- package/src/rules/components-forbidden-prop-value.ts +108 -0
- package/src/rules/components-prefer-library.ts +943 -0
- package/src/rules/components-unknown-prop.ts +94 -0
- package/src/rules/composition-pattern-schema.ts +125 -0
- package/src/rules/composition-pattern.test.ts +320 -0
- package/src/rules/composition-pattern.ts +271 -0
- package/src/rules/emit-gate.test.ts +42 -0
- package/src/rules/emit-gate.ts +60 -0
- package/src/rules/finding.ts +54 -0
- package/src/rules/index.ts +235 -0
- package/src/rules/jsx-preferred-component.ts +68 -0
- package/src/rules/jsx-preferred-import-path.ts +67 -0
- package/src/rules/presets.ts +57 -0
- package/src/rules/props-invalid-value.ts +115 -0
- package/src/rules/rules.test.ts +3636 -0
- package/src/rules/spacing-resolution.ts +184 -0
- package/src/rules/styles-no-raw-color.ts +329 -0
- package/src/rules/styles-no-raw-dimensions.ts +357 -0
- package/src/rules/styles-no-raw-spacing.ts +236 -0
- package/src/rules/styles-no-raw-typography.ts +237 -0
- package/src/rules/tailwind-arbitrary-color.test.ts +128 -0
- package/src/rules/tailwind-arbitrary-color.ts +93 -0
- package/src/rules/tailwind-arbitrary-spacing.test.ts +157 -0
- package/src/rules/tailwind-arbitrary-spacing.ts +120 -0
- package/src/rules/tailwind-forbidden-palette.test.ts +116 -0
- package/src/rules/tailwind-forbidden-palette.ts +107 -0
- package/src/rules/tailwind-off-scale-spacing-token.test.ts +100 -0
- package/src/rules/tailwind-off-scale-spacing-token.ts +108 -0
- package/src/rules/tailwind-raw-color-via-token.test.ts +108 -0
- package/src/rules/tailwind-raw-color-via-token.ts +82 -0
- package/src/rules/tailwind-rule-utils.ts +106 -0
- package/src/rules/tailwind-unknown-class.test.ts +157 -0
- package/src/rules/tailwind-unknown-class.ts +121 -0
- package/src/rules/taxonomy.test.ts +238 -0
- package/src/rules/theme-no-theme-coupled-literal.ts +132 -0
- package/src/rules/tiers.ts +111 -0
- package/src/rules/tokens-css-vars-must-be-defined.test.ts +244 -0
- package/src/rules/tokens-css-vars-must-be-defined.ts +156 -0
- package/src/rules/tokens-require-dual-fallback.ts +78 -0
- package/src/rules/types.ts +27 -0
- package/src/rules/utils.test.ts +110 -0
- package/src/rules/utils.ts +599 -0
- package/src/schema.ts +427 -0
- package/src/schemas/index.ts +275 -0
- package/src/schemas/schemas.test.ts +124 -0
- package/src/severity.ts +54 -0
- package/src/storyAdapter.test.ts +571 -0
- package/src/storyAdapter.ts +761 -0
- package/src/storyFilters.test.ts +350 -0
- package/src/storyFilters.ts +253 -0
- package/src/storybook-csf.ts +11 -0
- package/src/style-comparison.ts +423 -0
- package/src/test-utils.ts +68 -0
- package/src/token-parser.ts +2 -0
- package/src/token-types.ts +287 -0
- package/src/tokens/categories.ts +131 -0
- package/src/tokens/color.ts +193 -0
- package/src/tokens/design-token-parser.test.ts +159 -0
- package/src/tokens/design-token-parser.ts +402 -0
- package/src/tokens/dtcg.ts +428 -0
- package/src/tokens/index.ts +130 -0
- package/src/tokens/lookup.ts +19 -0
- package/src/tokens/scss.ts +171 -0
- package/src/tokens/tailwind-v4.ts +187 -0
- package/src/tokens/types.ts +43 -0
- package/src/topology/index.ts +9 -0
- package/src/topology/resolve-area.test.ts +174 -0
- package/src/topology/resolve-area.ts +215 -0
- package/src/types.ts +1057 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tailwind/arbitrary-spacing` — flags arbitrary Tailwind spacing utilities
|
|
3
|
+
* that cannot be proven to sit on the configured spacing scale.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { FactIndex, TailwindClassFact } from "../facts/index.js";
|
|
7
|
+
|
|
8
|
+
import { makeFinding } from "./finding.js";
|
|
9
|
+
import type { Finding, FindingReplaceClassTokenFix } from "./types.js";
|
|
10
|
+
import {
|
|
11
|
+
matchesScale,
|
|
12
|
+
normalizeLengthForScale,
|
|
13
|
+
parseLengthValue,
|
|
14
|
+
readUsageNode,
|
|
15
|
+
} from "./utils.js";
|
|
16
|
+
|
|
17
|
+
export const RULE_ID = "tailwind/arbitrary-spacing";
|
|
18
|
+
export const RULE_VERSION = "1";
|
|
19
|
+
|
|
20
|
+
const SPACING_UTILITY_TO_PROPERTY: ReadonlyMap<string, string> = new Map([
|
|
21
|
+
["m", "margin"],
|
|
22
|
+
["mt", "margin"],
|
|
23
|
+
["mr", "margin"],
|
|
24
|
+
["mb", "margin"],
|
|
25
|
+
["ml", "margin"],
|
|
26
|
+
["ms", "margin"],
|
|
27
|
+
["me", "margin"],
|
|
28
|
+
["mx", "margin"],
|
|
29
|
+
["my", "margin"],
|
|
30
|
+
["p", "padding"],
|
|
31
|
+
["pt", "padding"],
|
|
32
|
+
["pr", "padding"],
|
|
33
|
+
["pb", "padding"],
|
|
34
|
+
["pl", "padding"],
|
|
35
|
+
["ps", "padding"],
|
|
36
|
+
["pe", "padding"],
|
|
37
|
+
["px", "padding"],
|
|
38
|
+
["py", "padding"],
|
|
39
|
+
["gap", "gap"],
|
|
40
|
+
["gap-x", "gap"],
|
|
41
|
+
["gap-y", "gap"],
|
|
42
|
+
["space-x", "gap"],
|
|
43
|
+
["space-y", "gap"],
|
|
44
|
+
] as const);
|
|
45
|
+
|
|
46
|
+
export function ruleTailwindArbitrarySpacing(ix: FactIndex): Finding[] {
|
|
47
|
+
const findings: Finding[] = [];
|
|
48
|
+
|
|
49
|
+
for (const klass of ix.byKind("tailwind_class")) {
|
|
50
|
+
if (klass.value.kind !== "arbitrary") continue;
|
|
51
|
+
|
|
52
|
+
const property = SPACING_UTILITY_TO_PROPERTY.get(klass.utility);
|
|
53
|
+
if (!property) continue;
|
|
54
|
+
|
|
55
|
+
const policy = ix.policy.propertyScale(property);
|
|
56
|
+
if (!policy) continue;
|
|
57
|
+
const scale = ix.policy.scale(policy.scale);
|
|
58
|
+
if (!scale) continue;
|
|
59
|
+
const allowed = ix.policy.scaleValues(policy.scale).map((v) => v.value);
|
|
60
|
+
if (allowed.length === 0) continue;
|
|
61
|
+
|
|
62
|
+
const parsed = parseLengthValue(klass.value.raw);
|
|
63
|
+
const normalized = parsed ? normalizeLengthForScale(parsed, scale) : null;
|
|
64
|
+
if (normalized && matchesScale(normalized.value, allowed)) continue;
|
|
65
|
+
|
|
66
|
+
const node = readUsageNode(ix, klass.nodeId);
|
|
67
|
+
const evidenceIds = node
|
|
68
|
+
? [node.id, klass.id, policy.id, scale.id]
|
|
69
|
+
: [klass.id, policy.id, scale.id];
|
|
70
|
+
|
|
71
|
+
const fix = buildFix(klass);
|
|
72
|
+
|
|
73
|
+
findings.push(
|
|
74
|
+
makeFinding({
|
|
75
|
+
ruleId: RULE_ID,
|
|
76
|
+
ruleVersion: RULE_VERSION,
|
|
77
|
+
severity: policy.severity,
|
|
78
|
+
message: `\`${klass.raw}\` is an arbitrary spacing value not on the project's spacing scale.`,
|
|
79
|
+
location: klass.location,
|
|
80
|
+
evidence: ix.evidence(evidenceIds),
|
|
81
|
+
fingerprintIdentity: {
|
|
82
|
+
source: "tailwind_class",
|
|
83
|
+
file: klass.file,
|
|
84
|
+
nodeId: klass.nodeId,
|
|
85
|
+
originPath: klass.originPath,
|
|
86
|
+
raw: klass.raw,
|
|
87
|
+
utility: klass.utility,
|
|
88
|
+
value: klass.value.raw,
|
|
89
|
+
},
|
|
90
|
+
fix,
|
|
91
|
+
attributes: {
|
|
92
|
+
rawClass: klass.raw,
|
|
93
|
+
rawValue: klass.value.raw,
|
|
94
|
+
utility: klass.utility,
|
|
95
|
+
property,
|
|
96
|
+
scale: scale.name,
|
|
97
|
+
allowed,
|
|
98
|
+
normalizedValue: normalized?.value,
|
|
99
|
+
normalizedUnit: normalized ? scale.unit : undefined,
|
|
100
|
+
assumedRootFontSizePx: normalized?.assumedRootFontSizePx,
|
|
101
|
+
assumedEmBasePx: normalized?.assumedEmBasePx,
|
|
102
|
+
source: "tailwind",
|
|
103
|
+
},
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return findings;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function buildFix(klass: TailwindClassFact): FindingReplaceClassTokenFix {
|
|
112
|
+
return {
|
|
113
|
+
kind: "replaceClassToken",
|
|
114
|
+
title: `Review ${klass.raw}`,
|
|
115
|
+
from: klass.raw,
|
|
116
|
+
to: null,
|
|
117
|
+
utility: klass.utility,
|
|
118
|
+
deterministic: false,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
compileGlobalGovernanceFacts,
|
|
5
|
+
FactIndex,
|
|
6
|
+
makeTailwindClassFact,
|
|
7
|
+
makeTailwindTokenResolvedFact,
|
|
8
|
+
makeUsageNodeFact,
|
|
9
|
+
ruleTailwindForbiddenPalette,
|
|
10
|
+
runRules,
|
|
11
|
+
} from "../index.js";
|
|
12
|
+
|
|
13
|
+
const FILE = "src/app.tsx";
|
|
14
|
+
const LOC = { file: FILE, line: 1, column: 7 };
|
|
15
|
+
|
|
16
|
+
function indexWithPolicy(): FactIndex {
|
|
17
|
+
const ix = new FactIndex();
|
|
18
|
+
ix.addMany(
|
|
19
|
+
compileGlobalGovernanceFacts({
|
|
20
|
+
tailwind: {
|
|
21
|
+
palette: {
|
|
22
|
+
deny: ["red-*"],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
);
|
|
27
|
+
return ix;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function addResolvedClass(
|
|
31
|
+
ix: FactIndex,
|
|
32
|
+
raw = "bg-red-500",
|
|
33
|
+
token = "red-500",
|
|
34
|
+
source: "default" | "theme-css" = "default"
|
|
35
|
+
): void {
|
|
36
|
+
const node = makeUsageNodeFact({
|
|
37
|
+
file: FILE,
|
|
38
|
+
nodePath: "0",
|
|
39
|
+
element: "div",
|
|
40
|
+
location: LOC,
|
|
41
|
+
});
|
|
42
|
+
ix.add(node);
|
|
43
|
+
ix.add(
|
|
44
|
+
makeTailwindClassFact({
|
|
45
|
+
file: FILE,
|
|
46
|
+
nodeId: node.id,
|
|
47
|
+
raw,
|
|
48
|
+
originPath: `attribute#0/class#${raw}`,
|
|
49
|
+
prefix: null,
|
|
50
|
+
modifiers: [],
|
|
51
|
+
important: false,
|
|
52
|
+
negative: false,
|
|
53
|
+
utility: "bg",
|
|
54
|
+
value: { kind: "token", token },
|
|
55
|
+
location: LOC,
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
ix.add(
|
|
59
|
+
makeTailwindTokenResolvedFact({
|
|
60
|
+
utility: "bg",
|
|
61
|
+
token,
|
|
62
|
+
resolved: { kind: "color", value: "#ef4444", source },
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe("tailwind/forbidden-palette", () => {
|
|
68
|
+
it("flags denied palette tokens — ACCEPTANCE §5.28", () => {
|
|
69
|
+
const ix = indexWithPolicy();
|
|
70
|
+
addResolvedClass(ix);
|
|
71
|
+
|
|
72
|
+
const findings = ruleTailwindForbiddenPalette(ix);
|
|
73
|
+
expect(findings).toHaveLength(1);
|
|
74
|
+
expect(findings[0]).toMatchObject({
|
|
75
|
+
ruleId: "tailwind/forbidden-palette",
|
|
76
|
+
severity: "moderate",
|
|
77
|
+
message:
|
|
78
|
+
"`bg-red-500` uses the `red-500` palette token, which is forbidden by policy `deny: red-*`.",
|
|
79
|
+
attributes: {
|
|
80
|
+
utility: "bg",
|
|
81
|
+
token: "red-500",
|
|
82
|
+
matchedPattern: "red-*",
|
|
83
|
+
source: "default",
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("is registered in runRules", () => {
|
|
89
|
+
const ix = indexWithPolicy();
|
|
90
|
+
addResolvedClass(ix);
|
|
91
|
+
|
|
92
|
+
expect(runRules(ix).map((f) => f.ruleId)).toContain("tailwind/forbidden-palette");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("allows project-defined theme tokens even when their names do not match the stock allowlist", () => {
|
|
96
|
+
const ix = indexWithPolicy();
|
|
97
|
+
addResolvedClass(ix, "bg-danger", "danger", "theme-css");
|
|
98
|
+
|
|
99
|
+
expect(ruleTailwindForbiddenPalette(ix)).toHaveLength(0);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("never flags universal color keywords (bg-white/text-black/border-transparent) — BUG 6", () => {
|
|
103
|
+
// An allow-list policy that white/black would otherwise violate.
|
|
104
|
+
const ix = new FactIndex();
|
|
105
|
+
ix.addMany(
|
|
106
|
+
compileGlobalGovernanceFacts({
|
|
107
|
+
tailwind: { palette: { allow: ["brand-*", "neutral-*"] } },
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
for (const token of ["white", "black", "transparent", "current"]) {
|
|
111
|
+
addResolvedClass(ix, `bg-${token}`, token, "default");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
expect(ruleTailwindForbiddenPalette(ix)).toHaveLength(0);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tailwind/forbidden-palette` — enforces Tailwind palette allow/deny policy
|
|
3
|
+
* against resolved token-form color utilities.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { FactIndex, TailwindClassFact } from "../facts/index.js";
|
|
7
|
+
|
|
8
|
+
import { makeFinding } from "./finding.js";
|
|
9
|
+
import type { Finding, FindingReplaceClassTokenFix } from "./types.js";
|
|
10
|
+
import {
|
|
11
|
+
indexResolvedTailwindTokens,
|
|
12
|
+
matchPattern,
|
|
13
|
+
resolvedForClass,
|
|
14
|
+
TAILWIND_COLOR_UTILITIES,
|
|
15
|
+
UNIVERSAL_TAILWIND_COLOR_TOKENS,
|
|
16
|
+
} from "./tailwind-rule-utils.js";
|
|
17
|
+
import { readUsageNode } from "./utils.js";
|
|
18
|
+
|
|
19
|
+
export const RULE_ID = "tailwind/forbidden-palette";
|
|
20
|
+
export const RULE_VERSION = "1";
|
|
21
|
+
|
|
22
|
+
export function ruleTailwindForbiddenPalette(ix: FactIndex): Finding[] {
|
|
23
|
+
const deny = ix.policy.tailwindPaletteDeny();
|
|
24
|
+
const allow = ix.policy.tailwindPaletteAllow();
|
|
25
|
+
if (!deny && !allow) return [];
|
|
26
|
+
|
|
27
|
+
const resolvedByKey = indexResolvedTailwindTokens(ix);
|
|
28
|
+
const findings: Finding[] = [];
|
|
29
|
+
|
|
30
|
+
for (const klass of ix.byKind("tailwind_class")) {
|
|
31
|
+
if (klass.value.kind !== "token") continue;
|
|
32
|
+
const token = klass.value.token;
|
|
33
|
+
if (!TAILWIND_COLOR_UTILITIES.has(klass.utility)) continue;
|
|
34
|
+
if (UNIVERSAL_TAILWIND_COLOR_TOKENS.has(token)) continue;
|
|
35
|
+
|
|
36
|
+
const resolved = resolvedForClass(resolvedByKey, klass);
|
|
37
|
+
if (!resolved || resolved.resolved.kind !== "color") continue;
|
|
38
|
+
if (resolved.resolved.source === "theme-css") continue;
|
|
39
|
+
|
|
40
|
+
const deniedPattern = deny ? matchPattern(token, deny.patterns) : null;
|
|
41
|
+
const allowedPattern = allow ? matchPattern(token, allow.patterns) : null;
|
|
42
|
+
const allowViolation = allow && !allowedPattern;
|
|
43
|
+
if (!deniedPattern && !allowViolation) continue;
|
|
44
|
+
|
|
45
|
+
const policy = deniedPattern ? deny! : allow!;
|
|
46
|
+
const matchedPattern = deniedPattern ?? allowedPattern ?? null;
|
|
47
|
+
const node = readUsageNode(ix, klass.nodeId);
|
|
48
|
+
const evidenceIds = node
|
|
49
|
+
? [node.id, klass.id, resolved.id, policy.id]
|
|
50
|
+
: [klass.id, resolved.id, policy.id];
|
|
51
|
+
|
|
52
|
+
findings.push(
|
|
53
|
+
makeFinding({
|
|
54
|
+
ruleId: RULE_ID,
|
|
55
|
+
ruleVersion: RULE_VERSION,
|
|
56
|
+
severity: policy.severity,
|
|
57
|
+
message: messageFor(klass, token, deniedPattern, allow?.patterns ?? []),
|
|
58
|
+
location: klass.location,
|
|
59
|
+
evidence: ix.evidence(evidenceIds),
|
|
60
|
+
fingerprintIdentity: {
|
|
61
|
+
source: "tailwind_class",
|
|
62
|
+
file: klass.file,
|
|
63
|
+
nodeId: klass.nodeId,
|
|
64
|
+
originPath: klass.originPath,
|
|
65
|
+
raw: klass.raw,
|
|
66
|
+
utility: klass.utility,
|
|
67
|
+
token,
|
|
68
|
+
matchedPattern,
|
|
69
|
+
},
|
|
70
|
+
fix: buildFix(klass),
|
|
71
|
+
attributes: {
|
|
72
|
+
rawClass: klass.raw,
|
|
73
|
+
utility: klass.utility,
|
|
74
|
+
token,
|
|
75
|
+
matchedPattern,
|
|
76
|
+
allowedPatterns: allow?.patterns,
|
|
77
|
+
source: resolved.resolved.source,
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return findings;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function messageFor(
|
|
87
|
+
klass: TailwindClassFact,
|
|
88
|
+
token: string,
|
|
89
|
+
deniedPattern: string | null,
|
|
90
|
+
allowedPatterns: readonly string[]
|
|
91
|
+
): string {
|
|
92
|
+
if (deniedPattern) {
|
|
93
|
+
return `\`${klass.raw}\` uses the \`${token}\` palette token, which is forbidden by policy \`deny: ${deniedPattern}\`.`;
|
|
94
|
+
}
|
|
95
|
+
return `\`${klass.raw}\` uses the \`${token}\` palette token, which is outside the allowed Tailwind palette patterns [${allowedPatterns.join(", ")}].`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function buildFix(klass: TailwindClassFact): FindingReplaceClassTokenFix {
|
|
99
|
+
return {
|
|
100
|
+
kind: "replaceClassToken",
|
|
101
|
+
title: `Review ${klass.raw}`,
|
|
102
|
+
from: klass.raw,
|
|
103
|
+
to: null,
|
|
104
|
+
utility: klass.utility,
|
|
105
|
+
deterministic: false,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
compileGlobalGovernanceFacts,
|
|
5
|
+
FactIndex,
|
|
6
|
+
g,
|
|
7
|
+
makeTailwindClassFact,
|
|
8
|
+
makeTailwindTokenResolvedFact,
|
|
9
|
+
makeUsageNodeFact,
|
|
10
|
+
ruleTailwindOffScaleSpacingToken,
|
|
11
|
+
runRules,
|
|
12
|
+
} from "../index.js";
|
|
13
|
+
|
|
14
|
+
const FILE = "src/app.tsx";
|
|
15
|
+
const LOC = { file: FILE, line: 1, column: 7 };
|
|
16
|
+
|
|
17
|
+
function indexWithScale(): FactIndex {
|
|
18
|
+
const ix = new FactIndex();
|
|
19
|
+
ix.addMany(
|
|
20
|
+
compileGlobalGovernanceFacts({
|
|
21
|
+
scales: {
|
|
22
|
+
space: g.scale.px([0, 4, 8, 16, 32]),
|
|
23
|
+
},
|
|
24
|
+
styles: [
|
|
25
|
+
g.styles.rawSpacing().mustMatchScale("space", {
|
|
26
|
+
appliesTo: ["padding"],
|
|
27
|
+
severity: "warn",
|
|
28
|
+
}),
|
|
29
|
+
],
|
|
30
|
+
})
|
|
31
|
+
);
|
|
32
|
+
return ix;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function addResolvedSpacing(ix: FactIndex, raw = "p-3", token = "3", value = "0.75rem"): void {
|
|
36
|
+
const node = makeUsageNodeFact({
|
|
37
|
+
file: FILE,
|
|
38
|
+
nodePath: "0",
|
|
39
|
+
element: "div",
|
|
40
|
+
location: LOC,
|
|
41
|
+
});
|
|
42
|
+
ix.add(node);
|
|
43
|
+
ix.add(
|
|
44
|
+
makeTailwindClassFact({
|
|
45
|
+
file: FILE,
|
|
46
|
+
nodeId: node.id,
|
|
47
|
+
raw,
|
|
48
|
+
originPath: `attribute#0/class#${raw}`,
|
|
49
|
+
prefix: null,
|
|
50
|
+
modifiers: [],
|
|
51
|
+
important: false,
|
|
52
|
+
negative: false,
|
|
53
|
+
utility: "p",
|
|
54
|
+
value: { kind: "token", token },
|
|
55
|
+
location: LOC,
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
ix.add(
|
|
59
|
+
makeTailwindTokenResolvedFact({
|
|
60
|
+
utility: "p",
|
|
61
|
+
token,
|
|
62
|
+
resolved: { kind: "length", value, source: "theme-css" },
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe("tailwind/off-scale-spacing-token", () => {
|
|
68
|
+
it("flags spacing tokens whose resolved value misses the scale — ACCEPTANCE §5.29", () => {
|
|
69
|
+
const ix = indexWithScale();
|
|
70
|
+
addResolvedSpacing(ix);
|
|
71
|
+
|
|
72
|
+
const findings = ruleTailwindOffScaleSpacingToken(ix);
|
|
73
|
+
expect(findings).toHaveLength(1);
|
|
74
|
+
expect(findings[0]).toMatchObject({
|
|
75
|
+
ruleId: "tailwind/off-scale-spacing-token",
|
|
76
|
+
severity: "moderate",
|
|
77
|
+
message:
|
|
78
|
+
"`p-3` resolves to 12px, which is not on the project's spacing scale [0, 4, 8, 16, 32].",
|
|
79
|
+
attributes: {
|
|
80
|
+
utility: "p",
|
|
81
|
+
token: "3",
|
|
82
|
+
resolvedPx: 12,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("allows resolved spacing tokens on the scale", () => {
|
|
88
|
+
const ix = indexWithScale();
|
|
89
|
+
addResolvedSpacing(ix, "p-4", "4", "1rem");
|
|
90
|
+
|
|
91
|
+
expect(ruleTailwindOffScaleSpacingToken(ix)).toHaveLength(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("is registered in runRules", () => {
|
|
95
|
+
const ix = indexWithScale();
|
|
96
|
+
addResolvedSpacing(ix);
|
|
97
|
+
|
|
98
|
+
expect(runRules(ix).map((f) => f.ruleId)).toContain("tailwind/off-scale-spacing-token");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tailwind/off-scale-spacing-token` — checks resolved Tailwind spacing
|
|
3
|
+
* tokens against the same project spacing scale used for CSS declarations.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { FactIndex } from "../facts/index.js";
|
|
7
|
+
|
|
8
|
+
import { makeFinding } from "./finding.js";
|
|
9
|
+
import type { Finding, FindingReplaceClassTokenFix } from "./types.js";
|
|
10
|
+
import {
|
|
11
|
+
indexResolvedTailwindTokens,
|
|
12
|
+
resolvedForClass,
|
|
13
|
+
TAILWIND_SPACING_UTILITY_TO_PROPERTY,
|
|
14
|
+
} from "./tailwind-rule-utils.js";
|
|
15
|
+
import {
|
|
16
|
+
matchesScale,
|
|
17
|
+
normalizeLengthForScale,
|
|
18
|
+
parseLengthValue,
|
|
19
|
+
readUsageNode,
|
|
20
|
+
} from "./utils.js";
|
|
21
|
+
|
|
22
|
+
export const RULE_ID = "tailwind/off-scale-spacing-token";
|
|
23
|
+
export const RULE_VERSION = "1";
|
|
24
|
+
|
|
25
|
+
export function ruleTailwindOffScaleSpacingToken(ix: FactIndex): Finding[] {
|
|
26
|
+
const resolvedByKey = indexResolvedTailwindTokens(ix);
|
|
27
|
+
const findings: Finding[] = [];
|
|
28
|
+
|
|
29
|
+
for (const klass of ix.byKind("tailwind_class")) {
|
|
30
|
+
if (klass.value.kind !== "token") continue;
|
|
31
|
+
const property = TAILWIND_SPACING_UTILITY_TO_PROPERTY.get(klass.utility);
|
|
32
|
+
if (!property) continue;
|
|
33
|
+
|
|
34
|
+
const policy = ix.policy.propertyScale(property);
|
|
35
|
+
if (!policy) continue;
|
|
36
|
+
const scale = ix.policy.scale(policy.scale);
|
|
37
|
+
if (!scale) continue;
|
|
38
|
+
const allowed = ix.policy.scaleValues(policy.scale).map((v) => v.value);
|
|
39
|
+
if (allowed.length === 0) continue;
|
|
40
|
+
|
|
41
|
+
const resolved = resolvedForClass(resolvedByKey, klass);
|
|
42
|
+
if (!resolved || resolved.resolved.kind !== "length") continue;
|
|
43
|
+
|
|
44
|
+
const parsed = parseLengthValue(resolved.resolved.value);
|
|
45
|
+
if (!parsed) continue;
|
|
46
|
+
const normalized = normalizeLengthForScale(parsed, scale);
|
|
47
|
+
if (!normalized) continue;
|
|
48
|
+
const signedValue = klass.negative ? -normalized.value : normalized.value;
|
|
49
|
+
if (matchesScale(signedValue, allowed)) continue;
|
|
50
|
+
|
|
51
|
+
const node = readUsageNode(ix, klass.nodeId);
|
|
52
|
+
const evidenceIds = node
|
|
53
|
+
? [node.id, klass.id, resolved.id, policy.id, scale.id]
|
|
54
|
+
: [klass.id, resolved.id, policy.id, scale.id];
|
|
55
|
+
|
|
56
|
+
findings.push(
|
|
57
|
+
makeFinding({
|
|
58
|
+
ruleId: RULE_ID,
|
|
59
|
+
ruleVersion: RULE_VERSION,
|
|
60
|
+
severity: policy.severity,
|
|
61
|
+
message: `\`${klass.raw}\` resolves to ${formatResolvedValue(signedValue, scale.unit)}, which is not on the project's spacing scale [${allowed.join(", ")}].`,
|
|
62
|
+
location: klass.location,
|
|
63
|
+
evidence: ix.evidence(evidenceIds),
|
|
64
|
+
fingerprintIdentity: {
|
|
65
|
+
source: "tailwind_class",
|
|
66
|
+
file: klass.file,
|
|
67
|
+
nodeId: klass.nodeId,
|
|
68
|
+
originPath: klass.originPath,
|
|
69
|
+
raw: klass.raw,
|
|
70
|
+
utility: klass.utility,
|
|
71
|
+
token: klass.value.token,
|
|
72
|
+
resolvedValue: signedValue,
|
|
73
|
+
scale: scale.name,
|
|
74
|
+
},
|
|
75
|
+
fix: buildFix(klass.raw, klass.utility),
|
|
76
|
+
attributes: {
|
|
77
|
+
rawClass: klass.raw,
|
|
78
|
+
utility: klass.utility,
|
|
79
|
+
token: klass.value.token,
|
|
80
|
+
property,
|
|
81
|
+
scale: scale.name,
|
|
82
|
+
allowed,
|
|
83
|
+
resolvedValue: resolved.resolved.value,
|
|
84
|
+
resolvedPx: scale.unit === "px" ? signedValue : undefined,
|
|
85
|
+
resolvedUnit: scale.unit,
|
|
86
|
+
source: resolved.resolved.source,
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return findings;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function formatResolvedValue(value: number, unit: string): string {
|
|
96
|
+
return unit === "px" ? `${value}px` : `${value}${unit}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildFix(raw: string, utility: string): FindingReplaceClassTokenFix {
|
|
100
|
+
return {
|
|
101
|
+
kind: "replaceClassToken",
|
|
102
|
+
title: `Review ${raw}`,
|
|
103
|
+
from: raw,
|
|
104
|
+
to: null,
|
|
105
|
+
utility,
|
|
106
|
+
deterministic: false,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
compileGlobalGovernanceFacts,
|
|
5
|
+
FactIndex,
|
|
6
|
+
makeTailwindClassFact,
|
|
7
|
+
makeTailwindTokenResolvedFact,
|
|
8
|
+
makeUsageNodeFact,
|
|
9
|
+
ruleTailwindRawColorViaToken,
|
|
10
|
+
runRules,
|
|
11
|
+
} from "../index.js";
|
|
12
|
+
|
|
13
|
+
const FILE = "src/app.tsx";
|
|
14
|
+
const LOC = { file: FILE, line: 1, column: 7 };
|
|
15
|
+
|
|
16
|
+
function indexWithAllowPolicy(): FactIndex {
|
|
17
|
+
const ix = new FactIndex();
|
|
18
|
+
ix.addMany(
|
|
19
|
+
compileGlobalGovernanceFacts({
|
|
20
|
+
tailwind: {
|
|
21
|
+
palette: {
|
|
22
|
+
allow: ["brand-*"],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
);
|
|
27
|
+
return ix;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function addResolvedColor(
|
|
31
|
+
ix: FactIndex,
|
|
32
|
+
raw = "bg-red-500",
|
|
33
|
+
token = "red-500",
|
|
34
|
+
source: "default" | "theme-css" = "default"
|
|
35
|
+
): void {
|
|
36
|
+
const node = makeUsageNodeFact({
|
|
37
|
+
file: FILE,
|
|
38
|
+
nodePath: "0",
|
|
39
|
+
element: "div",
|
|
40
|
+
location: LOC,
|
|
41
|
+
});
|
|
42
|
+
ix.add(node);
|
|
43
|
+
ix.add(
|
|
44
|
+
makeTailwindClassFact({
|
|
45
|
+
file: FILE,
|
|
46
|
+
nodeId: node.id,
|
|
47
|
+
raw,
|
|
48
|
+
originPath: `attribute#0/class#${raw}`,
|
|
49
|
+
prefix: null,
|
|
50
|
+
modifiers: [],
|
|
51
|
+
important: false,
|
|
52
|
+
negative: false,
|
|
53
|
+
utility: "bg",
|
|
54
|
+
value: { kind: "token", token },
|
|
55
|
+
location: LOC,
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
ix.add(
|
|
59
|
+
makeTailwindTokenResolvedFact({
|
|
60
|
+
utility: "bg",
|
|
61
|
+
token,
|
|
62
|
+
resolved: { kind: "color", value: "#ef4444", source },
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe("tailwind/raw-color-via-token", () => {
|
|
68
|
+
it("flags default color tokens outside the allowed palette — ACCEPTANCE §5.30", () => {
|
|
69
|
+
const ix = indexWithAllowPolicy();
|
|
70
|
+
addResolvedColor(ix);
|
|
71
|
+
|
|
72
|
+
const findings = ruleTailwindRawColorViaToken(ix);
|
|
73
|
+
expect(findings).toHaveLength(1);
|
|
74
|
+
expect(findings[0]).toMatchObject({
|
|
75
|
+
ruleId: "tailwind/raw-color-via-token",
|
|
76
|
+
severity: "moderate",
|
|
77
|
+
message:
|
|
78
|
+
"`bg-red-500` resolves to raw color `#ef4444` (Tailwind default). Project policy requires `brand-*` palette tokens.",
|
|
79
|
+
attributes: {
|
|
80
|
+
utility: "bg",
|
|
81
|
+
token: "red-500",
|
|
82
|
+
resolvedValue: "#ef4444",
|
|
83
|
+
source: "default",
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("allows tokens matching the palette allow list", () => {
|
|
89
|
+
const ix = indexWithAllowPolicy();
|
|
90
|
+
addResolvedColor(ix, "bg-brand-500", "brand-500");
|
|
91
|
+
|
|
92
|
+
expect(ruleTailwindRawColorViaToken(ix)).toHaveLength(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("allows project-defined theme tokens outside the stock palette names", () => {
|
|
96
|
+
const ix = indexWithAllowPolicy();
|
|
97
|
+
addResolvedColor(ix, "bg-danger", "danger", "theme-css");
|
|
98
|
+
|
|
99
|
+
expect(ruleTailwindRawColorViaToken(ix)).toHaveLength(0);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("is registered in runRules", () => {
|
|
103
|
+
const ix = indexWithAllowPolicy();
|
|
104
|
+
addResolvedColor(ix);
|
|
105
|
+
|
|
106
|
+
expect(runRules(ix).map((f) => f.ruleId)).toContain("tailwind/raw-color-via-token");
|
|
107
|
+
});
|
|
108
|
+
});
|