uilint-eslint 0.2.76 → 0.2.77

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/rules/semantic-vision.ts","../../src/utils/create-rule.ts"],"sourcesContent":["/**\n * Rule: semantic-vision\n *\n * ESLint rule that reports cached vision analysis results for UI elements.\n * Vision analysis is performed by the UILint browser overlay and results are\n * cached in .uilint/screenshots/{timestamp}-{route}.json files.\n *\n * This rule:\n * 1. Finds cached vision analysis results for the current file\n * 2. Reports any issues that match elements in this file (by data-loc)\n * 3. If no cached results exist, silently passes (analysis is triggered by browser)\n */\n\nimport { existsSync, readdirSync, readFileSync } from \"fs\";\nimport { dirname, join, relative } from \"path\";\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\n\ntype MessageIds = \"visionIssue\" | \"analysisStale\";\n\ntype Options = [\n {\n /** Maximum age of cached results in milliseconds (default: 1 hour) */\n maxAgeMs?: number;\n /** Path to screenshots directory (default: .uilint/screenshots) */\n screenshotsPath?: string;\n }\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"semantic-vision\",\n version: \"1.0.0\",\n name: \"Vision Analysis\",\n description: \"Report cached vision analysis results from UILint browser overlay\",\n defaultSeverity: \"warn\",\n category: \"semantic\",\n icon: \"👁️\",\n hint: \"Vision AI for rendered UI\",\n defaultEnabled: false,\n requiresStyleguide: false,\n plugin: \"vision\",\n customInspector: \"vision-issue\",\n heatmapColor: \"#8B5CF6\",\n postInstallInstructions: \"Add the UILint browser overlay to your app and run analysis from the browser to generate cached results.\",\n defaultOptions: [{ maxAgeMs: 3600000, screenshotsPath: \".uilint/screenshots\" }],\n optionSchema: {\n fields: [\n {\n key: \"maxAgeMs\",\n label: \"Max cache age (milliseconds)\",\n type: \"number\",\n defaultValue: 3600000,\n placeholder: \"3600000\",\n description: \"Maximum age of cached results in milliseconds (default: 1 hour)\",\n },\n {\n key: \"screenshotsPath\",\n label: \"Screenshots directory path\",\n type: \"text\",\n defaultValue: \".uilint/screenshots\",\n placeholder: \".uilint/screenshots\",\n description: \"Relative path to the screenshots directory containing analysis results\",\n },\n ],\n },\n docs: `\n## What it does\n\nReports UI issues found by the UILint browser overlay's vision analysis. The overlay\ncaptures screenshots and analyzes them using vision AI, then caches the results.\nThis ESLint rule reads those cached results and reports them as linting errors.\n\n## How it works\n\n1. **Browser overlay**: When running your dev server with the UILint overlay, it captures\n screenshots and analyzes them using vision AI\n2. **Results cached**: Analysis results are saved to \\`.uilint/screenshots/*.json\\`\n3. **ESLint reports**: This rule reads cached results and reports issues at the correct\n source locations using \\`data-loc\\` attributes\n\n## Why it's useful\n\n- **Visual issues**: Catches problems that can only be seen in rendered UI\n- **Continuous feedback**: Issues appear in your editor as you develop\n- **No manual review**: AI spots spacing, alignment, and consistency issues automatically\n\n## Prerequisites\n\n1. **UILint overlay installed**: Add the overlay component to your app\n2. **Run analysis**: Load pages in the browser with the overlay active\n3. **Results cached**: Wait for analysis to complete and cache results\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/semantic-vision\": [\"warn\", {\n maxAgeMs: 3600000, // Ignore results older than 1 hour\n screenshotsPath: \".uilint/screenshots\" // Where cached results are stored\n}]\n\\`\\`\\`\n\n## Notes\n\n- If no cached results exist, the rule passes silently\n- Results are matched to source files using \\`data-loc\\` attributes\n- Stale results (older than \\`maxAgeMs\\`) are reported as warnings\n- Run the browser overlay to refresh cached analysis\n`,\n});\n\n/**\n * Vision analysis result structure stored in JSON files\n */\ninterface VisionAnalysisResult {\n /** Timestamp when analysis was performed */\n timestamp: number;\n /** Route that was analyzed (e.g., \"/\", \"/profile\") */\n route: string;\n /** Screenshot filename (for reference) */\n screenshotFile?: string;\n /** Issues found by vision analysis */\n issues: VisionIssue[];\n}\n\n/**\n * Individual issue from vision analysis\n */\ninterface VisionIssue {\n /** Element text that the LLM referenced */\n elementText?: string;\n /** data-loc reference (format: \"path:line:column\") */\n dataLoc?: string;\n /** Human-readable description of the issue */\n message: string;\n /** Issue category */\n category?: \"spacing\" | \"color\" | \"typography\" | \"alignment\" | \"accessibility\" | \"layout\" | \"other\";\n /** Severity level */\n severity?: \"error\" | \"warning\" | \"info\";\n}\n\n/**\n * Find project root by looking for package.json\n */\nfunction findProjectRoot(startDir: string): string {\n let dir = startDir;\n for (let i = 0; i < 20; i++) {\n if (existsSync(join(dir, \"package.json\"))) {\n return dir;\n }\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return startDir;\n}\n\n/**\n * Get all vision analysis result files from screenshots directory\n */\nfunction getVisionResultFiles(screenshotsDir: string): string[] {\n if (!existsSync(screenshotsDir)) {\n return [];\n }\n\n try {\n const files = readdirSync(screenshotsDir);\n return files\n .filter((f) => f.endsWith(\".json\"))\n .map((f) => join(screenshotsDir, f))\n .sort()\n .reverse(); // Most recent first\n } catch {\n return [];\n }\n}\n\n/**\n * Load and parse a vision analysis result file\n */\nfunction loadVisionResult(filePath: string): VisionAnalysisResult | null {\n try {\n const content = readFileSync(filePath, \"utf-8\");\n return JSON.parse(content) as VisionAnalysisResult;\n } catch {\n return null;\n }\n}\n\n/**\n * Parse a data-loc string into file path and location\n * Format: \"path/to/file.tsx:line:column\"\n */\nfunction parseDataLoc(dataLoc: string): { filePath: string; line: number; column: number } | null {\n // Match pattern: path:line:column (line and column are numbers)\n const match = dataLoc.match(/^(.+):(\\d+):(\\d+)$/);\n if (!match) return null;\n\n return {\n filePath: match[1]!,\n line: parseInt(match[2]!, 10),\n column: parseInt(match[3]!, 10),\n };\n}\n\n/**\n * Normalize file path for comparison (handle relative vs absolute paths)\n */\nfunction normalizeFilePath(filePath: string, projectRoot: string): string {\n // If it's already a relative path, return as-is\n if (!filePath.startsWith(\"/\")) {\n return filePath;\n }\n // Convert absolute to relative\n return relative(projectRoot, filePath);\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"semantic-vision\",\n meta: {\n type: \"suggestion\",\n docs: {\n description:\n \"Report cached vision analysis results from UILint browser overlay\",\n },\n messages: {\n visionIssue: \"[Vision] {{message}}\",\n analysisStale:\n \"Vision analysis results are stale (older than {{age}}). Re-run analysis in browser.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n maxAgeMs: {\n type: \"number\",\n description:\n \"Maximum age of cached results in milliseconds (default: 1 hour)\",\n },\n screenshotsPath: {\n type: \"string\",\n description:\n \"Path to screenshots directory (default: .uilint/screenshots)\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{ maxAgeMs: 60 * 60 * 1000 }], // 1 hour default\n create(context) {\n const options = context.options[0] || {};\n const maxAgeMs = options.maxAgeMs ?? 60 * 60 * 1000;\n const filePath = context.filename;\n const fileDir = dirname(filePath);\n\n // Find project root and screenshots directory\n const projectRoot = findProjectRoot(fileDir);\n const screenshotsDir = options.screenshotsPath\n ? join(projectRoot, options.screenshotsPath)\n : join(projectRoot, \".uilint\", \"screenshots\");\n\n // Get the relative path of the current file for matching against data-loc\n const relativeFilePath = normalizeFilePath(filePath, projectRoot);\n\n // Find all vision result files\n const resultFiles = getVisionResultFiles(screenshotsDir);\n if (resultFiles.length === 0) {\n // No cached results - silently pass (analysis happens in browser)\n return {};\n }\n\n // Collect issues that match this file from all recent results\n const matchingIssues: Array<{\n issue: VisionIssue;\n line: number;\n column: number;\n isStale: boolean;\n }> = [];\n\n const now = Date.now();\n\n for (const resultFile of resultFiles) {\n const result = loadVisionResult(resultFile);\n if (!result || !result.issues) continue;\n\n const isStale = now - result.timestamp > maxAgeMs;\n\n for (const issue of result.issues) {\n if (!issue.dataLoc) continue;\n\n const parsed = parseDataLoc(issue.dataLoc);\n if (!parsed) continue;\n\n // Check if this issue is for the current file\n const issueFilePath = normalizeFilePath(parsed.filePath, projectRoot);\n if (issueFilePath === relativeFilePath) {\n matchingIssues.push({\n issue,\n line: parsed.line,\n column: parsed.column,\n isStale,\n });\n }\n }\n }\n\n // De-duplicate issues by line:column:message\n const seenIssues = new Set<string>();\n const uniqueIssues = matchingIssues.filter((item) => {\n const key = `${item.line}:${item.column}:${item.issue.message}`;\n if (seenIssues.has(key)) return false;\n seenIssues.add(key);\n return true;\n });\n\n return {\n Program(node) {\n for (const { issue, line, column, isStale } of uniqueIssues) {\n // Build message with category prefix if available\n const categoryPrefix = issue.category\n ? `[${issue.category}] `\n : \"\";\n const message = `${categoryPrefix}${issue.message}`;\n\n // Report stale warning separately if enabled\n if (isStale) {\n const ageHours = Math.round(maxAgeMs / (60 * 60 * 1000));\n context.report({\n node,\n loc: { line, column },\n messageId: \"analysisStale\",\n data: { age: `${ageHours}h` },\n });\n }\n\n context.report({\n node,\n loc: { line, column },\n messageId: \"visionIssue\",\n data: { message },\n });\n }\n },\n };\n },\n});\n","/**\n * Rule creation helper using @typescript-eslint/utils\n */\n\nimport { ESLintUtils } from \"@typescript-eslint/utils\";\n\nexport const createRule = ESLintUtils.RuleCreator(\n (name) =>\n `https://github.com/peter-suggate/uilint/blob/main/packages/uilint-eslint/docs/rules/${name}.md`\n);\n\n/**\n * Schema for prompting user to configure a rule option in the CLI\n */\nexport interface OptionFieldSchema {\n /** Field name in the options object */\n key: string;\n /** Display label for the prompt */\n label: string;\n /** Prompt type */\n type: \"text\" | \"number\" | \"boolean\" | \"select\" | \"multiselect\";\n /** Default value */\n defaultValue: unknown;\n /** Placeholder text (for text/number inputs) */\n placeholder?: string;\n /** Options for select/multiselect */\n options?: Array<{ value: string | number; label: string }>;\n /** Description/hint for the field */\n description?: string;\n}\n\n/**\n * Schema describing how to prompt for rule options during installation\n */\nexport interface RuleOptionSchema {\n /** Fields that can be configured for this rule */\n fields: OptionFieldSchema[];\n}\n\n/**\n * External requirement that a rule needs to function\n */\nexport interface RuleRequirement {\n /** Requirement type for programmatic checks */\n type: \"ollama\" | \"git\" | \"coverage\" | \"semantic-index\" | \"styleguide\";\n /** Human-readable description */\n description: string;\n /** Optional: how to satisfy the requirement */\n setupHint?: string;\n}\n\n/**\n * Rule migration definition for updating rule options between versions\n */\nexport interface RuleMigration {\n /** Source version (semver) */\n from: string;\n /** Target version (semver) */\n to: string;\n /** Human-readable description of what changed */\n description: string;\n /** Function to migrate options from old format to new format */\n migrate: (oldOptions: unknown[]) => unknown[];\n /** Whether this migration contains breaking changes */\n breaking?: boolean;\n}\n\n/**\n * Colocated rule metadata - exported alongside each rule\n *\n * This structure keeps all rule metadata in the same file as the rule implementation,\n * making it easy to maintain and extend as new rules are added.\n */\nexport interface RuleMeta {\n /** Rule identifier (e.g., \"consistent-dark-mode\") - must match filename */\n id: string;\n\n /** Semantic version of the rule (e.g., \"1.0.0\") */\n version: string;\n\n /** Display name for CLI (e.g., \"No Arbitrary Tailwind\") */\n name: string;\n\n /** Short description for CLI selection prompts (one line) */\n description: string;\n\n /** Default severity level */\n defaultSeverity: \"error\" | \"warn\" | \"off\";\n\n /** Category for grouping in CLI */\n category: \"static\" | \"semantic\";\n\n /** Icon for display in CLI/UI (emoji or icon name) */\n icon?: string;\n\n /** Short hint about the rule type/requirements */\n hint?: string;\n\n /** Whether rule is enabled by default during install */\n defaultEnabled?: boolean;\n\n /** External requirements the rule needs */\n requirements?: RuleRequirement[];\n\n /** Instructions to show after installation */\n postInstallInstructions?: string;\n\n /** Framework compatibility */\n frameworks?: (\"next\" | \"vite\" | \"cra\" | \"remix\")[];\n\n /** Whether this rule requires a styleguide file */\n requiresStyleguide?: boolean;\n\n /** Default options for the rule (passed as second element in ESLint config) */\n defaultOptions?: unknown[];\n\n /** Schema for prompting user to configure options during install */\n optionSchema?: RuleOptionSchema;\n\n /**\n * Detailed documentation in markdown format.\n * Should include:\n * - What the rule does\n * - Why it's useful\n * - Examples of incorrect and correct code\n * - Configuration options explained\n */\n docs: string;\n\n /**\n * Internal utility dependencies that this rule requires.\n * When the rule is copied to a target project, these utilities\n * will be transformed to import from \"uilint-eslint\" instead\n * of relative paths.\n *\n * Example: [\"coverage-aggregator\", \"dependency-graph\"]\n */\n internalDependencies?: string[];\n\n /**\n * Whether this rule is directory-based (has lib/ folder with utilities).\n * Directory-based rules are installed as folders with index.ts and lib/ subdirectory.\n * Single-file rules are installed as single .ts files.\n *\n * When true, ESLint config imports will use:\n * ./.uilint/rules/rule-id/index.js\n * When false (default):\n * ./.uilint/rules/rule-id.js\n */\n isDirectoryBased?: boolean;\n\n /**\n * Migrations for updating rule options between versions.\n * Migrations are applied in order to transform options from older versions.\n */\n migrations?: RuleMigration[];\n\n /**\n * Which UI plugin should handle this rule.\n * Defaults based on category:\n * - \"static\" category → \"eslint\" plugin\n * - \"semantic\" category → \"semantic\" plugin\n *\n * Special cases:\n * - \"vision\" for semantic-vision rule\n */\n plugin?: \"eslint\" | \"vision\" | \"semantic\";\n\n /**\n * Custom inspector panel ID to use for this rule's issues.\n * If not specified, uses the plugin's default issue inspector.\n *\n * Examples:\n * - \"vision-issue\" for VisionIssueInspector\n * - \"duplicates\" for DuplicatesInspector\n * - \"semantic-issue\" for SemanticIssueInspector\n */\n customInspector?: string;\n\n /**\n * Custom heatmap color for this rule's issues.\n * CSS color value (hex, rgb, hsl, or named color).\n * If not specified, uses severity-based coloring.\n */\n heatmapColor?: string;\n}\n\n/**\n * Helper to define rule metadata with type safety\n */\nexport function defineRuleMeta(meta: RuleMeta): RuleMeta {\n return meta;\n}\n"],"mappings":";AAaA,SAAS,YAAY,aAAa,oBAAoB;AACtD,SAAS,SAAS,MAAM,gBAAgB;;;ACVxC,SAAS,mBAAmB;AAErB,IAAM,aAAa,YAAY;AAAA,EACpC,CAAC,SACC,uFAAuF,IAAI;AAC/F;AAqLO,SAAS,eAAeA,OAA0B;AACvD,SAAOA;AACT;;;ADjKO,IAAM,OAAO,eAAe;AAAA,EACjC,IAAI;AAAA,EACJ,SAAS;AAAA,EACT,MAAM;AAAA,EACN,aAAa;AAAA,EACb,iBAAiB;AAAA,EACjB,UAAU;AAAA,EACV,MAAM;AAAA,EACN,MAAM;AAAA,EACN,gBAAgB;AAAA,EAChB,oBAAoB;AAAA,EACpB,QAAQ;AAAA,EACR,iBAAiB;AAAA,EACjB,cAAc;AAAA,EACd,yBAAyB;AAAA,EACzB,gBAAgB,CAAC,EAAE,UAAU,MAAS,iBAAiB,sBAAsB,CAAC;AAAA,EAC9E,cAAc;AAAA,IACZ,QAAQ;AAAA,MACN;AAAA,QACE,KAAK;AAAA,QACL,OAAO;AAAA,QACP,MAAM;AAAA,QACN,cAAc;AAAA,QACd,aAAa;AAAA,QACb,aAAa;AAAA,MACf;AAAA,MACA;AAAA,QACE,KAAK;AAAA,QACL,OAAO;AAAA,QACP,MAAM;AAAA,QACN,cAAc;AAAA,QACd,aAAa;AAAA,QACb,aAAa;AAAA,MACf;AAAA,IACF;AAAA,EACF;AAAA,EACA,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4CR,CAAC;AAmCD,SAAS,gBAAgB,UAA0B;AACjD,MAAI,MAAM;AACV,WAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAC3B,QAAI,WAAW,KAAK,KAAK,cAAc,CAAC,GAAG;AACzC,aAAO;AAAA,IACT;AACA,UAAM,SAAS,QAAQ,GAAG;AAC1B,QAAI,WAAW,IAAK;AACpB,UAAM;AAAA,EACR;AACA,SAAO;AACT;AAKA,SAAS,qBAAqB,gBAAkC;AAC9D,MAAI,CAAC,WAAW,cAAc,GAAG;AAC/B,WAAO,CAAC;AAAA,EACV;AAEA,MAAI;AACF,UAAM,QAAQ,YAAY,cAAc;AACxC,WAAO,MACJ,OAAO,CAAC,MAAM,EAAE,SAAS,OAAO,CAAC,EACjC,IAAI,CAAC,MAAM,KAAK,gBAAgB,CAAC,CAAC,EAClC,KAAK,EACL,QAAQ;AAAA,EACb,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAKA,SAAS,iBAAiB,UAA+C;AACvE,MAAI;AACF,UAAM,UAAU,aAAa,UAAU,OAAO;AAC9C,WAAO,KAAK,MAAM,OAAO;AAAA,EAC3B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMA,SAAS,aAAa,SAA4E;AAEhG,QAAM,QAAQ,QAAQ,MAAM,oBAAoB;AAChD,MAAI,CAAC,MAAO,QAAO;AAEnB,SAAO;AAAA,IACL,UAAU,MAAM,CAAC;AAAA,IACjB,MAAM,SAAS,MAAM,CAAC,GAAI,EAAE;AAAA,IAC5B,QAAQ,SAAS,MAAM,CAAC,GAAI,EAAE;AAAA,EAChC;AACF;AAKA,SAAS,kBAAkB,UAAkB,aAA6B;AAExE,MAAI,CAAC,SAAS,WAAW,GAAG,GAAG;AAC7B,WAAO;AAAA,EACT;AAEA,SAAO,SAAS,aAAa,QAAQ;AACvC;AAEA,IAAO,0BAAQ,WAAgC;AAAA,EAC7C,MAAM;AAAA,EACN,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aACE;AAAA,IACJ;AAAA,IACA,UAAU;AAAA,MACR,aAAa;AAAA,MACb,eACE;AAAA,IACJ;AAAA,IACA,QAAQ;AAAA,MACN;AAAA,QACE,MAAM;AAAA,QACN,YAAY;AAAA,UACV,UAAU;AAAA,YACR,MAAM;AAAA,YACN,aACE;AAAA,UACJ;AAAA,UACA,iBAAiB;AAAA,YACf,MAAM;AAAA,YACN,aACE;AAAA,UACJ;AAAA,QACF;AAAA,QACA,sBAAsB;AAAA,MACxB;AAAA,IACF;AAAA,EACF;AAAA,EACA,gBAAgB,CAAC,EAAE,UAAU,KAAK,KAAK,IAAK,CAAC;AAAA;AAAA,EAC7C,OAAO,SAAS;AACd,UAAM,UAAU,QAAQ,QAAQ,CAAC,KAAK,CAAC;AACvC,UAAM,WAAW,QAAQ,YAAY,KAAK,KAAK;AAC/C,UAAM,WAAW,QAAQ;AACzB,UAAM,UAAU,QAAQ,QAAQ;AAGhC,UAAM,cAAc,gBAAgB,OAAO;AAC3C,UAAM,iBAAiB,QAAQ,kBAC3B,KAAK,aAAa,QAAQ,eAAe,IACzC,KAAK,aAAa,WAAW,aAAa;AAG9C,UAAM,mBAAmB,kBAAkB,UAAU,WAAW;AAGhE,UAAM,cAAc,qBAAqB,cAAc;AACvD,QAAI,YAAY,WAAW,GAAG;AAE5B,aAAO,CAAC;AAAA,IACV;AAGA,UAAM,iBAKD,CAAC;AAEN,UAAM,MAAM,KAAK,IAAI;AAErB,eAAW,cAAc,aAAa;AACpC,YAAM,SAAS,iBAAiB,UAAU;AAC1C,UAAI,CAAC,UAAU,CAAC,OAAO,OAAQ;AAE/B,YAAM,UAAU,MAAM,OAAO,YAAY;AAEzC,iBAAW,SAAS,OAAO,QAAQ;AACjC,YAAI,CAAC,MAAM,QAAS;AAEpB,cAAM,SAAS,aAAa,MAAM,OAAO;AACzC,YAAI,CAAC,OAAQ;AAGb,cAAM,gBAAgB,kBAAkB,OAAO,UAAU,WAAW;AACpE,YAAI,kBAAkB,kBAAkB;AACtC,yBAAe,KAAK;AAAA,YAClB;AAAA,YACA,MAAM,OAAO;AAAA,YACb,QAAQ,OAAO;AAAA,YACf;AAAA,UACF,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAGA,UAAM,aAAa,oBAAI,IAAY;AACnC,UAAM,eAAe,eAAe,OAAO,CAAC,SAAS;AACnD,YAAM,MAAM,GAAG,KAAK,IAAI,IAAI,KAAK,MAAM,IAAI,KAAK,MAAM,OAAO;AAC7D,UAAI,WAAW,IAAI,GAAG,EAAG,QAAO;AAChC,iBAAW,IAAI,GAAG;AAClB,aAAO;AAAA,IACT,CAAC;AAED,WAAO;AAAA,MACL,QAAQ,MAAM;AACZ,mBAAW,EAAE,OAAO,MAAM,QAAQ,QAAQ,KAAK,cAAc;AAE3D,gBAAM,iBAAiB,MAAM,WACzB,IAAI,MAAM,QAAQ,OAClB;AACJ,gBAAM,UAAU,GAAG,cAAc,GAAG,MAAM,OAAO;AAGjD,cAAI,SAAS;AACX,kBAAM,WAAW,KAAK,MAAM,YAAY,KAAK,KAAK,IAAK;AACvD,oBAAQ,OAAO;AAAA,cACb;AAAA,cACA,KAAK,EAAE,MAAM,OAAO;AAAA,cACpB,WAAW;AAAA,cACX,MAAM,EAAE,KAAK,GAAG,QAAQ,IAAI;AAAA,YAC9B,CAAC;AAAA,UACH;AAEA,kBAAQ,OAAO;AAAA,YACb;AAAA,YACA,KAAK,EAAE,MAAM,OAAO;AAAA,YACpB,WAAW;AAAA,YACX,MAAM,EAAE,QAAQ;AAAA,UAClB,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF,CAAC;","names":["meta"]}
1
+ {"version":3,"sources":["../../src/rules/semantic-vision.ts","../../src/utils/create-rule.ts"],"sourcesContent":["/**\n * Rule: semantic-vision\n *\n * ESLint rule that reports cached vision analysis results for UI elements.\n * Vision analysis is performed by the UILint browser overlay and results are\n * cached in .uilint/screenshots/{timestamp}-{route}.json files.\n *\n * This rule:\n * 1. Finds cached vision analysis results for the current file\n * 2. Reports any issues that match elements in this file (by data-loc)\n * 3. If no cached results exist, silently passes (analysis is triggered by browser)\n */\n\nimport { existsSync, readdirSync, readFileSync } from \"fs\";\nimport { dirname, join, relative } from \"path\";\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\n\ntype MessageIds = \"visionIssue\" | \"analysisStale\";\n\ntype Options = [\n {\n /** Maximum age of cached results in milliseconds (default: 1 hour) */\n maxAgeMs?: number;\n /** Path to screenshots directory (default: .uilint/screenshots) */\n screenshotsPath?: string;\n }\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"semantic-vision\",\n version: \"1.0.0\",\n name: \"Vision Analysis\",\n description: \"Report cached vision analysis results from UILint browser overlay\",\n defaultSeverity: \"warn\",\n category: \"semantic\",\n icon: \"👁️\",\n hint: \"Vision AI for rendered UI\",\n defaultEnabled: false,\n requiresStyleguide: false,\n plugin: \"vision\",\n customInspector: \"vision-issue\",\n heatmapColor: \"#8B5CF6\",\n postInstallInstructions: \"Add the UILint browser overlay to your app and run analysis from the browser to generate cached results.\",\n defaultOptions: [{ maxAgeMs: 3600000, screenshotsPath: \".uilint/screenshots\" }],\n optionSchema: {\n fields: [\n {\n key: \"maxAgeMs\",\n label: \"Max cache age (milliseconds)\",\n type: \"number\",\n defaultValue: 3600000,\n placeholder: \"3600000\",\n description: \"Maximum age of cached results in milliseconds (default: 1 hour)\",\n },\n {\n key: \"screenshotsPath\",\n label: \"Screenshots directory path\",\n type: \"text\",\n defaultValue: \".uilint/screenshots\",\n placeholder: \".uilint/screenshots\",\n description: \"Relative path to the screenshots directory containing analysis results\",\n },\n ],\n },\n docs: `\n## What it does\n\nReports UI issues found by the UILint browser overlay's vision analysis. The overlay\ncaptures screenshots and analyzes them using vision AI, then caches the results.\nThis ESLint rule reads those cached results and reports them as linting errors.\n\n## How it works\n\n1. **Browser overlay**: When running your dev server with the UILint overlay, it captures\n screenshots and analyzes them using vision AI\n2. **Results cached**: Analysis results are saved to \\`.uilint/screenshots/*.json\\`\n3. **ESLint reports**: This rule reads cached results and reports issues at the correct\n source locations using \\`data-loc\\` attributes\n\n## Why it's useful\n\n- **Visual issues**: Catches problems that can only be seen in rendered UI\n- **Continuous feedback**: Issues appear in your editor as you develop\n- **No manual review**: AI spots spacing, alignment, and consistency issues automatically\n\n## Prerequisites\n\n1. **UILint overlay installed**: Add the overlay component to your app\n2. **Run analysis**: Load pages in the browser with the overlay active\n3. **Results cached**: Wait for analysis to complete and cache results\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/semantic-vision\": [\"warn\", {\n maxAgeMs: 3600000, // Ignore results older than 1 hour\n screenshotsPath: \".uilint/screenshots\" // Where cached results are stored\n}]\n\\`\\`\\`\n\n## Notes\n\n- If no cached results exist, the rule passes silently\n- Results are matched to source files using \\`data-loc\\` attributes\n- Stale results (older than \\`maxAgeMs\\`) are reported as warnings\n- Run the browser overlay to refresh cached analysis\n`,\n});\n\n/**\n * Vision analysis result structure stored in JSON files\n */\ninterface VisionAnalysisResult {\n /** Timestamp when analysis was performed */\n timestamp: number;\n /** Route that was analyzed (e.g., \"/\", \"/profile\") */\n route: string;\n /** Screenshot filename (for reference) */\n screenshotFile?: string;\n /** Issues found by vision analysis */\n issues: VisionIssue[];\n}\n\n/**\n * Individual issue from vision analysis\n */\ninterface VisionIssue {\n /** Element text that the LLM referenced */\n elementText?: string;\n /** data-loc reference (format: \"path:line:column\") */\n dataLoc?: string;\n /** Human-readable description of the issue */\n message: string;\n /** Issue category */\n category?: \"spacing\" | \"color\" | \"typography\" | \"alignment\" | \"accessibility\" | \"layout\" | \"other\";\n /** Severity level */\n severity?: \"error\" | \"warning\" | \"info\";\n}\n\n/**\n * Find project root by looking for package.json\n */\nfunction findProjectRoot(startDir: string): string {\n let dir = startDir;\n for (let i = 0; i < 20; i++) {\n if (existsSync(join(dir, \"package.json\"))) {\n return dir;\n }\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return startDir;\n}\n\n/**\n * Get all vision analysis result files from screenshots directory\n */\nfunction getVisionResultFiles(screenshotsDir: string): string[] {\n if (!existsSync(screenshotsDir)) {\n return [];\n }\n\n try {\n const files = readdirSync(screenshotsDir);\n return files\n .filter((f) => f.endsWith(\".json\"))\n .map((f) => join(screenshotsDir, f))\n .sort()\n .reverse(); // Most recent first\n } catch {\n return [];\n }\n}\n\n/**\n * Load and parse a vision analysis result file\n */\nfunction loadVisionResult(filePath: string): VisionAnalysisResult | null {\n try {\n const content = readFileSync(filePath, \"utf-8\");\n return JSON.parse(content) as VisionAnalysisResult;\n } catch {\n return null;\n }\n}\n\n/**\n * Parse a data-loc string into file path and location\n * Format: \"path/to/file.tsx:line:column\"\n */\nfunction parseDataLoc(dataLoc: string): { filePath: string; line: number; column: number } | null {\n // Match pattern: path:line:column (line and column are numbers)\n const match = dataLoc.match(/^(.+):(\\d+):(\\d+)$/);\n if (!match) return null;\n\n return {\n filePath: match[1]!,\n line: parseInt(match[2]!, 10),\n column: parseInt(match[3]!, 10),\n };\n}\n\n/**\n * Normalize file path for comparison (handle relative vs absolute paths)\n */\nfunction normalizeFilePath(filePath: string, projectRoot: string): string {\n // If it's already a relative path, return as-is\n if (!filePath.startsWith(\"/\")) {\n return filePath;\n }\n // Convert absolute to relative\n return relative(projectRoot, filePath);\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"semantic-vision\",\n meta: {\n type: \"suggestion\",\n docs: {\n description:\n \"Report cached vision analysis results from UILint browser overlay\",\n },\n messages: {\n visionIssue: \"[Vision] {{message}}\",\n analysisStale:\n \"Vision analysis results are stale (older than {{age}}). Re-run analysis in browser.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n maxAgeMs: {\n type: \"number\",\n description:\n \"Maximum age of cached results in milliseconds (default: 1 hour)\",\n },\n screenshotsPath: {\n type: \"string\",\n description:\n \"Path to screenshots directory (default: .uilint/screenshots)\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{ maxAgeMs: 60 * 60 * 1000 }], // 1 hour default\n create(context) {\n const options = context.options[0] || {};\n const maxAgeMs = options.maxAgeMs ?? 60 * 60 * 1000;\n const filePath = context.filename;\n const fileDir = dirname(filePath);\n\n // Find project root and screenshots directory\n const projectRoot = findProjectRoot(fileDir);\n const screenshotsDir = options.screenshotsPath\n ? join(projectRoot, options.screenshotsPath)\n : join(projectRoot, \".uilint\", \"screenshots\");\n\n // Get the relative path of the current file for matching against data-loc\n const relativeFilePath = normalizeFilePath(filePath, projectRoot);\n\n // Find all vision result files\n const resultFiles = getVisionResultFiles(screenshotsDir);\n if (resultFiles.length === 0) {\n // No cached results - silently pass (analysis happens in browser)\n return {};\n }\n\n // Collect issues that match this file from all recent results\n const matchingIssues: Array<{\n issue: VisionIssue;\n line: number;\n column: number;\n isStale: boolean;\n }> = [];\n\n const now = Date.now();\n\n for (const resultFile of resultFiles) {\n const result = loadVisionResult(resultFile);\n if (!result || !result.issues) continue;\n\n const isStale = now - result.timestamp > maxAgeMs;\n\n for (const issue of result.issues) {\n if (!issue.dataLoc) continue;\n\n const parsed = parseDataLoc(issue.dataLoc);\n if (!parsed) continue;\n\n // Check if this issue is for the current file\n const issueFilePath = normalizeFilePath(parsed.filePath, projectRoot);\n if (issueFilePath === relativeFilePath) {\n matchingIssues.push({\n issue,\n line: parsed.line,\n column: parsed.column,\n isStale,\n });\n }\n }\n }\n\n // De-duplicate issues by line:column:message\n const seenIssues = new Set<string>();\n const uniqueIssues = matchingIssues.filter((item) => {\n const key = `${item.line}:${item.column}:${item.issue.message}`;\n if (seenIssues.has(key)) return false;\n seenIssues.add(key);\n return true;\n });\n\n return {\n Program(node) {\n for (const { issue, line, column, isStale } of uniqueIssues) {\n // Build message with category prefix if available\n const categoryPrefix = issue.category\n ? `[${issue.category}] `\n : \"\";\n const message = `${categoryPrefix}${issue.message}`;\n\n // Report stale warning separately if enabled\n if (isStale) {\n const ageHours = Math.round(maxAgeMs / (60 * 60 * 1000));\n context.report({\n node,\n loc: { line, column },\n messageId: \"analysisStale\",\n data: { age: `${ageHours}h` },\n });\n }\n\n context.report({\n node,\n loc: { line, column },\n messageId: \"visionIssue\",\n data: { message },\n });\n }\n },\n };\n },\n});\n","/**\n * Rule creation helper using @typescript-eslint/utils\n */\n\nimport { ESLintUtils } from \"@typescript-eslint/utils\";\n\nexport const createRule = ESLintUtils.RuleCreator(\n (name) =>\n `https://github.com/peter-suggate/uilint/blob/main/packages/uilint-eslint/docs/rules/${name}.md`\n);\n\n/**\n * Schema for prompting user to configure a rule option in the CLI\n */\nexport interface OptionFieldSchema {\n /** Field name in the options object */\n key: string;\n /** Display label for the prompt */\n label: string;\n /** Prompt type */\n type: \"text\" | \"number\" | \"boolean\" | \"select\" | \"multiselect\";\n /** Default value */\n defaultValue: unknown;\n /** Placeholder text (for text/number inputs) */\n placeholder?: string;\n /** Options for select/multiselect */\n options?: Array<{ value: string | number; label: string }>;\n /** Description/hint for the field */\n description?: string;\n}\n\n/**\n * Schema describing how to prompt for rule options during installation\n */\nexport interface RuleOptionSchema {\n /** Fields that can be configured for this rule */\n fields: OptionFieldSchema[];\n}\n\n/**\n * External requirement that a rule needs to function\n */\nexport interface RuleRequirement {\n /** Requirement type for programmatic checks */\n type: \"ollama\" | \"git\" | \"coverage\" | \"semantic-index\" | \"styleguide\";\n /** Human-readable description */\n description: string;\n /** Optional: how to satisfy the requirement */\n setupHint?: string;\n}\n\n/**\n * Rule migration definition for updating rule options between versions\n */\nexport interface RuleMigration {\n /** Source version (semver) */\n from: string;\n /** Target version (semver) */\n to: string;\n /** Human-readable description of what changed */\n description: string;\n /** Function to migrate options from old format to new format */\n migrate: (oldOptions: unknown[]) => unknown[];\n /** Whether this migration contains breaking changes */\n breaking?: boolean;\n}\n\n/**\n * Colocated rule metadata - exported alongside each rule\n *\n * This structure keeps all rule metadata in the same file as the rule implementation,\n * making it easy to maintain and extend as new rules are added.\n */\nexport interface RuleMeta {\n /** Rule identifier (e.g., \"consistent-dark-mode\") - must match filename */\n id: string;\n\n /** Semantic version of the rule (e.g., \"1.0.0\") */\n version: string;\n\n /** Display name for CLI (e.g., \"No Arbitrary Tailwind\") */\n name: string;\n\n /** Short description for CLI selection prompts (one line) */\n description: string;\n\n /** Default severity level */\n defaultSeverity: \"error\" | \"warn\" | \"off\";\n\n /** Category for grouping in CLI */\n category: \"static\" | \"semantic\";\n\n /** Icon for display in CLI/UI (emoji or icon name) */\n icon?: string;\n\n /** Short hint about the rule type/requirements */\n hint?: string;\n\n /** Whether rule is enabled by default during install */\n defaultEnabled?: boolean;\n\n /** External requirements the rule needs */\n requirements?: RuleRequirement[];\n\n /**\n * NPM packages that must be installed for this rule to work.\n * These will be added to the target project's dependencies during installation.\n *\n * Example: [\"xxhash-wasm\"] for rules using the xxhash library\n */\n npmDependencies?: string[];\n\n /** Instructions to show after installation */\n postInstallInstructions?: string;\n\n /** Framework compatibility */\n frameworks?: (\"next\" | \"vite\" | \"cra\" | \"remix\")[];\n\n /** Whether this rule requires a styleguide file */\n requiresStyleguide?: boolean;\n\n /** Default options for the rule (passed as second element in ESLint config) */\n defaultOptions?: unknown[];\n\n /** Schema for prompting user to configure options during install */\n optionSchema?: RuleOptionSchema;\n\n /**\n * Detailed documentation in markdown format.\n * Should include:\n * - What the rule does\n * - Why it's useful\n * - Examples of incorrect and correct code\n * - Configuration options explained\n */\n docs: string;\n\n /**\n * Internal utility dependencies that this rule requires.\n * When the rule is copied to a target project, these utilities\n * will be transformed to import from \"uilint-eslint\" instead\n * of relative paths.\n *\n * Example: [\"coverage-aggregator\", \"dependency-graph\"]\n */\n internalDependencies?: string[];\n\n /**\n * Whether this rule is directory-based (has lib/ folder with utilities).\n * Directory-based rules are installed as folders with index.ts and lib/ subdirectory.\n * Single-file rules are installed as single .ts files.\n *\n * When true, ESLint config imports will use:\n * ./.uilint/rules/rule-id/index.js\n * When false (default):\n * ./.uilint/rules/rule-id.js\n */\n isDirectoryBased?: boolean;\n\n /**\n * Migrations for updating rule options between versions.\n * Migrations are applied in order to transform options from older versions.\n */\n migrations?: RuleMigration[];\n\n /**\n * Which UI plugin should handle this rule.\n * Defaults based on category:\n * - \"static\" category → \"eslint\" plugin\n * - \"semantic\" category → \"semantic\" plugin\n *\n * Special cases:\n * - \"vision\" for semantic-vision rule\n */\n plugin?: \"eslint\" | \"vision\" | \"semantic\";\n\n /**\n * Custom inspector panel ID to use for this rule's issues.\n * If not specified, uses the plugin's default issue inspector.\n *\n * Examples:\n * - \"vision-issue\" for VisionIssueInspector\n * - \"duplicates\" for DuplicatesInspector\n * - \"semantic-issue\" for SemanticIssueInspector\n */\n customInspector?: string;\n\n /**\n * Custom heatmap color for this rule's issues.\n * CSS color value (hex, rgb, hsl, or named color).\n * If not specified, uses severity-based coloring.\n */\n heatmapColor?: string;\n}\n\n/**\n * Helper to define rule metadata with type safety\n */\nexport function defineRuleMeta(meta: RuleMeta): RuleMeta {\n return meta;\n}\n"],"mappings":"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","names":["meta"]}
@@ -175,6 +175,7 @@ var meta = defineRuleMeta({
175
175
  setupHint: "Run: uilint genstyleguide"
176
176
  }
177
177
  ],
178
+ npmDependencies: ["xxhash-wasm"],
178
179
  defaultOptions: [{ model: "qwen3-coder:30b", styleguidePath: ".uilint/styleguide.md" }],
179
180
  optionSchema: {
180
181
  fields: [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/rules/semantic/index.ts","../../src/utils/create-rule.ts","../../src/rules/semantic/lib/cache.ts","../../src/rules/semantic/lib/styleguide-loader.ts"],"sourcesContent":["/**\n * Rule: semantic\n *\n * LLM-powered semantic UI analysis using the project's styleguide.\n * This is the only rule that reads .uilint/styleguide.md.\n */\n\nimport { existsSync, readFileSync } from \"fs\";\nimport { spawnSync } from \"child_process\";\nimport { dirname, join, relative } from \"path\";\nimport { createRule, defineRuleMeta } from \"../../utils/create-rule.js\";\nimport {\n getCacheEntry,\n hashContentSync,\n setCacheEntry,\n type CachedIssue,\n} from \"./lib/cache.js\";\nimport { getStyleguide } from \"./lib/styleguide-loader.js\";\nimport { UILINT_DEFAULT_OLLAMA_MODEL } from \"uilint-core\";\nimport { buildSourceScanPrompt } from \"uilint-core\";\n\ntype MessageIds = \"semanticIssue\" | \"styleguideNotFound\" | \"analysisError\";\ntype Options = [\n {\n model?: string;\n styleguidePath?: string;\n }\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"semantic\",\n version: \"1.0.0\",\n name: \"Semantic Analysis\",\n description: \"LLM-powered semantic UI analysis using your styleguide\",\n defaultSeverity: \"warn\",\n category: \"semantic\",\n icon: \"🧠\",\n hint: \"LLM-powered UI analysis\",\n defaultEnabled: false,\n requiresStyleguide: true,\n plugin: \"semantic\",\n customInspector: \"semantic-issue\",\n requirements: [\n {\n type: \"ollama\",\n description: \"Requires Ollama running locally\",\n setupHint: \"Run: ollama serve && ollama pull qwen3-coder:30b\",\n },\n {\n type: \"styleguide\",\n description: \"Requires a styleguide file\",\n setupHint: \"Run: uilint genstyleguide\",\n },\n ],\n defaultOptions: [{ model: \"qwen3-coder:30b\", styleguidePath: \".uilint/styleguide.md\" }],\n optionSchema: {\n fields: [\n {\n key: \"model\",\n label: \"Ollama model to use\",\n type: \"text\",\n defaultValue: \"qwen3-coder:30b\",\n placeholder: \"qwen3-coder:30b\",\n description: \"The Ollama model name for semantic analysis\",\n },\n {\n key: \"styleguidePath\",\n label: \"Path to styleguide file\",\n type: \"text\",\n defaultValue: \".uilint/styleguide.md\",\n placeholder: \".uilint/styleguide.md\",\n description: \"Relative path to the styleguide markdown file\",\n },\n ],\n },\n docs: `\n## What it does\n\nUses a local LLM (via Ollama) to analyze your React components against your project's\nstyleguide. It catches semantic issues that pattern-based rules can't detect, like:\n- Using incorrect spacing that doesn't match your design system conventions\n- Inconsistent button styles across similar contexts\n- Missing accessibility patterns defined in your styleguide\n\n## Why it's useful\n\n- **Custom rules**: Enforces your project's unique conventions without writing custom ESLint rules\n- **Context-aware**: Understands component intent, not just syntax\n- **Evolving standards**: Update your styleguide, and the rule adapts automatically\n- **Local & private**: Runs entirely on your machine using Ollama\n\n## Prerequisites\n\n1. **Ollama installed**: \\`brew install ollama\\` or from ollama.ai\n2. **Model pulled**: \\`ollama pull qwen3-coder:30b\\` (or your preferred model)\n3. **Styleguide created**: Create \\`.uilint/styleguide.md\\` describing your conventions\n\n## Example Styleguide\n\n\\`\\`\\`markdown\n# UI Style Guide\n\n## Spacing\n- Use gap-4 for spacing between card elements\n- Use py-2 px-4 for button padding\n\n## Colors\n- Primary actions: bg-primary text-primary-foreground\n- Destructive actions: bg-destructive text-destructive-foreground\n\n## Components\n- All forms must include a Cancel button\n- Modal headers should use text-lg font-semibold\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/semantic\": [\"warn\", {\n model: \"qwen3-coder:30b\", // Ollama model name\n styleguidePath: \".uilint/styleguide.md\" // Path to styleguide\n}]\n\\`\\`\\`\n\n## Notes\n\n- Results are cached based on file content and styleguide hash\n- First run may be slow as the model loads; subsequent runs use cache\n- Works best with detailed, specific styleguide documentation\n- Set to \"off\" in CI to avoid slow builds (use pre-commit hooks locally)\n`,\n isDirectoryBased: true,\n});\n\nexport default createRule<Options, MessageIds>({\n name: \"semantic\",\n meta: {\n type: \"suggestion\",\n docs: {\n description: \"LLM-powered semantic UI analysis using styleguide\",\n },\n messages: {\n semanticIssue: \"{{message}}\",\n styleguideNotFound:\n \"No styleguide found. Create .uilint/styleguide.md or specify styleguidePath.\",\n analysisError: \"Semantic analysis failed: {{error}}\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n model: {\n type: \"string\",\n description: \"Ollama model to use\",\n },\n styleguidePath: {\n type: \"string\",\n description: \"Path to styleguide file\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{ model: UILINT_DEFAULT_OLLAMA_MODEL }],\n create(context) {\n const options = context.options[0] || {};\n const filePath = context.filename;\n const fileDir = dirname(filePath);\n\n // Get styleguide\n const { path: styleguidePath, content: styleguide } = getStyleguide(\n fileDir,\n options.styleguidePath\n );\n\n // Skip if no styleguide\n if (!styleguide) {\n console.error(\n `[uilint] Styleguide not found (styleguidePath=${String(\n options.styleguidePath ?? \"\"\n )}, startDir=${fileDir})`\n );\n\n return {\n Program(node) {\n context.report({\n node,\n messageId: \"styleguideNotFound\",\n });\n },\n };\n }\n\n // Read and hash file contents\n let fileContent: string;\n try {\n fileContent = readFileSync(filePath, \"utf-8\");\n } catch {\n console.error(`[uilint] Failed to read file ${filePath}`);\n return {\n Program(node) {\n context.report({\n node,\n messageId: \"analysisError\",\n data: { error: `Failed to read source file ${filePath}` },\n });\n },\n };\n }\n\n const fileHash = hashContentSync(fileContent);\n const styleguideHash = hashContentSync(styleguide);\n\n // Check cache\n const projectRoot = findProjectRoot(fileDir);\n const relativeFilePath = relative(projectRoot, filePath);\n const cached = getCacheEntry(\n projectRoot,\n relativeFilePath,\n fileHash,\n styleguideHash\n );\n\n const ENABLE_CACHE = false;\n if (ENABLE_CACHE && cached) {\n console.error(`[uilint] Cache hit for ${filePath}`);\n\n // Report cached issues\n return {\n Program(node) {\n for (const issue of cached.issues) {\n context.report({\n node,\n loc: { line: issue.line, column: issue.column || 0 },\n messageId: \"semanticIssue\",\n data: { message: issue.message },\n });\n }\n },\n };\n }\n\n // Cache miss: run sync analysis now (slow), cache, then report.\n ENABLE_CACHE &&\n console.error(\n `[uilint] Cache miss for ${filePath}, running semantic analysis`\n );\n\n return {\n Program(node) {\n const issues = runSemanticAnalysisSync(\n fileContent,\n styleguide,\n options.model || UILINT_DEFAULT_OLLAMA_MODEL,\n filePath\n );\n\n setCacheEntry(projectRoot, relativeFilePath, {\n fileHash,\n styleguideHash,\n issues,\n timestamp: Date.now(),\n });\n\n for (const issue of issues) {\n context.report({\n node,\n loc: { line: issue.line, column: issue.column || 0 },\n messageId: \"semanticIssue\",\n data: { message: issue.message },\n });\n }\n },\n };\n },\n});\n\n/**\n * Find project root by looking for package.json\n */\nfunction findProjectRoot(startDir: string): string {\n let dir = startDir;\n for (let i = 0; i < 20; i++) {\n if (existsSync(join(dir, \"package.json\"))) {\n return dir;\n }\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return startDir;\n}\n\n/**\n * Run semantic analysis using Ollama (synchronously).\n *\n * Implementation detail:\n * - ESLint rules are synchronous.\n * - Blocking on a Promise (sleep-loop/Atomics) would also block Node's event loop,\n * preventing the HTTP request to Ollama from ever completing.\n * - To keep this simple & debuggable, we run the async LLM call in a child Node\n * process and synchronously wait for it to exit.\n */\nfunction runSemanticAnalysisSync(\n sourceCode: string,\n styleguide: string,\n model: string,\n filePath?: string\n): CachedIssue[] {\n const startTime = Date.now();\n const fileDisplay = filePath ? ` ${filePath}` : \"\";\n\n console.error(`[uilint] Starting semantic analysis (sync)${fileDisplay}`);\n console.error(`[uilint] Model: ${model}`);\n\n // Build prompt in-process (pure string building).\n const prompt = buildSourceScanPrompt(sourceCode, styleguide, {});\n\n // Avoid `uilint-core/node` exports *and* CJS resolution:\n // resolve the installed dependency by file URL relative to this plugin bundle.\n // When built, `import.meta.url` points at `.../uilint-eslint/dist/index.js`,\n // and the dependency lives at `.../uilint-eslint/node_modules/uilint-core/dist/node.js`.\n const coreNodeUrl = new URL(\n \"../node_modules/uilint-core/dist/node.js\",\n import.meta.url\n ).href;\n\n const childScript = `\n import * as coreNode from ${JSON.stringify(coreNodeUrl)};\n const { OllamaClient, logInfo, logWarning, createProgress, pc } = coreNode;\n const chunks = [];\n for await (const c of process.stdin) chunks.push(c);\n const input = JSON.parse(Buffer.concat(chunks).toString(\"utf8\"));\n const model = input.model;\n const prompt = input.prompt;\n\n const client = new OllamaClient({ model });\n const ok = await client.isAvailable();\n if (!ok) {\n logWarning(\"Ollama not available, skipping semantic analysis\");\n process.stdout.write(JSON.stringify({ issues: [] }));\n process.exit(0);\n }\n\n logInfo(\\`Ollama connected \\${pc.dim(\\`(model: \\${model})\\`)}\\`);\n const progress = createProgress(\"Analyzing with LLM...\");\n try {\n const response = await client.complete(prompt, {\n json: true,\n stream: true,\n onProgress: (latestLine) => {\n const maxLen = 60;\n const display =\n latestLine.length > maxLen\n ? latestLine.slice(0, maxLen) + \"…\"\n : latestLine;\n progress.update(\\`LLM: \\${pc.dim(display || \"...\")}\\`);\n },\n });\n progress.succeed(\"LLM complete\");\n process.stdout.write(response);\n } catch (e) {\n progress.fail(\\`LLM failed: \\${e instanceof Error ? e.message : String(e)}\\`);\n process.exit(1);\n }\n `;\n\n const child = spawnSync(\n process.execPath,\n [\"--input-type=module\", \"-e\", childScript],\n {\n input: JSON.stringify({ model, prompt }),\n encoding: \"utf8\",\n stdio: [\"pipe\", \"pipe\", \"inherit\"],\n maxBuffer: 20 * 1024 * 1024,\n }\n );\n\n const elapsed = Date.now() - startTime;\n\n if (child.error) {\n console.error(\n `[uilint] Semantic analysis failed after ${elapsed}ms: ${child.error.message}`\n );\n return [];\n }\n\n if (typeof child.status === \"number\" && child.status !== 0) {\n console.error(\n `[uilint] Semantic analysis failed after ${elapsed}ms: child exited ${child.status}`\n );\n return [];\n }\n\n const responseText = (child.stdout || \"\").trim();\n if (!responseText) {\n console.error(\n `[uilint] Semantic analysis returned empty response (${elapsed}ms)`\n );\n return [];\n }\n\n try {\n const parsed = JSON.parse(responseText) as {\n issues?: Array<{ line?: number; column?: number; message?: string }>;\n };\n\n const issues = (parsed.issues || []).map((issue) => ({\n line: issue.line || 1,\n column: issue.column,\n message: issue.message || \"Semantic issue detected\",\n ruleId: \"uilint/semantic\",\n severity: 1 as const,\n }));\n\n if (issues.length > 0) {\n console.error(`[uilint] Found ${issues.length} issue(s) (${elapsed}ms)`);\n } else {\n console.error(`[uilint] No issues found (${elapsed}ms)`);\n }\n\n return issues;\n } catch (e) {\n console.error(\n `[uilint] Semantic analysis failed to parse response after ${elapsed}ms: ${\n e instanceof Error ? e.message : String(e)\n }`\n );\n return [];\n }\n}\n","/**\n * Rule creation helper using @typescript-eslint/utils\n */\n\nimport { ESLintUtils } from \"@typescript-eslint/utils\";\n\nexport const createRule = ESLintUtils.RuleCreator(\n (name) =>\n `https://github.com/peter-suggate/uilint/blob/main/packages/uilint-eslint/docs/rules/${name}.md`\n);\n\n/**\n * Schema for prompting user to configure a rule option in the CLI\n */\nexport interface OptionFieldSchema {\n /** Field name in the options object */\n key: string;\n /** Display label for the prompt */\n label: string;\n /** Prompt type */\n type: \"text\" | \"number\" | \"boolean\" | \"select\" | \"multiselect\";\n /** Default value */\n defaultValue: unknown;\n /** Placeholder text (for text/number inputs) */\n placeholder?: string;\n /** Options for select/multiselect */\n options?: Array<{ value: string | number; label: string }>;\n /** Description/hint for the field */\n description?: string;\n}\n\n/**\n * Schema describing how to prompt for rule options during installation\n */\nexport interface RuleOptionSchema {\n /** Fields that can be configured for this rule */\n fields: OptionFieldSchema[];\n}\n\n/**\n * External requirement that a rule needs to function\n */\nexport interface RuleRequirement {\n /** Requirement type for programmatic checks */\n type: \"ollama\" | \"git\" | \"coverage\" | \"semantic-index\" | \"styleguide\";\n /** Human-readable description */\n description: string;\n /** Optional: how to satisfy the requirement */\n setupHint?: string;\n}\n\n/**\n * Rule migration definition for updating rule options between versions\n */\nexport interface RuleMigration {\n /** Source version (semver) */\n from: string;\n /** Target version (semver) */\n to: string;\n /** Human-readable description of what changed */\n description: string;\n /** Function to migrate options from old format to new format */\n migrate: (oldOptions: unknown[]) => unknown[];\n /** Whether this migration contains breaking changes */\n breaking?: boolean;\n}\n\n/**\n * Colocated rule metadata - exported alongside each rule\n *\n * This structure keeps all rule metadata in the same file as the rule implementation,\n * making it easy to maintain and extend as new rules are added.\n */\nexport interface RuleMeta {\n /** Rule identifier (e.g., \"consistent-dark-mode\") - must match filename */\n id: string;\n\n /** Semantic version of the rule (e.g., \"1.0.0\") */\n version: string;\n\n /** Display name for CLI (e.g., \"No Arbitrary Tailwind\") */\n name: string;\n\n /** Short description for CLI selection prompts (one line) */\n description: string;\n\n /** Default severity level */\n defaultSeverity: \"error\" | \"warn\" | \"off\";\n\n /** Category for grouping in CLI */\n category: \"static\" | \"semantic\";\n\n /** Icon for display in CLI/UI (emoji or icon name) */\n icon?: string;\n\n /** Short hint about the rule type/requirements */\n hint?: string;\n\n /** Whether rule is enabled by default during install */\n defaultEnabled?: boolean;\n\n /** External requirements the rule needs */\n requirements?: RuleRequirement[];\n\n /** Instructions to show after installation */\n postInstallInstructions?: string;\n\n /** Framework compatibility */\n frameworks?: (\"next\" | \"vite\" | \"cra\" | \"remix\")[];\n\n /** Whether this rule requires a styleguide file */\n requiresStyleguide?: boolean;\n\n /** Default options for the rule (passed as second element in ESLint config) */\n defaultOptions?: unknown[];\n\n /** Schema for prompting user to configure options during install */\n optionSchema?: RuleOptionSchema;\n\n /**\n * Detailed documentation in markdown format.\n * Should include:\n * - What the rule does\n * - Why it's useful\n * - Examples of incorrect and correct code\n * - Configuration options explained\n */\n docs: string;\n\n /**\n * Internal utility dependencies that this rule requires.\n * When the rule is copied to a target project, these utilities\n * will be transformed to import from \"uilint-eslint\" instead\n * of relative paths.\n *\n * Example: [\"coverage-aggregator\", \"dependency-graph\"]\n */\n internalDependencies?: string[];\n\n /**\n * Whether this rule is directory-based (has lib/ folder with utilities).\n * Directory-based rules are installed as folders with index.ts and lib/ subdirectory.\n * Single-file rules are installed as single .ts files.\n *\n * When true, ESLint config imports will use:\n * ./.uilint/rules/rule-id/index.js\n * When false (default):\n * ./.uilint/rules/rule-id.js\n */\n isDirectoryBased?: boolean;\n\n /**\n * Migrations for updating rule options between versions.\n * Migrations are applied in order to transform options from older versions.\n */\n migrations?: RuleMigration[];\n\n /**\n * Which UI plugin should handle this rule.\n * Defaults based on category:\n * - \"static\" category → \"eslint\" plugin\n * - \"semantic\" category → \"semantic\" plugin\n *\n * Special cases:\n * - \"vision\" for semantic-vision rule\n */\n plugin?: \"eslint\" | \"vision\" | \"semantic\";\n\n /**\n * Custom inspector panel ID to use for this rule's issues.\n * If not specified, uses the plugin's default issue inspector.\n *\n * Examples:\n * - \"vision-issue\" for VisionIssueInspector\n * - \"duplicates\" for DuplicatesInspector\n * - \"semantic-issue\" for SemanticIssueInspector\n */\n customInspector?: string;\n\n /**\n * Custom heatmap color for this rule's issues.\n * CSS color value (hex, rgb, hsl, or named color).\n * If not specified, uses severity-based coloring.\n */\n heatmapColor?: string;\n}\n\n/**\n * Helper to define rule metadata with type safety\n */\nexport function defineRuleMeta(meta: RuleMeta): RuleMeta {\n return meta;\n}\n","/**\n * File-hash based caching for LLM semantic rule\n *\n * Uses xxhash for fast hashing of file contents.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { dirname, join } from \"path\";\n\n// Lazy-loaded xxhash\nlet xxhashInstance: Awaited<\n ReturnType<typeof import(\"xxhash-wasm\")[\"default\"]>\n> | null = null;\n\nasync function getXxhash() {\n if (!xxhashInstance) {\n const xxhash = await import(\"xxhash-wasm\");\n xxhashInstance = await xxhash.default();\n }\n return xxhashInstance;\n}\n\n/**\n * Synchronous hash using a simple djb2 algorithm (fallback when xxhash not available)\n */\nfunction djb2Hash(str: string): string {\n let hash = 5381;\n for (let i = 0; i < str.length; i++) {\n hash = (hash * 33) ^ str.charCodeAt(i);\n }\n return (hash >>> 0).toString(16);\n}\n\n/**\n * Hash content using xxhash (async) or djb2 (sync fallback)\n */\nexport async function hashContent(content: string): Promise<string> {\n try {\n const xxhash = await getXxhash();\n return xxhash.h64ToString(content);\n } catch {\n return djb2Hash(content);\n }\n}\n\n/**\n * Synchronous hash for when async is not possible\n */\nexport function hashContentSync(content: string): string {\n return djb2Hash(content);\n}\n\nexport interface CacheEntry {\n fileHash: string;\n styleguideHash: string;\n issues: CachedIssue[];\n timestamp: number;\n}\n\nexport interface CachedIssue {\n line: number;\n column?: number;\n message: string;\n ruleId: string;\n severity: 1 | 2; // 1 = warn, 2 = error\n}\n\nexport interface CacheStore {\n version: number;\n entries: Record<string, CacheEntry>;\n}\n\nconst CACHE_VERSION = 1;\nconst CACHE_FILE = \".uilint/.cache/eslint-semantic.json\";\n\n/**\n * Get the cache file path for a project\n */\nexport function getCachePath(projectRoot: string): string {\n return join(projectRoot, CACHE_FILE);\n}\n\n/**\n * Load the cache store\n */\nexport function loadCache(projectRoot: string): CacheStore {\n const cachePath = getCachePath(projectRoot);\n\n if (!existsSync(cachePath)) {\n return { version: CACHE_VERSION, entries: {} };\n }\n\n try {\n const content = readFileSync(cachePath, \"utf-8\");\n const cache = JSON.parse(content) as CacheStore;\n\n // Invalidate if version mismatch\n if (cache.version !== CACHE_VERSION) {\n return { version: CACHE_VERSION, entries: {} };\n }\n\n return cache;\n } catch {\n return { version: CACHE_VERSION, entries: {} };\n }\n}\n\n/**\n * Save the cache store\n */\nexport function saveCache(projectRoot: string, cache: CacheStore): void {\n const cachePath = getCachePath(projectRoot);\n\n try {\n const cacheDir = dirname(cachePath);\n if (!existsSync(cacheDir)) {\n mkdirSync(cacheDir, { recursive: true });\n }\n\n writeFileSync(cachePath, JSON.stringify(cache, null, 2), \"utf-8\");\n } catch {\n // Silently fail - caching is optional\n }\n}\n\n/**\n * Get cached entry for a file\n */\nexport function getCacheEntry(\n projectRoot: string,\n filePath: string,\n fileHash: string,\n styleguideHash: string\n): CacheEntry | null {\n const cache = loadCache(projectRoot);\n const entry = cache.entries[filePath];\n\n if (!entry) return null;\n\n // Check if hashes match\n if (entry.fileHash !== fileHash || entry.styleguideHash !== styleguideHash) {\n return null;\n }\n\n return entry;\n}\n\n/**\n * Set cached entry for a file\n */\nexport function setCacheEntry(\n projectRoot: string,\n filePath: string,\n entry: CacheEntry\n): void {\n const cache = loadCache(projectRoot);\n cache.entries[filePath] = entry;\n saveCache(projectRoot, cache);\n}\n\n/**\n * Clear cache for a specific file\n */\nexport function clearCacheEntry(projectRoot: string, filePath: string): void {\n const cache = loadCache(projectRoot);\n delete cache.entries[filePath];\n saveCache(projectRoot, cache);\n}\n\n/**\n * Clear entire cache\n */\nexport function clearCache(projectRoot: string): void {\n saveCache(projectRoot, { version: CACHE_VERSION, entries: {} });\n}\n","/**\n * Styleguide loader for the LLM semantic rule\n *\n * Only the semantic rule reads the styleguide - static rules use ESLint options.\n */\n\nimport { existsSync, readFileSync } from \"fs\";\nimport { dirname, isAbsolute, join, resolve } from \"path\";\n\nconst DEFAULT_STYLEGUIDE_PATHS = [\n \".uilint/styleguide.md\",\n \".uilint/styleguide.yaml\",\n \".uilint/styleguide.yml\",\n];\n\n/**\n * Find workspace root by walking up looking for pnpm-workspace.yaml, package.json, or .git\n */\nfunction findWorkspaceRoot(startDir: string): string {\n let dir = startDir;\n for (let i = 0; i < 20; i++) {\n if (\n existsSync(join(dir, \"pnpm-workspace.yaml\")) ||\n existsSync(join(dir, \".git\"))\n ) {\n return dir;\n }\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return startDir;\n}\n\n/**\n * Find the nearest package root (directory containing package.json),\n * stopping at the workspace root.\n */\nfunction findNearestPackageRoot(\n startDir: string,\n workspaceRoot: string\n): string {\n let dir = startDir;\n for (let i = 0; i < 30; i++) {\n if (existsSync(join(dir, \"package.json\"))) return dir;\n if (dir === workspaceRoot) break;\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return startDir;\n}\n\n/**\n * Find the styleguide file path\n */\nexport function findStyleguidePath(\n startDir: string,\n explicitPath?: string\n): string | null {\n // Explicit path takes precedence\n if (explicitPath) {\n if (isAbsolute(explicitPath)) {\n return existsSync(explicitPath) ? explicitPath : null;\n }\n\n // For relative explicit paths, try:\n // 1) relative to the file dir (back-compat)\n // 2) relative to the nearest package root (typical \"project root\")\n // 3) relative to workspace root (monorepo root)\n const workspaceRoot = findWorkspaceRoot(startDir);\n const packageRoot = findNearestPackageRoot(startDir, workspaceRoot);\n\n const candidates = [\n resolve(startDir, explicitPath),\n resolve(packageRoot, explicitPath),\n resolve(workspaceRoot, explicitPath),\n ];\n\n for (const p of candidates) {\n if (existsSync(p)) return p;\n }\n\n return null;\n }\n\n // Check from start dir up to workspace root\n const workspaceRoot = findWorkspaceRoot(startDir);\n let dir = startDir;\n\n while (true) {\n for (const relativePath of DEFAULT_STYLEGUIDE_PATHS) {\n const fullPath = join(dir, relativePath);\n if (existsSync(fullPath)) {\n return fullPath;\n }\n }\n\n // Stop at workspace root\n if (dir === workspaceRoot) break;\n\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n\n return null;\n}\n\n/**\n * Load styleguide content from file\n */\nexport function loadStyleguide(\n startDir: string,\n explicitPath?: string\n): string | null {\n const path = findStyleguidePath(startDir, explicitPath);\n if (!path) return null;\n\n try {\n return readFileSync(path, \"utf-8\");\n } catch {\n return null;\n }\n}\n\n/**\n * Get styleguide path and content\n */\nexport function getStyleguide(\n startDir: string,\n explicitPath?: string\n): { path: string | null; content: string | null } {\n const path = findStyleguidePath(startDir, explicitPath);\n if (!path) return { path: null, content: null };\n\n try {\n const content = readFileSync(path, \"utf-8\");\n return { path, content };\n } catch {\n return { path, content: null };\n }\n}\n"],"mappings":"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","names":["existsSync","readFileSync","dirname","join","meta","existsSync","readFileSync","dirname","join","workspaceRoot","readFileSync","dirname","readFileSync","existsSync","join"]}
1
+ {"version":3,"sources":["../../src/rules/semantic/index.ts","../../src/utils/create-rule.ts","../../src/rules/semantic/lib/cache.ts","../../src/rules/semantic/lib/styleguide-loader.ts"],"sourcesContent":["/**\n * Rule: semantic\n *\n * LLM-powered semantic UI analysis using the project's styleguide.\n * This is the only rule that reads .uilint/styleguide.md.\n */\n\nimport { existsSync, readFileSync } from \"fs\";\nimport { spawnSync } from \"child_process\";\nimport { dirname, join, relative } from \"path\";\nimport { createRule, defineRuleMeta } from \"../../utils/create-rule.js\";\nimport {\n getCacheEntry,\n hashContentSync,\n setCacheEntry,\n type CachedIssue,\n} from \"./lib/cache.js\";\nimport { getStyleguide } from \"./lib/styleguide-loader.js\";\nimport { UILINT_DEFAULT_OLLAMA_MODEL } from \"uilint-core\";\nimport { buildSourceScanPrompt } from \"uilint-core\";\n\ntype MessageIds = \"semanticIssue\" | \"styleguideNotFound\" | \"analysisError\";\ntype Options = [\n {\n model?: string;\n styleguidePath?: string;\n }\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"semantic\",\n version: \"1.0.0\",\n name: \"Semantic Analysis\",\n description: \"LLM-powered semantic UI analysis using your styleguide\",\n defaultSeverity: \"warn\",\n category: \"semantic\",\n icon: \"🧠\",\n hint: \"LLM-powered UI analysis\",\n defaultEnabled: false,\n requiresStyleguide: true,\n plugin: \"semantic\",\n customInspector: \"semantic-issue\",\n requirements: [\n {\n type: \"ollama\",\n description: \"Requires Ollama running locally\",\n setupHint: \"Run: ollama serve && ollama pull qwen3-coder:30b\",\n },\n {\n type: \"styleguide\",\n description: \"Requires a styleguide file\",\n setupHint: \"Run: uilint genstyleguide\",\n },\n ],\n npmDependencies: [\"xxhash-wasm\"],\n defaultOptions: [{ model: \"qwen3-coder:30b\", styleguidePath: \".uilint/styleguide.md\" }],\n optionSchema: {\n fields: [\n {\n key: \"model\",\n label: \"Ollama model to use\",\n type: \"text\",\n defaultValue: \"qwen3-coder:30b\",\n placeholder: \"qwen3-coder:30b\",\n description: \"The Ollama model name for semantic analysis\",\n },\n {\n key: \"styleguidePath\",\n label: \"Path to styleguide file\",\n type: \"text\",\n defaultValue: \".uilint/styleguide.md\",\n placeholder: \".uilint/styleguide.md\",\n description: \"Relative path to the styleguide markdown file\",\n },\n ],\n },\n docs: `\n## What it does\n\nUses a local LLM (via Ollama) to analyze your React components against your project's\nstyleguide. It catches semantic issues that pattern-based rules can't detect, like:\n- Using incorrect spacing that doesn't match your design system conventions\n- Inconsistent button styles across similar contexts\n- Missing accessibility patterns defined in your styleguide\n\n## Why it's useful\n\n- **Custom rules**: Enforces your project's unique conventions without writing custom ESLint rules\n- **Context-aware**: Understands component intent, not just syntax\n- **Evolving standards**: Update your styleguide, and the rule adapts automatically\n- **Local & private**: Runs entirely on your machine using Ollama\n\n## Prerequisites\n\n1. **Ollama installed**: \\`brew install ollama\\` or from ollama.ai\n2. **Model pulled**: \\`ollama pull qwen3-coder:30b\\` (or your preferred model)\n3. **Styleguide created**: Create \\`.uilint/styleguide.md\\` describing your conventions\n\n## Example Styleguide\n\n\\`\\`\\`markdown\n# UI Style Guide\n\n## Spacing\n- Use gap-4 for spacing between card elements\n- Use py-2 px-4 for button padding\n\n## Colors\n- Primary actions: bg-primary text-primary-foreground\n- Destructive actions: bg-destructive text-destructive-foreground\n\n## Components\n- All forms must include a Cancel button\n- Modal headers should use text-lg font-semibold\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/semantic\": [\"warn\", {\n model: \"qwen3-coder:30b\", // Ollama model name\n styleguidePath: \".uilint/styleguide.md\" // Path to styleguide\n}]\n\\`\\`\\`\n\n## Notes\n\n- Results are cached based on file content and styleguide hash\n- First run may be slow as the model loads; subsequent runs use cache\n- Works best with detailed, specific styleguide documentation\n- Set to \"off\" in CI to avoid slow builds (use pre-commit hooks locally)\n`,\n isDirectoryBased: true,\n});\n\nexport default createRule<Options, MessageIds>({\n name: \"semantic\",\n meta: {\n type: \"suggestion\",\n docs: {\n description: \"LLM-powered semantic UI analysis using styleguide\",\n },\n messages: {\n semanticIssue: \"{{message}}\",\n styleguideNotFound:\n \"No styleguide found. Create .uilint/styleguide.md or specify styleguidePath.\",\n analysisError: \"Semantic analysis failed: {{error}}\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n model: {\n type: \"string\",\n description: \"Ollama model to use\",\n },\n styleguidePath: {\n type: \"string\",\n description: \"Path to styleguide file\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{ model: UILINT_DEFAULT_OLLAMA_MODEL }],\n create(context) {\n const options = context.options[0] || {};\n const filePath = context.filename;\n const fileDir = dirname(filePath);\n\n // Get styleguide\n const { path: styleguidePath, content: styleguide } = getStyleguide(\n fileDir,\n options.styleguidePath\n );\n\n // Skip if no styleguide\n if (!styleguide) {\n console.error(\n `[uilint] Styleguide not found (styleguidePath=${String(\n options.styleguidePath ?? \"\"\n )}, startDir=${fileDir})`\n );\n\n return {\n Program(node) {\n context.report({\n node,\n messageId: \"styleguideNotFound\",\n });\n },\n };\n }\n\n // Read and hash file contents\n let fileContent: string;\n try {\n fileContent = readFileSync(filePath, \"utf-8\");\n } catch {\n console.error(`[uilint] Failed to read file ${filePath}`);\n return {\n Program(node) {\n context.report({\n node,\n messageId: \"analysisError\",\n data: { error: `Failed to read source file ${filePath}` },\n });\n },\n };\n }\n\n const fileHash = hashContentSync(fileContent);\n const styleguideHash = hashContentSync(styleguide);\n\n // Check cache\n const projectRoot = findProjectRoot(fileDir);\n const relativeFilePath = relative(projectRoot, filePath);\n const cached = getCacheEntry(\n projectRoot,\n relativeFilePath,\n fileHash,\n styleguideHash\n );\n\n const ENABLE_CACHE = false;\n if (ENABLE_CACHE && cached) {\n console.error(`[uilint] Cache hit for ${filePath}`);\n\n // Report cached issues\n return {\n Program(node) {\n for (const issue of cached.issues) {\n context.report({\n node,\n loc: { line: issue.line, column: issue.column || 0 },\n messageId: \"semanticIssue\",\n data: { message: issue.message },\n });\n }\n },\n };\n }\n\n // Cache miss: run sync analysis now (slow), cache, then report.\n ENABLE_CACHE &&\n console.error(\n `[uilint] Cache miss for ${filePath}, running semantic analysis`\n );\n\n return {\n Program(node) {\n const issues = runSemanticAnalysisSync(\n fileContent,\n styleguide,\n options.model || UILINT_DEFAULT_OLLAMA_MODEL,\n filePath\n );\n\n setCacheEntry(projectRoot, relativeFilePath, {\n fileHash,\n styleguideHash,\n issues,\n timestamp: Date.now(),\n });\n\n for (const issue of issues) {\n context.report({\n node,\n loc: { line: issue.line, column: issue.column || 0 },\n messageId: \"semanticIssue\",\n data: { message: issue.message },\n });\n }\n },\n };\n },\n});\n\n/**\n * Find project root by looking for package.json\n */\nfunction findProjectRoot(startDir: string): string {\n let dir = startDir;\n for (let i = 0; i < 20; i++) {\n if (existsSync(join(dir, \"package.json\"))) {\n return dir;\n }\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return startDir;\n}\n\n/**\n * Run semantic analysis using Ollama (synchronously).\n *\n * Implementation detail:\n * - ESLint rules are synchronous.\n * - Blocking on a Promise (sleep-loop/Atomics) would also block Node's event loop,\n * preventing the HTTP request to Ollama from ever completing.\n * - To keep this simple & debuggable, we run the async LLM call in a child Node\n * process and synchronously wait for it to exit.\n */\nfunction runSemanticAnalysisSync(\n sourceCode: string,\n styleguide: string,\n model: string,\n filePath?: string\n): CachedIssue[] {\n const startTime = Date.now();\n const fileDisplay = filePath ? ` ${filePath}` : \"\";\n\n console.error(`[uilint] Starting semantic analysis (sync)${fileDisplay}`);\n console.error(`[uilint] Model: ${model}`);\n\n // Build prompt in-process (pure string building).\n const prompt = buildSourceScanPrompt(sourceCode, styleguide, {});\n\n // Avoid `uilint-core/node` exports *and* CJS resolution:\n // resolve the installed dependency by file URL relative to this plugin bundle.\n // When built, `import.meta.url` points at `.../uilint-eslint/dist/index.js`,\n // and the dependency lives at `.../uilint-eslint/node_modules/uilint-core/dist/node.js`.\n const coreNodeUrl = new URL(\n \"../node_modules/uilint-core/dist/node.js\",\n import.meta.url\n ).href;\n\n const childScript = `\n import * as coreNode from ${JSON.stringify(coreNodeUrl)};\n const { OllamaClient, logInfo, logWarning, createProgress, pc } = coreNode;\n const chunks = [];\n for await (const c of process.stdin) chunks.push(c);\n const input = JSON.parse(Buffer.concat(chunks).toString(\"utf8\"));\n const model = input.model;\n const prompt = input.prompt;\n\n const client = new OllamaClient({ model });\n const ok = await client.isAvailable();\n if (!ok) {\n logWarning(\"Ollama not available, skipping semantic analysis\");\n process.stdout.write(JSON.stringify({ issues: [] }));\n process.exit(0);\n }\n\n logInfo(\\`Ollama connected \\${pc.dim(\\`(model: \\${model})\\`)}\\`);\n const progress = createProgress(\"Analyzing with LLM...\");\n try {\n const response = await client.complete(prompt, {\n json: true,\n stream: true,\n onProgress: (latestLine) => {\n const maxLen = 60;\n const display =\n latestLine.length > maxLen\n ? latestLine.slice(0, maxLen) + \"…\"\n : latestLine;\n progress.update(\\`LLM: \\${pc.dim(display || \"...\")}\\`);\n },\n });\n progress.succeed(\"LLM complete\");\n process.stdout.write(response);\n } catch (e) {\n progress.fail(\\`LLM failed: \\${e instanceof Error ? e.message : String(e)}\\`);\n process.exit(1);\n }\n `;\n\n const child = spawnSync(\n process.execPath,\n [\"--input-type=module\", \"-e\", childScript],\n {\n input: JSON.stringify({ model, prompt }),\n encoding: \"utf8\",\n stdio: [\"pipe\", \"pipe\", \"inherit\"],\n maxBuffer: 20 * 1024 * 1024,\n }\n );\n\n const elapsed = Date.now() - startTime;\n\n if (child.error) {\n console.error(\n `[uilint] Semantic analysis failed after ${elapsed}ms: ${child.error.message}`\n );\n return [];\n }\n\n if (typeof child.status === \"number\" && child.status !== 0) {\n console.error(\n `[uilint] Semantic analysis failed after ${elapsed}ms: child exited ${child.status}`\n );\n return [];\n }\n\n const responseText = (child.stdout || \"\").trim();\n if (!responseText) {\n console.error(\n `[uilint] Semantic analysis returned empty response (${elapsed}ms)`\n );\n return [];\n }\n\n try {\n const parsed = JSON.parse(responseText) as {\n issues?: Array<{ line?: number; column?: number; message?: string }>;\n };\n\n const issues = (parsed.issues || []).map((issue) => ({\n line: issue.line || 1,\n column: issue.column,\n message: issue.message || \"Semantic issue detected\",\n ruleId: \"uilint/semantic\",\n severity: 1 as const,\n }));\n\n if (issues.length > 0) {\n console.error(`[uilint] Found ${issues.length} issue(s) (${elapsed}ms)`);\n } else {\n console.error(`[uilint] No issues found (${elapsed}ms)`);\n }\n\n return issues;\n } catch (e) {\n console.error(\n `[uilint] Semantic analysis failed to parse response after ${elapsed}ms: ${\n e instanceof Error ? e.message : String(e)\n }`\n );\n return [];\n }\n}\n","/**\n * Rule creation helper using @typescript-eslint/utils\n */\n\nimport { ESLintUtils } from \"@typescript-eslint/utils\";\n\nexport const createRule = ESLintUtils.RuleCreator(\n (name) =>\n `https://github.com/peter-suggate/uilint/blob/main/packages/uilint-eslint/docs/rules/${name}.md`\n);\n\n/**\n * Schema for prompting user to configure a rule option in the CLI\n */\nexport interface OptionFieldSchema {\n /** Field name in the options object */\n key: string;\n /** Display label for the prompt */\n label: string;\n /** Prompt type */\n type: \"text\" | \"number\" | \"boolean\" | \"select\" | \"multiselect\";\n /** Default value */\n defaultValue: unknown;\n /** Placeholder text (for text/number inputs) */\n placeholder?: string;\n /** Options for select/multiselect */\n options?: Array<{ value: string | number; label: string }>;\n /** Description/hint for the field */\n description?: string;\n}\n\n/**\n * Schema describing how to prompt for rule options during installation\n */\nexport interface RuleOptionSchema {\n /** Fields that can be configured for this rule */\n fields: OptionFieldSchema[];\n}\n\n/**\n * External requirement that a rule needs to function\n */\nexport interface RuleRequirement {\n /** Requirement type for programmatic checks */\n type: \"ollama\" | \"git\" | \"coverage\" | \"semantic-index\" | \"styleguide\";\n /** Human-readable description */\n description: string;\n /** Optional: how to satisfy the requirement */\n setupHint?: string;\n}\n\n/**\n * Rule migration definition for updating rule options between versions\n */\nexport interface RuleMigration {\n /** Source version (semver) */\n from: string;\n /** Target version (semver) */\n to: string;\n /** Human-readable description of what changed */\n description: string;\n /** Function to migrate options from old format to new format */\n migrate: (oldOptions: unknown[]) => unknown[];\n /** Whether this migration contains breaking changes */\n breaking?: boolean;\n}\n\n/**\n * Colocated rule metadata - exported alongside each rule\n *\n * This structure keeps all rule metadata in the same file as the rule implementation,\n * making it easy to maintain and extend as new rules are added.\n */\nexport interface RuleMeta {\n /** Rule identifier (e.g., \"consistent-dark-mode\") - must match filename */\n id: string;\n\n /** Semantic version of the rule (e.g., \"1.0.0\") */\n version: string;\n\n /** Display name for CLI (e.g., \"No Arbitrary Tailwind\") */\n name: string;\n\n /** Short description for CLI selection prompts (one line) */\n description: string;\n\n /** Default severity level */\n defaultSeverity: \"error\" | \"warn\" | \"off\";\n\n /** Category for grouping in CLI */\n category: \"static\" | \"semantic\";\n\n /** Icon for display in CLI/UI (emoji or icon name) */\n icon?: string;\n\n /** Short hint about the rule type/requirements */\n hint?: string;\n\n /** Whether rule is enabled by default during install */\n defaultEnabled?: boolean;\n\n /** External requirements the rule needs */\n requirements?: RuleRequirement[];\n\n /**\n * NPM packages that must be installed for this rule to work.\n * These will be added to the target project's dependencies during installation.\n *\n * Example: [\"xxhash-wasm\"] for rules using the xxhash library\n */\n npmDependencies?: string[];\n\n /** Instructions to show after installation */\n postInstallInstructions?: string;\n\n /** Framework compatibility */\n frameworks?: (\"next\" | \"vite\" | \"cra\" | \"remix\")[];\n\n /** Whether this rule requires a styleguide file */\n requiresStyleguide?: boolean;\n\n /** Default options for the rule (passed as second element in ESLint config) */\n defaultOptions?: unknown[];\n\n /** Schema for prompting user to configure options during install */\n optionSchema?: RuleOptionSchema;\n\n /**\n * Detailed documentation in markdown format.\n * Should include:\n * - What the rule does\n * - Why it's useful\n * - Examples of incorrect and correct code\n * - Configuration options explained\n */\n docs: string;\n\n /**\n * Internal utility dependencies that this rule requires.\n * When the rule is copied to a target project, these utilities\n * will be transformed to import from \"uilint-eslint\" instead\n * of relative paths.\n *\n * Example: [\"coverage-aggregator\", \"dependency-graph\"]\n */\n internalDependencies?: string[];\n\n /**\n * Whether this rule is directory-based (has lib/ folder with utilities).\n * Directory-based rules are installed as folders with index.ts and lib/ subdirectory.\n * Single-file rules are installed as single .ts files.\n *\n * When true, ESLint config imports will use:\n * ./.uilint/rules/rule-id/index.js\n * When false (default):\n * ./.uilint/rules/rule-id.js\n */\n isDirectoryBased?: boolean;\n\n /**\n * Migrations for updating rule options between versions.\n * Migrations are applied in order to transform options from older versions.\n */\n migrations?: RuleMigration[];\n\n /**\n * Which UI plugin should handle this rule.\n * Defaults based on category:\n * - \"static\" category → \"eslint\" plugin\n * - \"semantic\" category → \"semantic\" plugin\n *\n * Special cases:\n * - \"vision\" for semantic-vision rule\n */\n plugin?: \"eslint\" | \"vision\" | \"semantic\";\n\n /**\n * Custom inspector panel ID to use for this rule's issues.\n * If not specified, uses the plugin's default issue inspector.\n *\n * Examples:\n * - \"vision-issue\" for VisionIssueInspector\n * - \"duplicates\" for DuplicatesInspector\n * - \"semantic-issue\" for SemanticIssueInspector\n */\n customInspector?: string;\n\n /**\n * Custom heatmap color for this rule's issues.\n * CSS color value (hex, rgb, hsl, or named color).\n * If not specified, uses severity-based coloring.\n */\n heatmapColor?: string;\n}\n\n/**\n * Helper to define rule metadata with type safety\n */\nexport function defineRuleMeta(meta: RuleMeta): RuleMeta {\n return meta;\n}\n","/**\n * File-hash based caching for LLM semantic rule\n *\n * Uses xxhash for fast hashing of file contents.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { dirname, join } from \"path\";\n\n// Lazy-loaded xxhash\nlet xxhashInstance: Awaited<\n ReturnType<typeof import(\"xxhash-wasm\")[\"default\"]>\n> | null = null;\n\nasync function getXxhash() {\n if (!xxhashInstance) {\n const xxhash = await import(\"xxhash-wasm\");\n xxhashInstance = await xxhash.default();\n }\n return xxhashInstance;\n}\n\n/**\n * Synchronous hash using a simple djb2 algorithm (fallback when xxhash not available)\n */\nfunction djb2Hash(str: string): string {\n let hash = 5381;\n for (let i = 0; i < str.length; i++) {\n hash = (hash * 33) ^ str.charCodeAt(i);\n }\n return (hash >>> 0).toString(16);\n}\n\n/**\n * Hash content using xxhash (async) or djb2 (sync fallback)\n */\nexport async function hashContent(content: string): Promise<string> {\n try {\n const xxhash = await getXxhash();\n return xxhash.h64ToString(content);\n } catch {\n return djb2Hash(content);\n }\n}\n\n/**\n * Synchronous hash for when async is not possible\n */\nexport function hashContentSync(content: string): string {\n return djb2Hash(content);\n}\n\nexport interface CacheEntry {\n fileHash: string;\n styleguideHash: string;\n issues: CachedIssue[];\n timestamp: number;\n}\n\nexport interface CachedIssue {\n line: number;\n column?: number;\n message: string;\n ruleId: string;\n severity: 1 | 2; // 1 = warn, 2 = error\n}\n\nexport interface CacheStore {\n version: number;\n entries: Record<string, CacheEntry>;\n}\n\nconst CACHE_VERSION = 1;\nconst CACHE_FILE = \".uilint/.cache/eslint-semantic.json\";\n\n/**\n * Get the cache file path for a project\n */\nexport function getCachePath(projectRoot: string): string {\n return join(projectRoot, CACHE_FILE);\n}\n\n/**\n * Load the cache store\n */\nexport function loadCache(projectRoot: string): CacheStore {\n const cachePath = getCachePath(projectRoot);\n\n if (!existsSync(cachePath)) {\n return { version: CACHE_VERSION, entries: {} };\n }\n\n try {\n const content = readFileSync(cachePath, \"utf-8\");\n const cache = JSON.parse(content) as CacheStore;\n\n // Invalidate if version mismatch\n if (cache.version !== CACHE_VERSION) {\n return { version: CACHE_VERSION, entries: {} };\n }\n\n return cache;\n } catch {\n return { version: CACHE_VERSION, entries: {} };\n }\n}\n\n/**\n * Save the cache store\n */\nexport function saveCache(projectRoot: string, cache: CacheStore): void {\n const cachePath = getCachePath(projectRoot);\n\n try {\n const cacheDir = dirname(cachePath);\n if (!existsSync(cacheDir)) {\n mkdirSync(cacheDir, { recursive: true });\n }\n\n writeFileSync(cachePath, JSON.stringify(cache, null, 2), \"utf-8\");\n } catch {\n // Silently fail - caching is optional\n }\n}\n\n/**\n * Get cached entry for a file\n */\nexport function getCacheEntry(\n projectRoot: string,\n filePath: string,\n fileHash: string,\n styleguideHash: string\n): CacheEntry | null {\n const cache = loadCache(projectRoot);\n const entry = cache.entries[filePath];\n\n if (!entry) return null;\n\n // Check if hashes match\n if (entry.fileHash !== fileHash || entry.styleguideHash !== styleguideHash) {\n return null;\n }\n\n return entry;\n}\n\n/**\n * Set cached entry for a file\n */\nexport function setCacheEntry(\n projectRoot: string,\n filePath: string,\n entry: CacheEntry\n): void {\n const cache = loadCache(projectRoot);\n cache.entries[filePath] = entry;\n saveCache(projectRoot, cache);\n}\n\n/**\n * Clear cache for a specific file\n */\nexport function clearCacheEntry(projectRoot: string, filePath: string): void {\n const cache = loadCache(projectRoot);\n delete cache.entries[filePath];\n saveCache(projectRoot, cache);\n}\n\n/**\n * Clear entire cache\n */\nexport function clearCache(projectRoot: string): void {\n saveCache(projectRoot, { version: CACHE_VERSION, entries: {} });\n}\n","/**\n * Styleguide loader for the LLM semantic rule\n *\n * Only the semantic rule reads the styleguide - static rules use ESLint options.\n */\n\nimport { existsSync, readFileSync } from \"fs\";\nimport { dirname, isAbsolute, join, resolve } from \"path\";\n\nconst DEFAULT_STYLEGUIDE_PATHS = [\n \".uilint/styleguide.md\",\n \".uilint/styleguide.yaml\",\n \".uilint/styleguide.yml\",\n];\n\n/**\n * Find workspace root by walking up looking for pnpm-workspace.yaml, package.json, or .git\n */\nfunction findWorkspaceRoot(startDir: string): string {\n let dir = startDir;\n for (let i = 0; i < 20; i++) {\n if (\n existsSync(join(dir, \"pnpm-workspace.yaml\")) ||\n existsSync(join(dir, \".git\"))\n ) {\n return dir;\n }\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return startDir;\n}\n\n/**\n * Find the nearest package root (directory containing package.json),\n * stopping at the workspace root.\n */\nfunction findNearestPackageRoot(\n startDir: string,\n workspaceRoot: string\n): string {\n let dir = startDir;\n for (let i = 0; i < 30; i++) {\n if (existsSync(join(dir, \"package.json\"))) return dir;\n if (dir === workspaceRoot) break;\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return startDir;\n}\n\n/**\n * Find the styleguide file path\n */\nexport function findStyleguidePath(\n startDir: string,\n explicitPath?: string\n): string | null {\n // Explicit path takes precedence\n if (explicitPath) {\n if (isAbsolute(explicitPath)) {\n return existsSync(explicitPath) ? explicitPath : null;\n }\n\n // For relative explicit paths, try:\n // 1) relative to the file dir (back-compat)\n // 2) relative to the nearest package root (typical \"project root\")\n // 3) relative to workspace root (monorepo root)\n const workspaceRoot = findWorkspaceRoot(startDir);\n const packageRoot = findNearestPackageRoot(startDir, workspaceRoot);\n\n const candidates = [\n resolve(startDir, explicitPath),\n resolve(packageRoot, explicitPath),\n resolve(workspaceRoot, explicitPath),\n ];\n\n for (const p of candidates) {\n if (existsSync(p)) return p;\n }\n\n return null;\n }\n\n // Check from start dir up to workspace root\n const workspaceRoot = findWorkspaceRoot(startDir);\n let dir = startDir;\n\n while (true) {\n for (const relativePath of DEFAULT_STYLEGUIDE_PATHS) {\n const fullPath = join(dir, relativePath);\n if (existsSync(fullPath)) {\n return fullPath;\n }\n }\n\n // Stop at workspace root\n if (dir === workspaceRoot) break;\n\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n\n return null;\n}\n\n/**\n * Load styleguide content from file\n */\nexport function loadStyleguide(\n startDir: string,\n explicitPath?: string\n): string | null {\n const path = findStyleguidePath(startDir, explicitPath);\n if (!path) return null;\n\n try {\n return readFileSync(path, \"utf-8\");\n } catch {\n return null;\n }\n}\n\n/**\n * Get styleguide path and content\n */\nexport function getStyleguide(\n startDir: string,\n explicitPath?: string\n): { path: string | null; content: string | null } {\n const path = findStyleguidePath(startDir, explicitPath);\n if (!path) return { path: null, content: null };\n\n try {\n const content = readFileSync(path, \"utf-8\");\n return { path, content };\n } catch {\n return { path, content: null };\n }\n}\n"],"mappings":"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","names":["existsSync","readFileSync","dirname","join","meta","existsSync","readFileSync","dirname","join","workspaceRoot","readFileSync","dirname","readFileSync","existsSync","join"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/create-rule.ts","../../src/rules/zustand-use-selectors.ts"],"sourcesContent":["/**\n * Rule creation helper using @typescript-eslint/utils\n */\n\nimport { ESLintUtils } from \"@typescript-eslint/utils\";\n\nexport const createRule = ESLintUtils.RuleCreator(\n (name) =>\n `https://github.com/peter-suggate/uilint/blob/main/packages/uilint-eslint/docs/rules/${name}.md`\n);\n\n/**\n * Schema for prompting user to configure a rule option in the CLI\n */\nexport interface OptionFieldSchema {\n /** Field name in the options object */\n key: string;\n /** Display label for the prompt */\n label: string;\n /** Prompt type */\n type: \"text\" | \"number\" | \"boolean\" | \"select\" | \"multiselect\";\n /** Default value */\n defaultValue: unknown;\n /** Placeholder text (for text/number inputs) */\n placeholder?: string;\n /** Options for select/multiselect */\n options?: Array<{ value: string | number; label: string }>;\n /** Description/hint for the field */\n description?: string;\n}\n\n/**\n * Schema describing how to prompt for rule options during installation\n */\nexport interface RuleOptionSchema {\n /** Fields that can be configured for this rule */\n fields: OptionFieldSchema[];\n}\n\n/**\n * External requirement that a rule needs to function\n */\nexport interface RuleRequirement {\n /** Requirement type for programmatic checks */\n type: \"ollama\" | \"git\" | \"coverage\" | \"semantic-index\" | \"styleguide\";\n /** Human-readable description */\n description: string;\n /** Optional: how to satisfy the requirement */\n setupHint?: string;\n}\n\n/**\n * Rule migration definition for updating rule options between versions\n */\nexport interface RuleMigration {\n /** Source version (semver) */\n from: string;\n /** Target version (semver) */\n to: string;\n /** Human-readable description of what changed */\n description: string;\n /** Function to migrate options from old format to new format */\n migrate: (oldOptions: unknown[]) => unknown[];\n /** Whether this migration contains breaking changes */\n breaking?: boolean;\n}\n\n/**\n * Colocated rule metadata - exported alongside each rule\n *\n * This structure keeps all rule metadata in the same file as the rule implementation,\n * making it easy to maintain and extend as new rules are added.\n */\nexport interface RuleMeta {\n /** Rule identifier (e.g., \"consistent-dark-mode\") - must match filename */\n id: string;\n\n /** Semantic version of the rule (e.g., \"1.0.0\") */\n version: string;\n\n /** Display name for CLI (e.g., \"No Arbitrary Tailwind\") */\n name: string;\n\n /** Short description for CLI selection prompts (one line) */\n description: string;\n\n /** Default severity level */\n defaultSeverity: \"error\" | \"warn\" | \"off\";\n\n /** Category for grouping in CLI */\n category: \"static\" | \"semantic\";\n\n /** Icon for display in CLI/UI (emoji or icon name) */\n icon?: string;\n\n /** Short hint about the rule type/requirements */\n hint?: string;\n\n /** Whether rule is enabled by default during install */\n defaultEnabled?: boolean;\n\n /** External requirements the rule needs */\n requirements?: RuleRequirement[];\n\n /** Instructions to show after installation */\n postInstallInstructions?: string;\n\n /** Framework compatibility */\n frameworks?: (\"next\" | \"vite\" | \"cra\" | \"remix\")[];\n\n /** Whether this rule requires a styleguide file */\n requiresStyleguide?: boolean;\n\n /** Default options for the rule (passed as second element in ESLint config) */\n defaultOptions?: unknown[];\n\n /** Schema for prompting user to configure options during install */\n optionSchema?: RuleOptionSchema;\n\n /**\n * Detailed documentation in markdown format.\n * Should include:\n * - What the rule does\n * - Why it's useful\n * - Examples of incorrect and correct code\n * - Configuration options explained\n */\n docs: string;\n\n /**\n * Internal utility dependencies that this rule requires.\n * When the rule is copied to a target project, these utilities\n * will be transformed to import from \"uilint-eslint\" instead\n * of relative paths.\n *\n * Example: [\"coverage-aggregator\", \"dependency-graph\"]\n */\n internalDependencies?: string[];\n\n /**\n * Whether this rule is directory-based (has lib/ folder with utilities).\n * Directory-based rules are installed as folders with index.ts and lib/ subdirectory.\n * Single-file rules are installed as single .ts files.\n *\n * When true, ESLint config imports will use:\n * ./.uilint/rules/rule-id/index.js\n * When false (default):\n * ./.uilint/rules/rule-id.js\n */\n isDirectoryBased?: boolean;\n\n /**\n * Migrations for updating rule options between versions.\n * Migrations are applied in order to transform options from older versions.\n */\n migrations?: RuleMigration[];\n\n /**\n * Which UI plugin should handle this rule.\n * Defaults based on category:\n * - \"static\" category → \"eslint\" plugin\n * - \"semantic\" category → \"semantic\" plugin\n *\n * Special cases:\n * - \"vision\" for semantic-vision rule\n */\n plugin?: \"eslint\" | \"vision\" | \"semantic\";\n\n /**\n * Custom inspector panel ID to use for this rule's issues.\n * If not specified, uses the plugin's default issue inspector.\n *\n * Examples:\n * - \"vision-issue\" for VisionIssueInspector\n * - \"duplicates\" for DuplicatesInspector\n * - \"semantic-issue\" for SemanticIssueInspector\n */\n customInspector?: string;\n\n /**\n * Custom heatmap color for this rule's issues.\n * CSS color value (hex, rgb, hsl, or named color).\n * If not specified, uses severity-based coloring.\n */\n heatmapColor?: string;\n}\n\n/**\n * Helper to define rule metadata with type safety\n */\nexport function defineRuleMeta(meta: RuleMeta): RuleMeta {\n return meta;\n}\n","/**\n * Rule: zustand-use-selectors\n *\n * Requires selector functions when accessing Zustand store state to prevent\n * unnecessary re-renders.\n *\n * Examples:\n * - Bad: const state = useStore()\n * - Bad: const { count } = useStore()\n * - Good: const count = useStore((s) => s.count)\n * - Good: const count = useStore(selectCount)\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\n\ntype MessageIds = \"missingSelector\" | \"useSelectorFunction\";\ntype Options = [\n {\n /** Regex pattern for store hook names (default: \"^use\\\\w*Store$\") */\n storePattern?: string;\n /** Allow useShallow() wrapper without selector */\n allowShallow?: boolean;\n /** Require named selector functions instead of inline arrows */\n requireNamedSelectors?: boolean;\n }\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"zustand-use-selectors\",\n version: \"1.0.0\",\n name: \"Zustand Use Selectors\",\n description: \"Require selector functions when accessing Zustand store state\",\n defaultSeverity: \"warn\",\n category: \"static\",\n icon: \"⚡\",\n hint: \"Prevents unnecessary re-renders\",\n defaultEnabled: true,\n defaultOptions: [\n { storePattern: \"^use\\\\w*Store$\", allowShallow: true, requireNamedSelectors: false },\n ],\n optionSchema: {\n fields: [\n {\n key: \"storePattern\",\n label: \"Store hook pattern\",\n type: \"text\",\n defaultValue: \"^use\\\\w*Store$\",\n description: \"Regex pattern for identifying Zustand store hooks\",\n },\n {\n key: \"allowShallow\",\n label: \"Allow useShallow\",\n type: \"boolean\",\n defaultValue: true,\n description: \"Allow useShallow() wrapper without explicit selector\",\n },\n {\n key: \"requireNamedSelectors\",\n label: \"Require named selectors\",\n type: \"boolean\",\n defaultValue: false,\n description: \"Require named selector functions instead of inline arrows\",\n },\n ],\n },\n docs: `\n## What it does\n\nEnforces the use of selector functions when accessing Zustand store state.\nWhen you call a Zustand store without a selector, your component subscribes\nto the entire store and re-renders on any state change.\n\n## Why it's useful\n\n- **Performance**: Prevents unnecessary re-renders\n- **Optimization**: Components only update when selected state changes\n- **Best Practice**: Follows Zustand's recommended patterns\n\n## Examples\n\n### ❌ Incorrect\n\n\\`\\`\\`tsx\n// Subscribes to entire store - re-renders on any change\nconst state = useStore();\nconst { count, user } = useStore();\n\n// Component re-renders when anything changes, not just count\nfunction Counter() {\n const { count } = useStore();\n return <span>{count}</span>;\n}\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n// Only re-renders when count changes\nconst count = useStore((state) => state.count);\n\n// Named selector\nconst selectCount = (state) => state.count;\nconst count = useStore(selectCount);\n\n// Multiple values with shallow\nimport { useShallow } from 'zustand/shallow';\nconst { count, user } = useStore(\n useShallow((state) => ({ count: state.count, user: state.user }))\n);\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/zustand-use-selectors\": [\"warn\", {\n storePattern: \"^use\\\\\\\\w*Store$\", // Match useXxxStore pattern\n allowShallow: true, // Allow useShallow without inline selector\n requireNamedSelectors: false // Allow inline arrow selectors\n}]\n\\`\\`\\`\n`,\n});\n\n/**\n * Check if a node is a Zustand store call based on the pattern\n */\nfunction isZustandStoreCall(\n callee: TSESTree.Node,\n storePattern: RegExp\n): boolean {\n if (callee.type === \"Identifier\") {\n return storePattern.test(callee.name);\n }\n return false;\n}\n\n/**\n * Check if the first argument is a selector function or reference\n */\nfunction hasSelector(args: TSESTree.CallExpressionArgument[]): boolean {\n if (args.length === 0) {\n return false;\n }\n\n const firstArg = args[0];\n\n // Arrow function: (s) => s.count\n if (firstArg.type === \"ArrowFunctionExpression\") {\n return true;\n }\n\n // Function expression: function(s) { return s.count; }\n if (firstArg.type === \"FunctionExpression\") {\n return true;\n }\n\n // Named selector reference: selectCount\n if (firstArg.type === \"Identifier\") {\n return true;\n }\n\n // Member expression: selectors.count or module.selectCount\n if (firstArg.type === \"MemberExpression\") {\n return true;\n }\n\n // Call expression (might be useShallow or similar)\n if (firstArg.type === \"CallExpression\") {\n return true;\n }\n\n return false;\n}\n\n/**\n * Check if the selector is wrapped in useShallow\n */\nfunction isShallowWrapped(args: TSESTree.CallExpressionArgument[]): boolean {\n if (args.length === 0) {\n return false;\n }\n\n const firstArg = args[0];\n\n if (firstArg.type === \"CallExpression\") {\n if (\n firstArg.callee.type === \"Identifier\" &&\n firstArg.callee.name === \"useShallow\"\n ) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Check if the selector is an inline arrow function\n */\nfunction isInlineSelector(args: TSESTree.CallExpressionArgument[]): boolean {\n if (args.length === 0) {\n return false;\n }\n\n const firstArg = args[0];\n return (\n firstArg.type === \"ArrowFunctionExpression\" ||\n firstArg.type === \"FunctionExpression\"\n );\n}\n\n/**\n * Get the store name from the call expression\n */\nfunction getStoreName(callee: TSESTree.Node): string {\n if (callee.type === \"Identifier\") {\n return callee.name;\n }\n return \"useStore\";\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"zustand-use-selectors\",\n meta: {\n type: \"problem\",\n docs: {\n description:\n \"Require selector functions when accessing Zustand store state\",\n },\n messages: {\n missingSelector:\n \"Call to '{{storeName}}' is missing a selector. Use '{{storeName}}((state) => state.property)' to prevent unnecessary re-renders.\",\n useSelectorFunction:\n \"Consider using a named selector function instead of an inline arrow for '{{storeName}}'. Example: '{{storeName}}(selectProperty)'\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n storePattern: {\n type: \"string\",\n description: \"Regex pattern for store hook names\",\n },\n allowShallow: {\n type: \"boolean\",\n description: \"Allow useShallow() wrapper\",\n },\n requireNamedSelectors: {\n type: \"boolean\",\n description: \"Require named selector functions\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [\n {\n storePattern: \"^use\\\\w*Store$\",\n allowShallow: true,\n requireNamedSelectors: false,\n },\n ],\n create(context) {\n const options = context.options[0] || {};\n const storePatternStr = options.storePattern ?? \"^use\\\\w*Store$\";\n const allowShallow = options.allowShallow ?? true;\n const requireNamedSelectors = options.requireNamedSelectors ?? false;\n\n let storePattern: RegExp;\n try {\n storePattern = new RegExp(storePatternStr);\n } catch {\n // If invalid regex, use default\n storePattern = /^use\\w*Store$/;\n }\n\n return {\n CallExpression(node) {\n // Check if this is a Zustand store call\n if (!isZustandStoreCall(node.callee, storePattern)) {\n return;\n }\n\n const storeName = getStoreName(node.callee);\n\n // Check for selector\n if (!hasSelector(node.arguments)) {\n context.report({\n node,\n messageId: \"missingSelector\",\n data: { storeName },\n });\n return;\n }\n\n // If useShallow is used and allowed, that's fine\n if (allowShallow && isShallowWrapped(node.arguments)) {\n return;\n }\n\n // Check for named selectors if required\n if (requireNamedSelectors && isInlineSelector(node.arguments)) {\n context.report({\n node,\n messageId: \"useSelectorFunction\",\n data: { storeName },\n });\n }\n },\n };\n },\n});\n"],"mappings":"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","names":["meta"]}
1
+ {"version":3,"sources":["../../src/utils/create-rule.ts","../../src/rules/zustand-use-selectors.ts"],"sourcesContent":["/**\n * Rule creation helper using @typescript-eslint/utils\n */\n\nimport { ESLintUtils } from \"@typescript-eslint/utils\";\n\nexport const createRule = ESLintUtils.RuleCreator(\n (name) =>\n `https://github.com/peter-suggate/uilint/blob/main/packages/uilint-eslint/docs/rules/${name}.md`\n);\n\n/**\n * Schema for prompting user to configure a rule option in the CLI\n */\nexport interface OptionFieldSchema {\n /** Field name in the options object */\n key: string;\n /** Display label for the prompt */\n label: string;\n /** Prompt type */\n type: \"text\" | \"number\" | \"boolean\" | \"select\" | \"multiselect\";\n /** Default value */\n defaultValue: unknown;\n /** Placeholder text (for text/number inputs) */\n placeholder?: string;\n /** Options for select/multiselect */\n options?: Array<{ value: string | number; label: string }>;\n /** Description/hint for the field */\n description?: string;\n}\n\n/**\n * Schema describing how to prompt for rule options during installation\n */\nexport interface RuleOptionSchema {\n /** Fields that can be configured for this rule */\n fields: OptionFieldSchema[];\n}\n\n/**\n * External requirement that a rule needs to function\n */\nexport interface RuleRequirement {\n /** Requirement type for programmatic checks */\n type: \"ollama\" | \"git\" | \"coverage\" | \"semantic-index\" | \"styleguide\";\n /** Human-readable description */\n description: string;\n /** Optional: how to satisfy the requirement */\n setupHint?: string;\n}\n\n/**\n * Rule migration definition for updating rule options between versions\n */\nexport interface RuleMigration {\n /** Source version (semver) */\n from: string;\n /** Target version (semver) */\n to: string;\n /** Human-readable description of what changed */\n description: string;\n /** Function to migrate options from old format to new format */\n migrate: (oldOptions: unknown[]) => unknown[];\n /** Whether this migration contains breaking changes */\n breaking?: boolean;\n}\n\n/**\n * Colocated rule metadata - exported alongside each rule\n *\n * This structure keeps all rule metadata in the same file as the rule implementation,\n * making it easy to maintain and extend as new rules are added.\n */\nexport interface RuleMeta {\n /** Rule identifier (e.g., \"consistent-dark-mode\") - must match filename */\n id: string;\n\n /** Semantic version of the rule (e.g., \"1.0.0\") */\n version: string;\n\n /** Display name for CLI (e.g., \"No Arbitrary Tailwind\") */\n name: string;\n\n /** Short description for CLI selection prompts (one line) */\n description: string;\n\n /** Default severity level */\n defaultSeverity: \"error\" | \"warn\" | \"off\";\n\n /** Category for grouping in CLI */\n category: \"static\" | \"semantic\";\n\n /** Icon for display in CLI/UI (emoji or icon name) */\n icon?: string;\n\n /** Short hint about the rule type/requirements */\n hint?: string;\n\n /** Whether rule is enabled by default during install */\n defaultEnabled?: boolean;\n\n /** External requirements the rule needs */\n requirements?: RuleRequirement[];\n\n /**\n * NPM packages that must be installed for this rule to work.\n * These will be added to the target project's dependencies during installation.\n *\n * Example: [\"xxhash-wasm\"] for rules using the xxhash library\n */\n npmDependencies?: string[];\n\n /** Instructions to show after installation */\n postInstallInstructions?: string;\n\n /** Framework compatibility */\n frameworks?: (\"next\" | \"vite\" | \"cra\" | \"remix\")[];\n\n /** Whether this rule requires a styleguide file */\n requiresStyleguide?: boolean;\n\n /** Default options for the rule (passed as second element in ESLint config) */\n defaultOptions?: unknown[];\n\n /** Schema for prompting user to configure options during install */\n optionSchema?: RuleOptionSchema;\n\n /**\n * Detailed documentation in markdown format.\n * Should include:\n * - What the rule does\n * - Why it's useful\n * - Examples of incorrect and correct code\n * - Configuration options explained\n */\n docs: string;\n\n /**\n * Internal utility dependencies that this rule requires.\n * When the rule is copied to a target project, these utilities\n * will be transformed to import from \"uilint-eslint\" instead\n * of relative paths.\n *\n * Example: [\"coverage-aggregator\", \"dependency-graph\"]\n */\n internalDependencies?: string[];\n\n /**\n * Whether this rule is directory-based (has lib/ folder with utilities).\n * Directory-based rules are installed as folders with index.ts and lib/ subdirectory.\n * Single-file rules are installed as single .ts files.\n *\n * When true, ESLint config imports will use:\n * ./.uilint/rules/rule-id/index.js\n * When false (default):\n * ./.uilint/rules/rule-id.js\n */\n isDirectoryBased?: boolean;\n\n /**\n * Migrations for updating rule options between versions.\n * Migrations are applied in order to transform options from older versions.\n */\n migrations?: RuleMigration[];\n\n /**\n * Which UI plugin should handle this rule.\n * Defaults based on category:\n * - \"static\" category → \"eslint\" plugin\n * - \"semantic\" category → \"semantic\" plugin\n *\n * Special cases:\n * - \"vision\" for semantic-vision rule\n */\n plugin?: \"eslint\" | \"vision\" | \"semantic\";\n\n /**\n * Custom inspector panel ID to use for this rule's issues.\n * If not specified, uses the plugin's default issue inspector.\n *\n * Examples:\n * - \"vision-issue\" for VisionIssueInspector\n * - \"duplicates\" for DuplicatesInspector\n * - \"semantic-issue\" for SemanticIssueInspector\n */\n customInspector?: string;\n\n /**\n * Custom heatmap color for this rule's issues.\n * CSS color value (hex, rgb, hsl, or named color).\n * If not specified, uses severity-based coloring.\n */\n heatmapColor?: string;\n}\n\n/**\n * Helper to define rule metadata with type safety\n */\nexport function defineRuleMeta(meta: RuleMeta): RuleMeta {\n return meta;\n}\n","/**\n * Rule: zustand-use-selectors\n *\n * Requires selector functions when accessing Zustand store state to prevent\n * unnecessary re-renders.\n *\n * Examples:\n * - Bad: const state = useStore()\n * - Bad: const { count } = useStore()\n * - Good: const count = useStore((s) => s.count)\n * - Good: const count = useStore(selectCount)\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\n\ntype MessageIds = \"missingSelector\" | \"useSelectorFunction\";\ntype Options = [\n {\n /** Regex pattern for store hook names (default: \"^use\\\\w*Store$\") */\n storePattern?: string;\n /** Allow useShallow() wrapper without selector */\n allowShallow?: boolean;\n /** Require named selector functions instead of inline arrows */\n requireNamedSelectors?: boolean;\n }\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"zustand-use-selectors\",\n version: \"1.0.0\",\n name: \"Zustand Use Selectors\",\n description: \"Require selector functions when accessing Zustand store state\",\n defaultSeverity: \"warn\",\n category: \"static\",\n icon: \"⚡\",\n hint: \"Prevents unnecessary re-renders\",\n defaultEnabled: true,\n defaultOptions: [\n { storePattern: \"^use\\\\w*Store$\", allowShallow: true, requireNamedSelectors: false },\n ],\n optionSchema: {\n fields: [\n {\n key: \"storePattern\",\n label: \"Store hook pattern\",\n type: \"text\",\n defaultValue: \"^use\\\\w*Store$\",\n description: \"Regex pattern for identifying Zustand store hooks\",\n },\n {\n key: \"allowShallow\",\n label: \"Allow useShallow\",\n type: \"boolean\",\n defaultValue: true,\n description: \"Allow useShallow() wrapper without explicit selector\",\n },\n {\n key: \"requireNamedSelectors\",\n label: \"Require named selectors\",\n type: \"boolean\",\n defaultValue: false,\n description: \"Require named selector functions instead of inline arrows\",\n },\n ],\n },\n docs: `\n## What it does\n\nEnforces the use of selector functions when accessing Zustand store state.\nWhen you call a Zustand store without a selector, your component subscribes\nto the entire store and re-renders on any state change.\n\n## Why it's useful\n\n- **Performance**: Prevents unnecessary re-renders\n- **Optimization**: Components only update when selected state changes\n- **Best Practice**: Follows Zustand's recommended patterns\n\n## Examples\n\n### ❌ Incorrect\n\n\\`\\`\\`tsx\n// Subscribes to entire store - re-renders on any change\nconst state = useStore();\nconst { count, user } = useStore();\n\n// Component re-renders when anything changes, not just count\nfunction Counter() {\n const { count } = useStore();\n return <span>{count}</span>;\n}\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n// Only re-renders when count changes\nconst count = useStore((state) => state.count);\n\n// Named selector\nconst selectCount = (state) => state.count;\nconst count = useStore(selectCount);\n\n// Multiple values with shallow\nimport { useShallow } from 'zustand/shallow';\nconst { count, user } = useStore(\n useShallow((state) => ({ count: state.count, user: state.user }))\n);\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/zustand-use-selectors\": [\"warn\", {\n storePattern: \"^use\\\\\\\\w*Store$\", // Match useXxxStore pattern\n allowShallow: true, // Allow useShallow without inline selector\n requireNamedSelectors: false // Allow inline arrow selectors\n}]\n\\`\\`\\`\n`,\n});\n\n/**\n * Check if a node is a Zustand store call based on the pattern\n */\nfunction isZustandStoreCall(\n callee: TSESTree.Node,\n storePattern: RegExp\n): boolean {\n if (callee.type === \"Identifier\") {\n return storePattern.test(callee.name);\n }\n return false;\n}\n\n/**\n * Check if the first argument is a selector function or reference\n */\nfunction hasSelector(args: TSESTree.CallExpressionArgument[]): boolean {\n if (args.length === 0) {\n return false;\n }\n\n const firstArg = args[0];\n\n // Arrow function: (s) => s.count\n if (firstArg.type === \"ArrowFunctionExpression\") {\n return true;\n }\n\n // Function expression: function(s) { return s.count; }\n if (firstArg.type === \"FunctionExpression\") {\n return true;\n }\n\n // Named selector reference: selectCount\n if (firstArg.type === \"Identifier\") {\n return true;\n }\n\n // Member expression: selectors.count or module.selectCount\n if (firstArg.type === \"MemberExpression\") {\n return true;\n }\n\n // Call expression (might be useShallow or similar)\n if (firstArg.type === \"CallExpression\") {\n return true;\n }\n\n return false;\n}\n\n/**\n * Check if the selector is wrapped in useShallow\n */\nfunction isShallowWrapped(args: TSESTree.CallExpressionArgument[]): boolean {\n if (args.length === 0) {\n return false;\n }\n\n const firstArg = args[0];\n\n if (firstArg.type === \"CallExpression\") {\n if (\n firstArg.callee.type === \"Identifier\" &&\n firstArg.callee.name === \"useShallow\"\n ) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Check if the selector is an inline arrow function\n */\nfunction isInlineSelector(args: TSESTree.CallExpressionArgument[]): boolean {\n if (args.length === 0) {\n return false;\n }\n\n const firstArg = args[0];\n return (\n firstArg.type === \"ArrowFunctionExpression\" ||\n firstArg.type === \"FunctionExpression\"\n );\n}\n\n/**\n * Get the store name from the call expression\n */\nfunction getStoreName(callee: TSESTree.Node): string {\n if (callee.type === \"Identifier\") {\n return callee.name;\n }\n return \"useStore\";\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"zustand-use-selectors\",\n meta: {\n type: \"problem\",\n docs: {\n description:\n \"Require selector functions when accessing Zustand store state\",\n },\n messages: {\n missingSelector:\n \"Call to '{{storeName}}' is missing a selector. Use '{{storeName}}((state) => state.property)' to prevent unnecessary re-renders.\",\n useSelectorFunction:\n \"Consider using a named selector function instead of an inline arrow for '{{storeName}}'. Example: '{{storeName}}(selectProperty)'\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n storePattern: {\n type: \"string\",\n description: \"Regex pattern for store hook names\",\n },\n allowShallow: {\n type: \"boolean\",\n description: \"Allow useShallow() wrapper\",\n },\n requireNamedSelectors: {\n type: \"boolean\",\n description: \"Require named selector functions\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [\n {\n storePattern: \"^use\\\\w*Store$\",\n allowShallow: true,\n requireNamedSelectors: false,\n },\n ],\n create(context) {\n const options = context.options[0] || {};\n const storePatternStr = options.storePattern ?? \"^use\\\\w*Store$\";\n const allowShallow = options.allowShallow ?? true;\n const requireNamedSelectors = options.requireNamedSelectors ?? false;\n\n let storePattern: RegExp;\n try {\n storePattern = new RegExp(storePatternStr);\n } catch {\n // If invalid regex, use default\n storePattern = /^use\\w*Store$/;\n }\n\n return {\n CallExpression(node) {\n // Check if this is a Zustand store call\n if (!isZustandStoreCall(node.callee, storePattern)) {\n return;\n }\n\n const storeName = getStoreName(node.callee);\n\n // Check for selector\n if (!hasSelector(node.arguments)) {\n context.report({\n node,\n messageId: \"missingSelector\",\n data: { storeName },\n });\n return;\n }\n\n // If useShallow is used and allowed, that's fine\n if (allowShallow && isShallowWrapped(node.arguments)) {\n return;\n }\n\n // Check for named selectors if required\n if (requireNamedSelectors && isInlineSelector(node.arguments)) {\n context.report({\n node,\n messageId: \"useSelectorFunction\",\n data: { storeName },\n });\n }\n },\n };\n },\n});\n"],"mappings":"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","names":["meta"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uilint-eslint",
3
- "version": "0.2.76",
3
+ "version": "0.2.77",
4
4
  "description": "ESLint plugin for UILint - AI-powered UI consistency checking",
5
5
  "author": "Peter Suggate",
6
6
  "repository": {
@@ -35,7 +35,7 @@
35
35
  "@typescript-eslint/utils": "^8.35.1",
36
36
  "oxc-resolver": "^11.0.0",
37
37
  "xxhash-wasm": "^1.1.0",
38
- "uilint-core": "0.2.76"
38
+ "uilint-core": "0.2.77"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/eslint": "^9.6.1",
@@ -55,6 +55,7 @@ export const meta = defineRuleMeta({
55
55
  setupHint: "Run: uilint genstyleguide",
56
56
  },
57
57
  ],
58
+ npmDependencies: ["xxhash-wasm"],
58
59
  defaultOptions: [{ model: "qwen3-coder:30b", styleguidePath: ".uilint/styleguide.md" }],
59
60
  optionSchema: {
60
61
  fields: [
@@ -102,6 +102,14 @@ export interface RuleMeta {
102
102
  /** External requirements the rule needs */
103
103
  requirements?: RuleRequirement[];
104
104
 
105
+ /**
106
+ * NPM packages that must be installed for this rule to work.
107
+ * These will be added to the target project's dependencies during installation.
108
+ *
109
+ * Example: ["xxhash-wasm"] for rules using the xxhash library
110
+ */
111
+ npmDependencies?: string[];
112
+
105
113
  /** Instructions to show after installation */
106
114
  postInstallInstructions?: string;
107
115