uilint 0.2.1 → 0.2.4

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.4",
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.4",
47
+ "uilint-eslint": "0.2.4"
47
48
  },
48
49
  "optionalDependencies": {
49
50
  "@langfuse/client": "^4.5.1",
@@ -76,6 +77,7 @@
76
77
  "typecheck": "tsc --noEmit",
77
78
  "lint": "eslint src/",
78
79
  "test": "vitest",
80
+ "test:watch": "vitest --watch",
79
81
  "test:unit": "vitest run test/unit",
80
82
  "test:integration": "vitest run test/integration"
81
83
  }
@@ -0,0 +1,435 @@
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 TypeScript project with @typescript-eslint,
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 existing rules installed in the project for patterns:
60
+
61
+ ```bash
62
+ ls .uilint/rules/
63
+ ```
64
+
65
+ If no rules exist yet, you can reference examples from the uilint-eslint package or look at similar patterns. Common rule patterns include:
66
+
67
+ - Component analysis and hook counting
68
+ - Cross-file analysis and import tracking
69
+ - className/Tailwind pattern matching
70
+ - Attribute analysis in JSX
71
+
72
+ ### Step 3: Write the Rule
73
+
74
+ Create the rule file at `.uilint/rules/{rule-name}.ts` in the target project.
75
+
76
+ Follow this structure exactly:
77
+
78
+ ```typescript
79
+ /**
80
+ * Rule: {rule-name}
81
+ *
82
+ * {Description of what this rule does and why}
83
+ */
84
+
85
+ import { createRule } from "uilint-eslint";
86
+ import type { TSESTree } from "@typescript-eslint/utils";
87
+
88
+ type MessageIds = "{messageId1}" | "{messageId2}";
89
+ type Options = [
90
+ {
91
+ /** Option description */
92
+ optionName?: optionType;
93
+ }?
94
+ ];
95
+
96
+ export default createRule<Options, MessageIds>({
97
+ name: "{rule-name}",
98
+ meta: {
99
+ type: "problem" | "suggestion" | "layout",
100
+ docs: {
101
+ description: "{Human-readable description}",
102
+ },
103
+ messages: {
104
+ messageId1: "{Error message with {{placeholders}}}",
105
+ messageId2: "{Another message}",
106
+ },
107
+ schema: [
108
+ {
109
+ type: "object",
110
+ properties: {
111
+ optionName: {
112
+ type: "string",
113
+ description: "Option description",
114
+ },
115
+ },
116
+ additionalProperties: false,
117
+ },
118
+ ],
119
+ },
120
+ defaultOptions: [{ optionName: "default" }],
121
+ create(context) {
122
+ const options = context.options[0] || {};
123
+ // Use options with defaults
124
+ const optionName = options.optionName ?? "default";
125
+
126
+ return {
127
+ // AST visitor methods
128
+ JSXOpeningElement(node) {
129
+ // Analyze JSX elements
130
+ },
131
+ ImportDeclaration(node) {
132
+ // Track imports
133
+ },
134
+ CallExpression(node) {
135
+ // Analyze function calls like hooks
136
+ },
137
+ "Program:exit"() {
138
+ // Final analysis after parsing entire file
139
+ },
140
+ };
141
+ },
142
+ });
143
+ ```
144
+
145
+ ### Step 4: Write Comprehensive Tests
146
+
147
+ Create the test file at `.uilint/rules/{rule-name}.test.ts` in the target project.
148
+
149
+ Follow this structure:
150
+
151
+ ```typescript
152
+ /**
153
+ * Tests for: {rule-name}
154
+ *
155
+ * {Description}
156
+ */
157
+
158
+ import { RuleTester } from "@typescript-eslint/rule-tester";
159
+ import { describe, it, afterAll, beforeEach } from "vitest";
160
+ import rule from "./{rule-name}";
161
+ // If rule uses caching:
162
+ // import { clearCache } from "../utils/import-graph.js";
163
+
164
+ RuleTester.afterAll = afterAll;
165
+ RuleTester.describe = describe;
166
+ RuleTester.it = it;
167
+
168
+ const ruleTester = new RuleTester({
169
+ languageOptions: {
170
+ ecmaVersion: 2022,
171
+ sourceType: "module",
172
+ parserOptions: {
173
+ ecmaFeatures: { jsx: true },
174
+ },
175
+ },
176
+ });
177
+
178
+ // Clear cache between tests if needed
179
+ // beforeEach(() => {
180
+ // clearCache();
181
+ // });
182
+
183
+ ruleTester.run("{rule-name}", rule, {
184
+ valid: [
185
+ // ============================================
186
+ // PREFERRED PATTERN USED CORRECTLY
187
+ // ============================================
188
+ {
189
+ name: "uses preferred pattern",
190
+ code: `
191
+ // Valid code example
192
+ `,
193
+ },
194
+ {
195
+ name: "with custom options",
196
+ code: `...`,
197
+ options: [{ optionName: "custom" }],
198
+ },
199
+
200
+ // ============================================
201
+ // EXCEPTIONS / EDGE CASES
202
+ // ============================================
203
+ {
204
+ name: "exception case is allowed",
205
+ code: `...`,
206
+ },
207
+ ],
208
+
209
+ invalid: [
210
+ // ============================================
211
+ // BASIC VIOLATIONS
212
+ // ============================================
213
+ {
214
+ name: "violates pattern",
215
+ code: `
216
+ // Invalid code example
217
+ `,
218
+ errors: [
219
+ {
220
+ messageId: "messageId1",
221
+ data: { key: "value" },
222
+ },
223
+ ],
224
+ },
225
+
226
+ // ============================================
227
+ // WITH OPTIONS
228
+ // ============================================
229
+ {
230
+ name: "violates with custom options",
231
+ code: `...`,
232
+ options: [{ optionName: "strict" }],
233
+ errors: [
234
+ {
235
+ messageId: "messageId2",
236
+ },
237
+ ],
238
+ },
239
+ ],
240
+ });
241
+ ```
242
+
243
+ ### Step 5: Configure ESLint to Use the Rule
244
+
245
+ The rule will be automatically configured in your `eslint.config.{js,ts,mjs,cjs}` file. The installer creates a custom plugin that references your local rules:
246
+
247
+ ```javascript
248
+ import { createRule } from "uilint-eslint";
249
+ import yourRuleName from "./.uilint/rules/your-rule-name.js";
250
+
251
+ export default [
252
+ // ... existing config
253
+ {
254
+ plugins: {
255
+ "uilint-custom": {
256
+ rules: {
257
+ "your-rule-name": yourRuleName,
258
+ },
259
+ },
260
+ },
261
+ rules: {
262
+ "uilint-custom/your-rule-name": "error",
263
+ },
264
+ },
265
+ ];
266
+ ```
267
+
268
+ If you're creating the rule manually (not via the installer), you'll need to add the import and configure it in your ESLint config.
269
+
270
+ ## Common AST Patterns
271
+
272
+ ### Detecting JSX Elements
273
+
274
+ ```typescript
275
+ JSXOpeningElement(node) {
276
+ // Get element name
277
+ if (node.name.type === "JSXIdentifier") {
278
+ const name = node.name.name;
279
+ // Check if it's a component (PascalCase) vs HTML (lowercase)
280
+ if (/^[A-Z]/.test(name)) {
281
+ // It's a component
282
+ }
283
+ }
284
+ }
285
+ ```
286
+
287
+ ### Tracking Imports
288
+
289
+ ```typescript
290
+ const importMap = new Map<string, string>();
291
+
292
+ return {
293
+ ImportDeclaration(node) {
294
+ const source = node.source.value as string;
295
+ for (const spec of node.specifiers) {
296
+ if (spec.type === "ImportSpecifier") {
297
+ importMap.set(spec.local.name, source);
298
+ }
299
+ }
300
+ },
301
+
302
+ JSXOpeningElement(node) {
303
+ if (node.name.type === "JSXIdentifier") {
304
+ const importSource = importMap.get(node.name.name);
305
+ // Now you know where it was imported from
306
+ }
307
+ },
308
+ };
309
+ ```
310
+
311
+ ### Checking className for Tailwind Patterns
312
+
313
+ ```typescript
314
+ JSXAttribute(node) {
315
+ if (node.name.type !== "JSXIdentifier" || node.name.name !== "className") {
316
+ return;
317
+ }
318
+
319
+ if (node.value?.type === "Literal" && typeof node.value.value === "string") {
320
+ const classes = node.value.value;
321
+ // Analyze classes
322
+ }
323
+ }
324
+ ```
325
+
326
+ ### Counting React Hooks in Components
327
+
328
+ ```typescript
329
+ const componentStack: ComponentInfo[] = [];
330
+
331
+ function isComponentName(name: string): boolean {
332
+ return /^[A-Z]/.test(name);
333
+ }
334
+
335
+ function isHookCall(callee: TSESTree.Expression): string | null {
336
+ if (callee.type === "Identifier" && callee.name.startsWith("use")) {
337
+ return callee.name;
338
+ }
339
+ return null;
340
+ }
341
+
342
+ return {
343
+ FunctionDeclaration(node) {
344
+ if (node.id && isComponentName(node.id.name)) {
345
+ componentStack.push({ name: node.id.name, node, count: 0 });
346
+ }
347
+ },
348
+ "FunctionDeclaration:exit"(node) {
349
+ const component = componentStack.pop();
350
+ if (component && component.count > threshold) {
351
+ context.report({ node, messageId: "excessive" });
352
+ }
353
+ },
354
+ CallExpression(node) {
355
+ const hookName = isHookCall(node.callee);
356
+ if (hookName && componentStack.length > 0) {
357
+ componentStack[componentStack.length - 1].count++;
358
+ }
359
+ },
360
+ };
361
+ ```
362
+
363
+ ### Cross-File Analysis
364
+
365
+ For rules that need to analyze imports across files, use the import-graph utility:
366
+
367
+ ```typescript
368
+ import { getComponentLibrary, clearCache } from "../utils/import-graph.js";
369
+
370
+ // In create():
371
+ "Program:exit"() {
372
+ const filename = context.filename || context.getFilename();
373
+
374
+ for (const usage of componentUsages) {
375
+ const libraryInfo = getComponentLibrary(
376
+ filename,
377
+ usage.componentName,
378
+ usage.importSource
379
+ );
380
+ // libraryInfo.library, libraryInfo.isLocalComponent, etc.
381
+ }
382
+ }
383
+ ```
384
+
385
+ ## Error Message Best Practices
386
+
387
+ 1. Be specific about what's wrong
388
+ 2. Suggest the fix
389
+ 3. Use placeholders for dynamic content
390
+
391
+ Good:
392
+
393
+ ```typescript
394
+ messages: {
395
+ useDesignSystem:
396
+ "Use <{{preferred}}> from '{{source}}' instead of native <{{element}}>.",
397
+ }
398
+ ```
399
+
400
+ Bad:
401
+
402
+ ```typescript
403
+ messages: {
404
+ error: "Invalid element", // Too vague
405
+ }
406
+ ```
407
+
408
+ ## Testing Tips
409
+
410
+ 1. **Test valid cases first** - Make sure correct code passes
411
+ 2. **Test with options** - Verify all configuration options work
412
+ 3. **Test edge cases** - JSX member expressions, aliases, spread attributes
413
+ 4. **Test real-world patterns** - Forms, modals, lists, etc.
414
+ 5. **Clear cache if using cross-file analysis** - Use `beforeEach`
415
+
416
+ ## Reference Files
417
+
418
+ See the following files for complete working examples:
419
+
420
+ - `references/RULE-TEMPLATE.ts` - Complete rule template
421
+ - `references/TEST-TEMPLATE.ts` - Complete test template
422
+ - `references/REGISTRY-ENTRY.md` - Registry entry format
423
+
424
+ ## Final Checklist
425
+
426
+ Before finishing, verify:
427
+
428
+ - [ ] Rule file created at `.uilint/rules/{name}.ts`
429
+ - [ ] Test file created at `.uilint/rules/{name}.test.ts` (if applicable)
430
+ - [ ] Rule imports `createRule` from `"uilint-eslint"`
431
+ - [ ] ESLint config updated to import and use the rule from `.uilint/rules/`
432
+ - [ ] Rule configured in ESLint with appropriate severity
433
+ - [ ] Error messages are clear and actionable
434
+ - [ ] Configuration options are documented in schema
435
+ - [ ] Rule works correctly when ESLint runs
@@ -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