cloudflare/kumo

Public

mirrored fromhttps://github.com/cloudflare/kumoAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
27f04eb919d6e913dbf53f15c37a4251611a568c

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

lint/enforce-variant-standard.js

186lines · modecode

1import { defineRule } from "oxlint";
2
3/**
4 * Enforces the Kumo variant export standard for components:
5 * - KUMO_{COMPONENT}_VARIANTS (required)
6 * - KUMO_{COMPONENT}_DEFAULT_VARIANTS (required)
7 * - KUMO_{COMPONENT}_BASE_STYLES (optional, but must have KUMO_ prefix)
8 *
9 * Only applies to files in src/components/**\/*.tsx
10 */
11
12/**
13 * Extract component name from file path.
14 * Example: "src/components/button/button.tsx" -> "BUTTON"
15 */
16function getComponentNameFromPath(filename) {
17 const match = filename.match(/src\/components\/([^/]+)\/\1\.tsx$/);
18 if (!match) return null;
19 return match[1].toUpperCase().replace(/-/g, "_");
20}
21
22/**
23 * Check if export name matches expected pattern.
24 * Returns { valid: boolean, expectedName?: string, exportType?: string }
25 */
26function validateExportName(exportName, componentName) {
27 const variantsPattern = `KUMO_${componentName}_VARIANTS`;
28 const defaultVariantsPattern = `KUMO_${componentName}_DEFAULT_VARIANTS`;
29 const baseStylesPattern = `KUMO_${componentName}_BASE_STYLES`;
30
31 if (exportName === variantsPattern) {
32 return { valid: true, exportType: "VARIANTS" };
33 }
34 if (exportName === defaultVariantsPattern) {
35 return { valid: true, exportType: "DEFAULT_VARIANTS" };
36 }
37 if (exportName === baseStylesPattern) {
38 return { valid: true, exportType: "BASE_STYLES" };
39 }
40
41 // Check for incorrect naming patterns
42 if (exportName.endsWith("_VARIANTS") && exportName.startsWith("KUMO_")) {
43 return {
44 valid: false,
45 expectedName: variantsPattern,
46 exportType: "VARIANTS",
47 };
48 }
49 if (
50 exportName.endsWith("_DEFAULT_VARIANTS") &&
51 exportName.startsWith("KUMO_")
52 ) {
53 return {
54 valid: false,
55 expectedName: defaultVariantsPattern,
56 exportType: "DEFAULT_VARIANTS",
57 };
58 }
59 if (exportName.endsWith("_BASE_STYLES")) {
60 // BASE_STYLES must have KUMO_ prefix
61 if (!exportName.startsWith("KUMO_")) {
62 return {
63 valid: false,
64 expectedName: baseStylesPattern,
65 exportType: "BASE_STYLES",
66 };
67 }
68 // Wrong component name
69 return {
70 valid: false,
71 expectedName: baseStylesPattern,
72 exportType: "BASE_STYLES",
73 };
74 }
75
76 return { valid: true }; // Not a variant-related export
77}
78
79export const enforceVariantStandardRule = defineRule({
80 meta: {
81 type: "problem",
82 docs: {
83 description:
84 "Enforce Kumo variant standard: KUMO_{COMPONENT}_VARIANTS, KUMO_{COMPONENT}_DEFAULT_VARIANTS, and optionally KUMO_{COMPONENT}_BASE_STYLES",
85 },
86 messages: {
87 incorrectName:
88 "Export name '{{actual}}' should be '{{expected}}' to follow Kumo variant naming convention",
89 missingVariants:
90 "Component must export KUMO_{{component}}_VARIANTS. Found: {{found}}",
91 missingDefaultVariants:
92 "Component must export KUMO_{{component}}_DEFAULT_VARIANTS. Found: {{found}}",
93 },
94 schema: [],
95 },
96 defaultOptions: [],
97 createOnce(context) {
98 const foundExports = new Set();
99 let programNode = null;
100 let filename = null;
101 let componentName = null;
102 let shouldCheck = false;
103
104 return {
105 Program(node) {
106 programNode = node;
107 filename = context.filename;
108
109 // Only apply to component files in src/components/**/*.tsx
110 if (!filename.match(/src\/components\/[^/]+\/[^/]+\.tsx$/)) {
111 shouldCheck = false;
112 return;
113 }
114
115 componentName = getComponentNameFromPath(filename);
116 shouldCheck = componentName !== null;
117 },
118 ExportNamedDeclaration(node) {
119 if (!shouldCheck) return;
120
121 // Check for named const exports
122 if (
123 node.declaration &&
124 node.declaration.type === "VariableDeclaration"
125 ) {
126 for (const decl of node.declaration.declarations) {
127 if (decl.id && decl.id.type === "Identifier") {
128 const exportName = decl.id.name;
129 foundExports.add(exportName);
130
131 const validation = validateExportName(exportName, componentName);
132 if (!validation.valid && validation.expectedName) {
133 context.report({
134 node: decl.id,
135 messageId: "incorrectName",
136 data: {
137 actual: exportName,
138 expected: validation.expectedName,
139 },
140 });
141 }
142 }
143 }
144 }
145 },
146 "Program:exit"() {
147 if (!shouldCheck) return;
148
149 const expectedVariants = `KUMO_${componentName}_VARIANTS`;
150 const expectedDefaultVariants = `KUMO_${componentName}_DEFAULT_VARIANTS`;
151
152 // Check for required exports at end of file
153 const hasVariants = foundExports.has(expectedVariants);
154 const hasDefaultVariants = foundExports.has(expectedDefaultVariants);
155
156 if (!hasVariants) {
157 context.report({
158 node: programNode,
159 messageId: "missingVariants",
160 data: {
161 component: componentName,
162 found:
163 Array.from(foundExports)
164 .filter((e) => e.includes("VARIANT"))
165 .join(", ") || "none",
166 },
167 });
168 }
169
170 if (!hasDefaultVariants) {
171 context.report({
172 node: programNode,
173 messageId: "missingDefaultVariants",
174 data: {
175 component: componentName,
176 found:
177 Array.from(foundExports)
178 .filter((e) => e.includes("VARIANT"))
179 .join(", ") || "none",
180 },
181 });
182 }
183 },
184 };
185 },
186});
187