uilint 0.2.1 → 0.2.3
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/dist/index.js +2155 -253
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
- package/skills/ui-consistency-enforcer/SKILL.md +445 -0
- package/skills/ui-consistency-enforcer/references/REGISTRY-ENTRY.md +163 -0
- package/skills/ui-consistency-enforcer/references/RULE-TEMPLATE.ts +253 -0
- package/skills/ui-consistency-enforcer/references/TEST-TEMPLATE.ts +496 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uilint",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "CLI for UILint - AI-powered UI consistency checking",
|
|
5
5
|
"author": "Peter Suggate",
|
|
6
6
|
"repository": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"dist",
|
|
30
|
+
"skills",
|
|
30
31
|
"README.md"
|
|
31
32
|
],
|
|
32
33
|
"engines": {
|
|
@@ -41,9 +42,9 @@
|
|
|
41
42
|
"dotenv": "^16.4.7",
|
|
42
43
|
"magicast": "^0.3.5",
|
|
43
44
|
"picocolors": "^1.1.1",
|
|
44
|
-
"
|
|
45
|
-
"uilint-
|
|
46
|
-
"
|
|
45
|
+
"ws": "^8.18.0",
|
|
46
|
+
"uilint-core": "0.2.3",
|
|
47
|
+
"uilint-eslint": "0.2.3"
|
|
47
48
|
},
|
|
48
49
|
"optionalDependencies": {
|
|
49
50
|
"@langfuse/client": "^4.5.1",
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ui-consistency-enforcer
|
|
3
|
+
description: |
|
|
4
|
+
When the user asks you to update React UI code in ways that suggest they want
|
|
5
|
+
consistency with existing project patterns, style guides, or design systems,
|
|
6
|
+
this skill helps you create a bulletproof ESLint rule to enforce that pattern
|
|
7
|
+
permanently. Trigger keywords: "make consistent", "match the style", "follow
|
|
8
|
+
the pattern", "like the other components", "according to our design system",
|
|
9
|
+
"enforce across the codebase", "prevent this in the future".
|
|
10
|
+
license: MIT
|
|
11
|
+
metadata:
|
|
12
|
+
author: uilint
|
|
13
|
+
version: "1.0.0"
|
|
14
|
+
category: react-eslint
|
|
15
|
+
compatibility: |
|
|
16
|
+
Requires Node.js 20+, TypeScript project with @typescript-eslint/utils,
|
|
17
|
+
and uilint-eslint package installed.
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# UI Consistency Enforcer Skill
|
|
21
|
+
|
|
22
|
+
You are an expert at writing bulletproof ESLint rules for React/TypeScript projects
|
|
23
|
+
using the @typescript-eslint framework. When the user asks you to update UI code in
|
|
24
|
+
a way that suggests they want consistency with existing patterns, you should:
|
|
25
|
+
|
|
26
|
+
1. **Identify the consistency pattern** the user wants to enforce
|
|
27
|
+
2. **Create an ESLint rule** that will catch future violations
|
|
28
|
+
3. **Write comprehensive tests** for the rule
|
|
29
|
+
4. **Register the rule** in the uilint-eslint package
|
|
30
|
+
|
|
31
|
+
## When to Activate
|
|
32
|
+
|
|
33
|
+
Activate this skill when the user's request matches patterns like:
|
|
34
|
+
|
|
35
|
+
- "Update this component to use our Button instead of native button"
|
|
36
|
+
- "Make sure all forms use our design system components"
|
|
37
|
+
- "This should match the pattern in our other components"
|
|
38
|
+
- "Enforce that we always use X instead of Y"
|
|
39
|
+
- "Create a lint rule to prevent this"
|
|
40
|
+
- "Make this consistent with our style guide"
|
|
41
|
+
- Refactoring multiple components to follow a pattern
|
|
42
|
+
- Adding design system enforcement
|
|
43
|
+
|
|
44
|
+
## Step-by-Step Process
|
|
45
|
+
|
|
46
|
+
### Step 1: Understand the Pattern
|
|
47
|
+
|
|
48
|
+
Before writing the rule, understand:
|
|
49
|
+
|
|
50
|
+
1. **What pattern should be enforced?** (e.g., "use shadcn Button instead of native button")
|
|
51
|
+
2. **Where does it apply?** (all files? only components? specific directories?)
|
|
52
|
+
3. **What are the exceptions?** (tests? internal utilities? specific files?)
|
|
53
|
+
4. **What's the fix?** (what should developers do instead?)
|
|
54
|
+
|
|
55
|
+
Ask clarifying questions if needed before proceeding.
|
|
56
|
+
|
|
57
|
+
### Step 2: Analyze Existing Rules
|
|
58
|
+
|
|
59
|
+
Look at the existing uilint-eslint rules for patterns:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
ls packages/uilint-eslint/src/rules/
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Read similar rules to understand the codebase patterns:
|
|
66
|
+
- `prefer-zustand-state-management.ts` - component analysis, hook counting
|
|
67
|
+
- `no-mixed-component-libraries.ts` - cross-file analysis, import tracking
|
|
68
|
+
- `consistent-spacing.ts` - className/Tailwind pattern matching
|
|
69
|
+
- `consistent-dark-mode.ts` - attribute analysis in JSX
|
|
70
|
+
|
|
71
|
+
### Step 3: Write the Rule
|
|
72
|
+
|
|
73
|
+
Create the rule file at `packages/uilint-eslint/src/rules/{rule-name}.ts`.
|
|
74
|
+
|
|
75
|
+
Follow this structure exactly:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
/**
|
|
79
|
+
* Rule: {rule-name}
|
|
80
|
+
*
|
|
81
|
+
* {Description of what this rule does and why}
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
import { createRule } from "../utils/create-rule.js";
|
|
85
|
+
import type { TSESTree } from "@typescript-eslint/utils";
|
|
86
|
+
|
|
87
|
+
type MessageIds = "{messageId1}" | "{messageId2}";
|
|
88
|
+
type Options = [
|
|
89
|
+
{
|
|
90
|
+
/** Option description */
|
|
91
|
+
optionName?: optionType;
|
|
92
|
+
}?
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
export default createRule<Options, MessageIds>({
|
|
96
|
+
name: "{rule-name}",
|
|
97
|
+
meta: {
|
|
98
|
+
type: "problem" | "suggestion" | "layout",
|
|
99
|
+
docs: {
|
|
100
|
+
description: "{Human-readable description}",
|
|
101
|
+
},
|
|
102
|
+
messages: {
|
|
103
|
+
messageId1: "{Error message with {{placeholders}}}",
|
|
104
|
+
messageId2: "{Another message}",
|
|
105
|
+
},
|
|
106
|
+
schema: [
|
|
107
|
+
{
|
|
108
|
+
type: "object",
|
|
109
|
+
properties: {
|
|
110
|
+
optionName: {
|
|
111
|
+
type: "string",
|
|
112
|
+
description: "Option description",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
additionalProperties: false,
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
defaultOptions: [{ optionName: "default" }],
|
|
120
|
+
create(context) {
|
|
121
|
+
const options = context.options[0] || {};
|
|
122
|
+
// Use options with defaults
|
|
123
|
+
const optionName = options.optionName ?? "default";
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
// AST visitor methods
|
|
127
|
+
JSXOpeningElement(node) {
|
|
128
|
+
// Analyze JSX elements
|
|
129
|
+
},
|
|
130
|
+
ImportDeclaration(node) {
|
|
131
|
+
// Track imports
|
|
132
|
+
},
|
|
133
|
+
CallExpression(node) {
|
|
134
|
+
// Analyze function calls like hooks
|
|
135
|
+
},
|
|
136
|
+
"Program:exit"() {
|
|
137
|
+
// Final analysis after parsing entire file
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Step 4: Write Comprehensive Tests
|
|
145
|
+
|
|
146
|
+
Create the test file at `packages/uilint-eslint/src/rules/{rule-name}.test.ts`.
|
|
147
|
+
|
|
148
|
+
Follow this structure:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
/**
|
|
152
|
+
* Tests for: {rule-name}
|
|
153
|
+
*
|
|
154
|
+
* {Description}
|
|
155
|
+
*/
|
|
156
|
+
|
|
157
|
+
import { RuleTester } from "@typescript-eslint/rule-tester";
|
|
158
|
+
import { describe, it, afterAll, beforeEach } from "vitest";
|
|
159
|
+
import rule from "./{rule-name}";
|
|
160
|
+
// If rule uses caching:
|
|
161
|
+
// import { clearCache } from "../utils/import-graph.js";
|
|
162
|
+
|
|
163
|
+
RuleTester.afterAll = afterAll;
|
|
164
|
+
RuleTester.describe = describe;
|
|
165
|
+
RuleTester.it = it;
|
|
166
|
+
|
|
167
|
+
const ruleTester = new RuleTester({
|
|
168
|
+
languageOptions: {
|
|
169
|
+
ecmaVersion: 2022,
|
|
170
|
+
sourceType: "module",
|
|
171
|
+
parserOptions: {
|
|
172
|
+
ecmaFeatures: { jsx: true },
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Clear cache between tests if needed
|
|
178
|
+
// beforeEach(() => {
|
|
179
|
+
// clearCache();
|
|
180
|
+
// });
|
|
181
|
+
|
|
182
|
+
ruleTester.run("{rule-name}", rule, {
|
|
183
|
+
valid: [
|
|
184
|
+
// ============================================
|
|
185
|
+
// PREFERRED PATTERN USED CORRECTLY
|
|
186
|
+
// ============================================
|
|
187
|
+
{
|
|
188
|
+
name: "uses preferred pattern",
|
|
189
|
+
code: `
|
|
190
|
+
// Valid code example
|
|
191
|
+
`,
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: "with custom options",
|
|
195
|
+
code: `...`,
|
|
196
|
+
options: [{ optionName: "custom" }],
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
// ============================================
|
|
200
|
+
// EXCEPTIONS / EDGE CASES
|
|
201
|
+
// ============================================
|
|
202
|
+
{
|
|
203
|
+
name: "exception case is allowed",
|
|
204
|
+
code: `...`,
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
|
|
208
|
+
invalid: [
|
|
209
|
+
// ============================================
|
|
210
|
+
// BASIC VIOLATIONS
|
|
211
|
+
// ============================================
|
|
212
|
+
{
|
|
213
|
+
name: "violates pattern",
|
|
214
|
+
code: `
|
|
215
|
+
// Invalid code example
|
|
216
|
+
`,
|
|
217
|
+
errors: [
|
|
218
|
+
{
|
|
219
|
+
messageId: "messageId1",
|
|
220
|
+
data: { key: "value" },
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
// ============================================
|
|
226
|
+
// WITH OPTIONS
|
|
227
|
+
// ============================================
|
|
228
|
+
{
|
|
229
|
+
name: "violates with custom options",
|
|
230
|
+
code: `...`,
|
|
231
|
+
options: [{ optionName: "strict" }],
|
|
232
|
+
errors: [
|
|
233
|
+
{
|
|
234
|
+
messageId: "messageId2",
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Step 5: Register the Rule
|
|
243
|
+
|
|
244
|
+
Add the rule to `packages/uilint-eslint/src/rule-registry.ts`:
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
{
|
|
248
|
+
id: "{rule-name}",
|
|
249
|
+
name: "{Display Name}",
|
|
250
|
+
description: "{Short description for CLI}",
|
|
251
|
+
defaultSeverity: "warn" | "error",
|
|
252
|
+
defaultOptions: [{ /* defaults */ }],
|
|
253
|
+
optionSchema: {
|
|
254
|
+
fields: [
|
|
255
|
+
{
|
|
256
|
+
key: "optionName",
|
|
257
|
+
label: "Option Label",
|
|
258
|
+
type: "select" | "text" | "boolean" | "number",
|
|
259
|
+
defaultValue: "default",
|
|
260
|
+
options: [{ value: "x", label: "X" }], // for select
|
|
261
|
+
description: "Help text",
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
},
|
|
265
|
+
category: "static",
|
|
266
|
+
},
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Step 6: Regenerate Index and Test
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
# Regenerate the index file
|
|
273
|
+
pnpm -C packages/uilint-eslint generate:index
|
|
274
|
+
|
|
275
|
+
# Run the tests
|
|
276
|
+
pnpm -C packages/uilint-eslint test
|
|
277
|
+
|
|
278
|
+
# Build to verify
|
|
279
|
+
pnpm -C packages/uilint-eslint build
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Common AST Patterns
|
|
283
|
+
|
|
284
|
+
### Detecting JSX Elements
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
JSXOpeningElement(node) {
|
|
288
|
+
// Get element name
|
|
289
|
+
if (node.name.type === "JSXIdentifier") {
|
|
290
|
+
const name = node.name.name;
|
|
291
|
+
// Check if it's a component (PascalCase) vs HTML (lowercase)
|
|
292
|
+
if (/^[A-Z]/.test(name)) {
|
|
293
|
+
// It's a component
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Tracking Imports
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
const importMap = new Map<string, string>();
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
ImportDeclaration(node) {
|
|
306
|
+
const source = node.source.value as string;
|
|
307
|
+
for (const spec of node.specifiers) {
|
|
308
|
+
if (spec.type === "ImportSpecifier") {
|
|
309
|
+
importMap.set(spec.local.name, source);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
JSXOpeningElement(node) {
|
|
315
|
+
if (node.name.type === "JSXIdentifier") {
|
|
316
|
+
const importSource = importMap.get(node.name.name);
|
|
317
|
+
// Now you know where it was imported from
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Checking className for Tailwind Patterns
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
JSXAttribute(node) {
|
|
327
|
+
if (node.name.type !== "JSXIdentifier" || node.name.name !== "className") {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (node.value?.type === "Literal" && typeof node.value.value === "string") {
|
|
332
|
+
const classes = node.value.value;
|
|
333
|
+
// Analyze classes
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Counting React Hooks in Components
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
const componentStack: ComponentInfo[] = [];
|
|
342
|
+
|
|
343
|
+
function isComponentName(name: string): boolean {
|
|
344
|
+
return /^[A-Z]/.test(name);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function isHookCall(callee: TSESTree.Expression): string | null {
|
|
348
|
+
if (callee.type === "Identifier" && callee.name.startsWith("use")) {
|
|
349
|
+
return callee.name;
|
|
350
|
+
}
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
FunctionDeclaration(node) {
|
|
356
|
+
if (node.id && isComponentName(node.id.name)) {
|
|
357
|
+
componentStack.push({ name: node.id.name, node, count: 0 });
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
"FunctionDeclaration:exit"(node) {
|
|
361
|
+
const component = componentStack.pop();
|
|
362
|
+
if (component && component.count > threshold) {
|
|
363
|
+
context.report({ node, messageId: "excessive" });
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
CallExpression(node) {
|
|
367
|
+
const hookName = isHookCall(node.callee);
|
|
368
|
+
if (hookName && componentStack.length > 0) {
|
|
369
|
+
componentStack[componentStack.length - 1].count++;
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Cross-File Analysis
|
|
376
|
+
|
|
377
|
+
For rules that need to analyze imports across files, use the import-graph utility:
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
import { getComponentLibrary, clearCache } from "../utils/import-graph.js";
|
|
381
|
+
|
|
382
|
+
// In create():
|
|
383
|
+
"Program:exit"() {
|
|
384
|
+
const filename = context.filename || context.getFilename();
|
|
385
|
+
|
|
386
|
+
for (const usage of componentUsages) {
|
|
387
|
+
const libraryInfo = getComponentLibrary(
|
|
388
|
+
filename,
|
|
389
|
+
usage.componentName,
|
|
390
|
+
usage.importSource
|
|
391
|
+
);
|
|
392
|
+
// libraryInfo.library, libraryInfo.isLocalComponent, etc.
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
## Error Message Best Practices
|
|
398
|
+
|
|
399
|
+
1. Be specific about what's wrong
|
|
400
|
+
2. Suggest the fix
|
|
401
|
+
3. Use placeholders for dynamic content
|
|
402
|
+
|
|
403
|
+
Good:
|
|
404
|
+
```typescript
|
|
405
|
+
messages: {
|
|
406
|
+
useDesignSystem:
|
|
407
|
+
"Use <{{preferred}}> from '{{source}}' instead of native <{{element}}>.",
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Bad:
|
|
412
|
+
```typescript
|
|
413
|
+
messages: {
|
|
414
|
+
error: "Invalid element", // Too vague
|
|
415
|
+
}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
## Testing Tips
|
|
419
|
+
|
|
420
|
+
1. **Test valid cases first** - Make sure correct code passes
|
|
421
|
+
2. **Test with options** - Verify all configuration options work
|
|
422
|
+
3. **Test edge cases** - JSX member expressions, aliases, spread attributes
|
|
423
|
+
4. **Test real-world patterns** - Forms, modals, lists, etc.
|
|
424
|
+
5. **Clear cache if using cross-file analysis** - Use `beforeEach`
|
|
425
|
+
|
|
426
|
+
## Reference Files
|
|
427
|
+
|
|
428
|
+
See the following files for complete working examples:
|
|
429
|
+
|
|
430
|
+
- `references/RULE-TEMPLATE.ts` - Complete rule template
|
|
431
|
+
- `references/TEST-TEMPLATE.ts` - Complete test template
|
|
432
|
+
- `references/REGISTRY-ENTRY.md` - Registry entry format
|
|
433
|
+
|
|
434
|
+
## Final Checklist
|
|
435
|
+
|
|
436
|
+
Before finishing, verify:
|
|
437
|
+
|
|
438
|
+
- [ ] Rule file created at `packages/uilint-eslint/src/rules/{name}.ts`
|
|
439
|
+
- [ ] Test file created at `packages/uilint-eslint/src/rules/{name}.test.ts`
|
|
440
|
+
- [ ] Rule added to `packages/uilint-eslint/src/rule-registry.ts`
|
|
441
|
+
- [ ] Index regenerated with `pnpm -C packages/uilint-eslint generate:index`
|
|
442
|
+
- [ ] All tests pass with `pnpm -C packages/uilint-eslint test`
|
|
443
|
+
- [ ] Build succeeds with `pnpm -C packages/uilint-eslint build`
|
|
444
|
+
- [ ] Error messages are clear and actionable
|
|
445
|
+
- [ ] Configuration options are documented in schema
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Adding a Rule to the Registry
|
|
2
|
+
|
|
3
|
+
After creating your rule, you must register it in `packages/uilint-eslint/src/rule-registry.ts`.
|
|
4
|
+
|
|
5
|
+
## Entry Format
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
{
|
|
9
|
+
id: "rule-name", // Must match filename (without .ts)
|
|
10
|
+
name: "Human Readable Name", // Display name for CLI
|
|
11
|
+
description: "Short description", // Shown in rule selection prompts
|
|
12
|
+
defaultSeverity: "warn", // "error" | "warn" | "off"
|
|
13
|
+
defaultOptions: [{ ... }], // Default configuration
|
|
14
|
+
optionSchema: { // For interactive configuration
|
|
15
|
+
fields: [
|
|
16
|
+
{
|
|
17
|
+
key: "optionKey", // Property name in options object
|
|
18
|
+
label: "Prompt Label", // Shown in CLI prompts
|
|
19
|
+
type: "text", // "text" | "number" | "boolean" | "select" | "multiselect"
|
|
20
|
+
defaultValue: "default", // Pre-filled value
|
|
21
|
+
placeholder: "hint text", // For text/number inputs
|
|
22
|
+
options: [ // For select/multiselect
|
|
23
|
+
{ value: "a", label: "Option A" },
|
|
24
|
+
{ value: "b", label: "Option B" },
|
|
25
|
+
],
|
|
26
|
+
description: "Help text", // Explains the option
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
requiresStyleguide: false, // true if needs .uilint/styleguide.md
|
|
31
|
+
category: "static", // "static" | "semantic"
|
|
32
|
+
},
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Field Types
|
|
36
|
+
|
|
37
|
+
### text
|
|
38
|
+
For string values:
|
|
39
|
+
```typescript
|
|
40
|
+
{
|
|
41
|
+
key: "importSource",
|
|
42
|
+
label: "Import path for component",
|
|
43
|
+
type: "text",
|
|
44
|
+
defaultValue: "@/components/ui/button",
|
|
45
|
+
placeholder: "@/components/ui/button",
|
|
46
|
+
description: "The module path to import the preferred component from",
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### number
|
|
51
|
+
For numeric values:
|
|
52
|
+
```typescript
|
|
53
|
+
{
|
|
54
|
+
key: "maxCount",
|
|
55
|
+
label: "Maximum allowed count",
|
|
56
|
+
type: "number",
|
|
57
|
+
defaultValue: 3,
|
|
58
|
+
placeholder: "3",
|
|
59
|
+
description: "Maximum number of items before triggering warning",
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### boolean
|
|
64
|
+
For on/off toggles:
|
|
65
|
+
```typescript
|
|
66
|
+
{
|
|
67
|
+
key: "strict",
|
|
68
|
+
label: "Enable strict mode",
|
|
69
|
+
type: "boolean",
|
|
70
|
+
defaultValue: false,
|
|
71
|
+
description: "When enabled, also checks for edge cases",
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### select
|
|
76
|
+
For single-choice options:
|
|
77
|
+
```typescript
|
|
78
|
+
{
|
|
79
|
+
key: "preferred",
|
|
80
|
+
label: "Preferred component library",
|
|
81
|
+
type: "select",
|
|
82
|
+
defaultValue: "shadcn",
|
|
83
|
+
options: [
|
|
84
|
+
{ value: "shadcn", label: "shadcn/ui" },
|
|
85
|
+
{ value: "mui", label: "Material UI" },
|
|
86
|
+
{ value: "chakra", label: "Chakra UI" },
|
|
87
|
+
],
|
|
88
|
+
description: "The UI library to enforce",
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### multiselect
|
|
93
|
+
For multiple-choice options:
|
|
94
|
+
```typescript
|
|
95
|
+
{
|
|
96
|
+
key: "elements",
|
|
97
|
+
label: "Elements to check",
|
|
98
|
+
type: "multiselect",
|
|
99
|
+
defaultValue: ["button", "input"],
|
|
100
|
+
options: [
|
|
101
|
+
{ value: "button", label: "Button" },
|
|
102
|
+
{ value: "input", label: "Input" },
|
|
103
|
+
{ value: "select", label: "Select" },
|
|
104
|
+
{ value: "textarea", label: "Textarea" },
|
|
105
|
+
],
|
|
106
|
+
description: "HTML elements to enforce replacement for",
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Complete Example
|
|
111
|
+
|
|
112
|
+
Here's a complete registry entry for a rule that enforces design system buttons:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
{
|
|
116
|
+
id: "prefer-design-system-button",
|
|
117
|
+
name: "Prefer Design System Button",
|
|
118
|
+
description: "Enforce using Button from design system instead of native <button>",
|
|
119
|
+
defaultSeverity: "warn",
|
|
120
|
+
defaultOptions: [
|
|
121
|
+
{
|
|
122
|
+
preferred: "Button",
|
|
123
|
+
importSource: "@/components/ui/button",
|
|
124
|
+
checkSubmit: true,
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
optionSchema: {
|
|
128
|
+
fields: [
|
|
129
|
+
{
|
|
130
|
+
key: "preferred",
|
|
131
|
+
label: "Preferred component name",
|
|
132
|
+
type: "text",
|
|
133
|
+
defaultValue: "Button",
|
|
134
|
+
placeholder: "Button",
|
|
135
|
+
description: "The component to use instead of native button",
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
key: "importSource",
|
|
139
|
+
label: "Import path",
|
|
140
|
+
type: "text",
|
|
141
|
+
defaultValue: "@/components/ui/button",
|
|
142
|
+
placeholder: "@/components/ui/button",
|
|
143
|
+
description: "Where to import the component from",
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
key: "checkSubmit",
|
|
147
|
+
label: "Check submit buttons",
|
|
148
|
+
type: "boolean",
|
|
149
|
+
defaultValue: true,
|
|
150
|
+
description: "Also check button[type='submit']",
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
requiresStyleguide: false,
|
|
155
|
+
category: "static",
|
|
156
|
+
},
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## After Adding Entry
|
|
160
|
+
|
|
161
|
+
1. Run `pnpm -C packages/uilint-eslint generate:index` to regenerate the index
|
|
162
|
+
2. The rule will now appear in `uilint install` prompts
|
|
163
|
+
3. Users can configure it interactively during installation
|