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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uilint",
3
- "version": "0.2.1",
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
- "uilint-core": "^0.2.1",
45
- "uilint-eslint": "^0.2.1",
46
- "ws": "^8.18.0"
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