uilint-eslint 0.2.76 → 0.2.78

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/utils/create-rule.ts","../../src/rules/consistent-dark-mode.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: consistent-dark-mode\n *\n * Ensures consistent dark mode theming in Tailwind CSS classes.\n * - Error: When some color classes have dark: variants but others don't within the same element\n * - Warning: When Tailwind color classes are used in a file but no dark: theming exists\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\n\ntype MessageIds = \"inconsistentDarkMode\" | \"missingDarkMode\";\ntype Options = [\n {\n /** Whether to warn when no dark mode classes are found in a file that uses Tailwind colors. Default: true */\n warnOnMissingDarkMode?: boolean;\n }?\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"consistent-dark-mode\",\n version: \"1.0.0\",\n name: \"Consistent Dark Mode\",\n description: \"Ensure consistent dark: theming (error on mix, warn on missing)\",\n defaultSeverity: \"error\",\n category: \"static\",\n icon: \"🌓\",\n hint: \"Ensures dark mode consistency\",\n defaultEnabled: true,\n defaultOptions: [{ warnOnMissingDarkMode: true }],\n optionSchema: {\n fields: [\n {\n key: \"warnOnMissingDarkMode\",\n label: \"Warn when elements lack dark: variant\",\n type: \"boolean\",\n defaultValue: true,\n description: \"Enable warnings for elements missing dark mode variants\",\n },\n ],\n },\n docs: `\n## What it does\n\nDetects inconsistent dark mode theming in Tailwind CSS classes. Reports errors when\nsome color classes in an element have \\`dark:\\` variants but others don't, and optionally\nwarns when a file uses color classes without any dark mode theming.\n\n## Why it's useful\n\n- **Prevents broken dark mode**: Catches cases where some colors change in dark mode but others don't\n- **Encourages completeness**: Prompts you to add dark mode support where it's missing\n- **No false positives**: Only flags explicit Tailwind colors, not custom/CSS variable colors\n\n## Examples\n\n### ❌ Incorrect\n\n\\`\\`\\`tsx\n// Some colors have dark variants, others don't\n<div className=\"bg-white dark:bg-slate-900 text-black\">\n// ^^^^^^^^^ missing dark: variant\n\n// Mix of themed and unthemed\n<button className=\"bg-blue-500 dark:bg-blue-600 border-gray-300\">\n// ^^^^^^^^^^^^^^^ missing dark: variant\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n// All color classes have dark variants\n<div className=\"bg-white dark:bg-slate-900 text-black dark:text-white\">\n\n// Using semantic/custom colors (automatically themed via CSS variables)\n<div className=\"bg-background text-foreground\">\n<div className=\"bg-brand text-brand-foreground\">\n<div className=\"bg-primary text-primary-foreground\">\n\n// Consistent theming\n<button className=\"bg-blue-500 dark:bg-blue-600 border-gray-300 dark:border-gray-600\">\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/consistent-dark-mode\": [\"error\", {\n warnOnMissingDarkMode: true // Warn if file uses colors without any dark mode\n}]\n\\`\\`\\`\n\n## Notes\n\n- Only explicit Tailwind colors (like \\`blue-500\\`, \\`white\\`, \\`slate-900\\`) require dark variants\n- Custom/semantic colors (\\`background\\`, \\`foreground\\`, \\`brand\\`, \\`primary\\`, etc.) are exempt\n- These are assumed to be CSS variables that handle dark mode automatically\n- Transparent, inherit, and current values are exempt\n- Non-color utilities (like \\`text-lg\\`, \\`border-2\\`) are correctly ignored\n`,\n});\n\n// Color-related class prefixes that should have dark mode variants\nconst COLOR_PREFIXES = [\n \"bg-\",\n \"text-\",\n \"border-\",\n \"border-t-\",\n \"border-r-\",\n \"border-b-\",\n \"border-l-\",\n \"border-x-\",\n \"border-y-\",\n \"ring-\",\n \"ring-offset-\",\n \"divide-\",\n \"outline-\",\n \"shadow-\",\n \"accent-\",\n \"caret-\",\n \"fill-\",\n \"stroke-\",\n \"decoration-\",\n \"placeholder-\",\n \"from-\",\n \"via-\",\n \"to-\",\n];\n\n// Values that don't need dark variants (colorless or inherited)\nconst EXEMPT_SUFFIXES = [\"transparent\", \"inherit\", \"current\", \"auto\", \"none\"];\n\n// Built-in Tailwind CSS color palette names\n// These are the ONLY colors that should trigger dark mode warnings.\n// Custom colors (like 'brand', 'company-primary') are assumed to be\n// CSS variables that handle dark mode automatically.\nconst TAILWIND_COLOR_NAMES = new Set([\n // Special colors\n \"white\",\n \"black\",\n // Gray scale palettes\n \"slate\",\n \"gray\",\n \"zinc\",\n \"neutral\",\n \"stone\",\n // Warm colors\n \"red\",\n \"orange\",\n \"amber\",\n \"yellow\",\n // Green colors\n \"lime\",\n \"green\",\n \"emerald\",\n \"teal\",\n // Blue colors\n \"cyan\",\n \"sky\",\n \"blue\",\n \"indigo\",\n // Purple/Pink colors\n \"violet\",\n \"purple\",\n \"fuchsia\",\n \"pink\",\n \"rose\",\n]);\n\n/**\n * Check if a class has 'dark' in its variant chain\n */\nfunction hasDarkVariant(className: string): boolean {\n const parts = className.split(\":\");\n // All parts except the last are variants\n const variants = parts.slice(0, -1);\n return variants.includes(\"dark\");\n}\n\n/**\n * Get the base class (without any variants like hover:, dark:, md:, etc.)\n */\nfunction getBaseClass(className: string): string {\n const parts = className.split(\":\");\n return parts[parts.length - 1] || \"\";\n}\n\n/**\n * Find the color prefix this class uses, if any\n */\nfunction getColorPrefix(baseClass: string): string | null {\n // Sort by length descending to match more specific prefixes first\n // (e.g., \"border-t-\" before \"border-\")\n const sortedPrefixes = [...COLOR_PREFIXES].sort(\n (a, b) => b.length - a.length\n );\n return sortedPrefixes.find((p) => baseClass.startsWith(p)) || null;\n}\n\n/**\n * Check if the value is an explicit Tailwind color.\n * Uses an allowlist approach: only built-in Tailwind color names trigger warnings.\n * Custom colors (like 'brand', 'primary', 'company-blue') are assumed to be\n * CSS variables that handle dark mode automatically and should NOT trigger.\n *\n * Matches patterns like:\n * - white, black (standalone colors)\n * - blue-500, slate-900 (color-scale)\n * - blue-500/50, gray-900/80 (with opacity modifier)\n */\nfunction isTailwindColor(value: string): boolean {\n // Remove opacity modifier if present (e.g., \"blue-500/50\" -> \"blue-500\")\n const valueWithoutOpacity = value.split(\"/\")[0] || value;\n\n // Check for standalone colors (white, black)\n if (TAILWIND_COLOR_NAMES.has(valueWithoutOpacity)) {\n return true;\n }\n\n // Check for color-scale pattern (e.g., \"blue-500\", \"slate-900\")\n // Pattern: colorName-number where number is 50, 100, 200, ..., 950\n const match = valueWithoutOpacity.match(/^([a-z]+)-(\\d+)$/);\n if (match) {\n const colorName = match[1];\n const scale = match[2];\n // Valid Tailwind scales are: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950\n const validScales = [\n \"50\",\n \"100\",\n \"200\",\n \"300\",\n \"400\",\n \"500\",\n \"600\",\n \"700\",\n \"800\",\n \"900\",\n \"950\",\n ];\n if (colorName && TAILWIND_COLOR_NAMES.has(colorName) && validScales.includes(scale || \"\")) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Check if the value after the prefix looks like an explicit Tailwind color.\n * Uses allowlist approach: only built-in Tailwind colors should trigger dark mode warnings.\n * Custom/semantic colors (brand, primary, foreground, etc.) are NOT flagged.\n */\nfunction isColorValue(baseClass: string, prefix: string): boolean {\n const value = baseClass.slice(prefix.length);\n\n // Empty value is not a color\n if (!value) {\n return false;\n }\n\n // Only flag explicit Tailwind colors\n // Custom colors, CSS variable colors, and semantic colors are exempt\n return isTailwindColor(value);\n}\n\n/**\n * Check if a class is exempt from dark mode requirements\n */\nfunction isExempt(baseClass: string): boolean {\n return EXEMPT_SUFFIXES.some((suffix) => baseClass.endsWith(suffix));\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"consistent-dark-mode\",\n meta: {\n type: \"problem\",\n docs: {\n description: \"Ensure consistent dark mode theming in Tailwind classes\",\n },\n messages: {\n inconsistentDarkMode:\n \"Inconsistent dark mode: '{{unthemed}}' lack dark: variants while other color classes have them.\",\n missingDarkMode:\n \"No dark mode theming detected. Consider adding dark: variants for color classes.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n warnOnMissingDarkMode: {\n type: \"boolean\",\n description:\n \"Whether to warn when no dark mode classes are found in a file that uses Tailwind colors\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{ warnOnMissingDarkMode: true }],\n create(context) {\n const options = context.options[0] || {};\n const warnOnMissingDarkMode = options.warnOnMissingDarkMode ?? true;\n\n let fileHasColorClasses = false;\n let fileHasDarkMode = false;\n const reportedNodes = new Set<TSESTree.Node>();\n\n function checkClassString(node: TSESTree.Node, classString: string) {\n const classes = classString.split(/\\s+/).filter(Boolean);\n if (classes.length === 0) return;\n\n // Track usage per color prefix: { hasLight, hasDark, lightClasses }\n const prefixUsage = new Map<\n string,\n { hasLight: boolean; hasDark: boolean; lightClasses: string[] }\n >();\n\n for (const cls of classes) {\n const baseClass = getBaseClass(cls);\n const prefix = getColorPrefix(baseClass);\n\n if (!prefix) continue;\n if (isExempt(baseClass)) continue;\n\n // Verify this is actually a color class, not something like text-lg\n if (!isColorValue(baseClass, prefix)) continue;\n\n if (!prefixUsage.has(prefix)) {\n prefixUsage.set(prefix, {\n hasLight: false,\n hasDark: false,\n lightClasses: [],\n });\n }\n\n const usage = prefixUsage.get(prefix)!;\n\n if (hasDarkVariant(cls)) {\n usage.hasDark = true;\n fileHasDarkMode = true;\n } else {\n usage.hasLight = true;\n usage.lightClasses.push(cls);\n }\n }\n\n // Track if file uses color classes\n if (prefixUsage.size > 0) {\n fileHasColorClasses = true;\n }\n\n // Check for inconsistency: some prefixes have dark variants, others don't\n const entries = Array.from(prefixUsage.entries());\n const hasSomeDark = entries.some(([_, u]) => u.hasDark);\n\n if (hasSomeDark) {\n const unthemedEntries = entries.filter(\n ([_, usage]) => usage.hasLight && !usage.hasDark\n );\n\n if (unthemedEntries.length > 0 && !reportedNodes.has(node)) {\n reportedNodes.add(node);\n // Collect the actual class names that lack dark variants\n const unthemedClasses = unthemedEntries.flatMap(\n ([_, u]) => u.lightClasses\n );\n\n context.report({\n node,\n messageId: \"inconsistentDarkMode\",\n data: { unthemed: unthemedClasses.join(\", \") },\n });\n }\n }\n }\n\n function processStringValue(node: TSESTree.Node, value: string) {\n checkClassString(node, value);\n }\n\n function processTemplateLiteral(node: TSESTree.TemplateLiteral) {\n for (const quasi of node.quasis) {\n checkClassString(quasi, quasi.value.raw);\n }\n }\n\n return {\n // Check className attributes in JSX\n JSXAttribute(node) {\n if (\n node.name.type === \"JSXIdentifier\" &&\n (node.name.name === \"className\" || node.name.name === \"class\")\n ) {\n const value = node.value;\n\n // Handle string literal: className=\"...\"\n if (value?.type === \"Literal\" && typeof value.value === \"string\") {\n processStringValue(value, value.value);\n }\n\n // Handle JSX expression: className={...}\n if (value?.type === \"JSXExpressionContainer\") {\n const expr = value.expression;\n\n // Direct string: className={\"...\"}\n if (expr.type === \"Literal\" && typeof expr.value === \"string\") {\n processStringValue(expr, expr.value);\n }\n\n // Template literal: className={`...`}\n if (expr.type === \"TemplateLiteral\") {\n processTemplateLiteral(expr);\n }\n }\n }\n },\n\n // Check cn(), clsx(), classnames(), cva() calls\n CallExpression(node) {\n if (node.callee.type !== \"Identifier\") return;\n const name = node.callee.name;\n\n if (\n name === \"cn\" ||\n name === \"clsx\" ||\n name === \"classnames\" ||\n name === \"cva\" ||\n name === \"twMerge\"\n ) {\n for (const arg of node.arguments) {\n if (arg.type === \"Literal\" && typeof arg.value === \"string\") {\n processStringValue(arg, arg.value);\n }\n if (arg.type === \"TemplateLiteral\") {\n processTemplateLiteral(arg);\n }\n // Handle arrays of class strings\n if (arg.type === \"ArrayExpression\") {\n for (const element of arg.elements) {\n if (\n element?.type === \"Literal\" &&\n typeof element.value === \"string\"\n ) {\n processStringValue(element, element.value);\n }\n if (element?.type === \"TemplateLiteral\") {\n processTemplateLiteral(element);\n }\n }\n }\n }\n }\n },\n\n // At the end of the file, check if Tailwind colors are used without any dark mode\n \"Program:exit\"(node) {\n if (warnOnMissingDarkMode && fileHasColorClasses && !fileHasDarkMode) {\n context.report({\n node,\n messageId: \"missingDarkMode\",\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/consistent-dark-mode.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: consistent-dark-mode\n *\n * Ensures consistent dark mode theming in Tailwind CSS classes.\n * - Error: When some color classes have dark: variants but others don't within the same element\n * - Warning: When Tailwind color classes are used in a file but no dark: theming exists\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\n\ntype MessageIds = \"inconsistentDarkMode\" | \"missingDarkMode\";\ntype Options = [\n {\n /** Whether to warn when no dark mode classes are found in a file that uses Tailwind colors. Default: true */\n warnOnMissingDarkMode?: boolean;\n }?\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"consistent-dark-mode\",\n version: \"1.0.0\",\n name: \"Consistent Dark Mode\",\n description: \"Ensure consistent dark: theming (error on mix, warn on missing)\",\n defaultSeverity: \"error\",\n category: \"static\",\n icon: \"🌓\",\n hint: \"Ensures dark mode consistency\",\n defaultEnabled: true,\n defaultOptions: [{ warnOnMissingDarkMode: true }],\n optionSchema: {\n fields: [\n {\n key: \"warnOnMissingDarkMode\",\n label: \"Warn when elements lack dark: variant\",\n type: \"boolean\",\n defaultValue: true,\n description: \"Enable warnings for elements missing dark mode variants\",\n },\n ],\n },\n docs: `\n## What it does\n\nDetects inconsistent dark mode theming in Tailwind CSS classes. Reports errors when\nsome color classes in an element have \\`dark:\\` variants but others don't, and optionally\nwarns when a file uses color classes without any dark mode theming.\n\n## Why it's useful\n\n- **Prevents broken dark mode**: Catches cases where some colors change in dark mode but others don't\n- **Encourages completeness**: Prompts you to add dark mode support where it's missing\n- **No false positives**: Only flags explicit Tailwind colors, not custom/CSS variable colors\n\n## Examples\n\n### ❌ Incorrect\n\n\\`\\`\\`tsx\n// Some colors have dark variants, others don't\n<div className=\"bg-white dark:bg-slate-900 text-black\">\n// ^^^^^^^^^ missing dark: variant\n\n// Mix of themed and unthemed\n<button className=\"bg-blue-500 dark:bg-blue-600 border-gray-300\">\n// ^^^^^^^^^^^^^^^ missing dark: variant\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n// All color classes have dark variants\n<div className=\"bg-white dark:bg-slate-900 text-black dark:text-white\">\n\n// Using semantic/custom colors (automatically themed via CSS variables)\n<div className=\"bg-background text-foreground\">\n<div className=\"bg-brand text-brand-foreground\">\n<div className=\"bg-primary text-primary-foreground\">\n\n// Consistent theming\n<button className=\"bg-blue-500 dark:bg-blue-600 border-gray-300 dark:border-gray-600\">\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/consistent-dark-mode\": [\"error\", {\n warnOnMissingDarkMode: true // Warn if file uses colors without any dark mode\n}]\n\\`\\`\\`\n\n## Notes\n\n- Only explicit Tailwind colors (like \\`blue-500\\`, \\`white\\`, \\`slate-900\\`) require dark variants\n- Custom/semantic colors (\\`background\\`, \\`foreground\\`, \\`brand\\`, \\`primary\\`, etc.) are exempt\n- These are assumed to be CSS variables that handle dark mode automatically\n- Transparent, inherit, and current values are exempt\n- Non-color utilities (like \\`text-lg\\`, \\`border-2\\`) are correctly ignored\n`,\n});\n\n// Color-related class prefixes that should have dark mode variants\nconst COLOR_PREFIXES = [\n \"bg-\",\n \"text-\",\n \"border-\",\n \"border-t-\",\n \"border-r-\",\n \"border-b-\",\n \"border-l-\",\n \"border-x-\",\n \"border-y-\",\n \"ring-\",\n \"ring-offset-\",\n \"divide-\",\n \"outline-\",\n \"shadow-\",\n \"accent-\",\n \"caret-\",\n \"fill-\",\n \"stroke-\",\n \"decoration-\",\n \"placeholder-\",\n \"from-\",\n \"via-\",\n \"to-\",\n];\n\n// Values that don't need dark variants (colorless or inherited)\nconst EXEMPT_SUFFIXES = [\"transparent\", \"inherit\", \"current\", \"auto\", \"none\"];\n\n// Built-in Tailwind CSS color palette names\n// These are the ONLY colors that should trigger dark mode warnings.\n// Custom colors (like 'brand', 'company-primary') are assumed to be\n// CSS variables that handle dark mode automatically.\nconst TAILWIND_COLOR_NAMES = new Set([\n // Special colors\n \"white\",\n \"black\",\n // Gray scale palettes\n \"slate\",\n \"gray\",\n \"zinc\",\n \"neutral\",\n \"stone\",\n // Warm colors\n \"red\",\n \"orange\",\n \"amber\",\n \"yellow\",\n // Green colors\n \"lime\",\n \"green\",\n \"emerald\",\n \"teal\",\n // Blue colors\n \"cyan\",\n \"sky\",\n \"blue\",\n \"indigo\",\n // Purple/Pink colors\n \"violet\",\n \"purple\",\n \"fuchsia\",\n \"pink\",\n \"rose\",\n]);\n\n/**\n * Check if a class has 'dark' in its variant chain\n */\nfunction hasDarkVariant(className: string): boolean {\n const parts = className.split(\":\");\n // All parts except the last are variants\n const variants = parts.slice(0, -1);\n return variants.includes(\"dark\");\n}\n\n/**\n * Get the base class (without any variants like hover:, dark:, md:, etc.)\n */\nfunction getBaseClass(className: string): string {\n const parts = className.split(\":\");\n return parts[parts.length - 1] || \"\";\n}\n\n/**\n * Find the color prefix this class uses, if any\n */\nfunction getColorPrefix(baseClass: string): string | null {\n // Sort by length descending to match more specific prefixes first\n // (e.g., \"border-t-\" before \"border-\")\n const sortedPrefixes = [...COLOR_PREFIXES].sort(\n (a, b) => b.length - a.length\n );\n return sortedPrefixes.find((p) => baseClass.startsWith(p)) || null;\n}\n\n/**\n * Check if the value is an explicit Tailwind color.\n * Uses an allowlist approach: only built-in Tailwind color names trigger warnings.\n * Custom colors (like 'brand', 'primary', 'company-blue') are assumed to be\n * CSS variables that handle dark mode automatically and should NOT trigger.\n *\n * Matches patterns like:\n * - white, black (standalone colors)\n * - blue-500, slate-900 (color-scale)\n * - blue-500/50, gray-900/80 (with opacity modifier)\n */\nfunction isTailwindColor(value: string): boolean {\n // Remove opacity modifier if present (e.g., \"blue-500/50\" -> \"blue-500\")\n const valueWithoutOpacity = value.split(\"/\")[0] || value;\n\n // Check for standalone colors (white, black)\n if (TAILWIND_COLOR_NAMES.has(valueWithoutOpacity)) {\n return true;\n }\n\n // Check for color-scale pattern (e.g., \"blue-500\", \"slate-900\")\n // Pattern: colorName-number where number is 50, 100, 200, ..., 950\n const match = valueWithoutOpacity.match(/^([a-z]+)-(\\d+)$/);\n if (match) {\n const colorName = match[1];\n const scale = match[2];\n // Valid Tailwind scales are: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950\n const validScales = [\n \"50\",\n \"100\",\n \"200\",\n \"300\",\n \"400\",\n \"500\",\n \"600\",\n \"700\",\n \"800\",\n \"900\",\n \"950\",\n ];\n if (colorName && TAILWIND_COLOR_NAMES.has(colorName) && validScales.includes(scale || \"\")) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Check if the value after the prefix looks like an explicit Tailwind color.\n * Uses allowlist approach: only built-in Tailwind colors should trigger dark mode warnings.\n * Custom/semantic colors (brand, primary, foreground, etc.) are NOT flagged.\n */\nfunction isColorValue(baseClass: string, prefix: string): boolean {\n const value = baseClass.slice(prefix.length);\n\n // Empty value is not a color\n if (!value) {\n return false;\n }\n\n // Only flag explicit Tailwind colors\n // Custom colors, CSS variable colors, and semantic colors are exempt\n return isTailwindColor(value);\n}\n\n/**\n * Check if a class is exempt from dark mode requirements\n */\nfunction isExempt(baseClass: string): boolean {\n return EXEMPT_SUFFIXES.some((suffix) => baseClass.endsWith(suffix));\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"consistent-dark-mode\",\n meta: {\n type: \"problem\",\n docs: {\n description: \"Ensure consistent dark mode theming in Tailwind classes\",\n },\n messages: {\n inconsistentDarkMode:\n \"Inconsistent dark mode: '{{unthemed}}' lack dark: variants while other color classes have them.\",\n missingDarkMode:\n \"No dark mode theming detected. Consider adding dark: variants for color classes.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n warnOnMissingDarkMode: {\n type: \"boolean\",\n description:\n \"Whether to warn when no dark mode classes are found in a file that uses Tailwind colors\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{ warnOnMissingDarkMode: true }],\n create(context) {\n const options = context.options[0] || {};\n const warnOnMissingDarkMode = options.warnOnMissingDarkMode ?? true;\n\n let fileHasColorClasses = false;\n let fileHasDarkMode = false;\n const reportedNodes = new Set<TSESTree.Node>();\n\n function checkClassString(node: TSESTree.Node, classString: string) {\n const classes = classString.split(/\\s+/).filter(Boolean);\n if (classes.length === 0) return;\n\n // Track usage per color prefix: { hasLight, hasDark, lightClasses }\n const prefixUsage = new Map<\n string,\n { hasLight: boolean; hasDark: boolean; lightClasses: string[] }\n >();\n\n for (const cls of classes) {\n const baseClass = getBaseClass(cls);\n const prefix = getColorPrefix(baseClass);\n\n if (!prefix) continue;\n if (isExempt(baseClass)) continue;\n\n // Verify this is actually a color class, not something like text-lg\n if (!isColorValue(baseClass, prefix)) continue;\n\n if (!prefixUsage.has(prefix)) {\n prefixUsage.set(prefix, {\n hasLight: false,\n hasDark: false,\n lightClasses: [],\n });\n }\n\n const usage = prefixUsage.get(prefix)!;\n\n if (hasDarkVariant(cls)) {\n usage.hasDark = true;\n fileHasDarkMode = true;\n } else {\n usage.hasLight = true;\n usage.lightClasses.push(cls);\n }\n }\n\n // Track if file uses color classes\n if (prefixUsage.size > 0) {\n fileHasColorClasses = true;\n }\n\n // Check for inconsistency: some prefixes have dark variants, others don't\n const entries = Array.from(prefixUsage.entries());\n const hasSomeDark = entries.some(([_, u]) => u.hasDark);\n\n if (hasSomeDark) {\n const unthemedEntries = entries.filter(\n ([_, usage]) => usage.hasLight && !usage.hasDark\n );\n\n if (unthemedEntries.length > 0 && !reportedNodes.has(node)) {\n reportedNodes.add(node);\n // Collect the actual class names that lack dark variants\n const unthemedClasses = unthemedEntries.flatMap(\n ([_, u]) => u.lightClasses\n );\n\n context.report({\n node,\n messageId: \"inconsistentDarkMode\",\n data: { unthemed: unthemedClasses.join(\", \") },\n });\n }\n }\n }\n\n function processStringValue(node: TSESTree.Node, value: string) {\n checkClassString(node, value);\n }\n\n function processTemplateLiteral(node: TSESTree.TemplateLiteral) {\n for (const quasi of node.quasis) {\n checkClassString(quasi, quasi.value.raw);\n }\n }\n\n return {\n // Check className attributes in JSX\n JSXAttribute(node) {\n if (\n node.name.type === \"JSXIdentifier\" &&\n (node.name.name === \"className\" || node.name.name === \"class\")\n ) {\n const value = node.value;\n\n // Handle string literal: className=\"...\"\n if (value?.type === \"Literal\" && typeof value.value === \"string\") {\n processStringValue(value, value.value);\n }\n\n // Handle JSX expression: className={...}\n if (value?.type === \"JSXExpressionContainer\") {\n const expr = value.expression;\n\n // Direct string: className={\"...\"}\n if (expr.type === \"Literal\" && typeof expr.value === \"string\") {\n processStringValue(expr, expr.value);\n }\n\n // Template literal: className={`...`}\n if (expr.type === \"TemplateLiteral\") {\n processTemplateLiteral(expr);\n }\n }\n }\n },\n\n // Check cn(), clsx(), classnames(), cva() calls\n CallExpression(node) {\n if (node.callee.type !== \"Identifier\") return;\n const name = node.callee.name;\n\n if (\n name === \"cn\" ||\n name === \"clsx\" ||\n name === \"classnames\" ||\n name === \"cva\" ||\n name === \"twMerge\"\n ) {\n for (const arg of node.arguments) {\n if (arg.type === \"Literal\" && typeof arg.value === \"string\") {\n processStringValue(arg, arg.value);\n }\n if (arg.type === \"TemplateLiteral\") {\n processTemplateLiteral(arg);\n }\n // Handle arrays of class strings\n if (arg.type === \"ArrayExpression\") {\n for (const element of arg.elements) {\n if (\n element?.type === \"Literal\" &&\n typeof element.value === \"string\"\n ) {\n processStringValue(element, element.value);\n }\n if (element?.type === \"TemplateLiteral\") {\n processTemplateLiteral(element);\n }\n }\n }\n }\n }\n },\n\n // At the end of the file, check if Tailwind colors are used without any dark mode\n \"Program:exit\"(node) {\n if (warnOnMissingDarkMode && fileHasColorClasses && !fileHasDarkMode) {\n context.report({\n node,\n messageId: \"missingDarkMode\",\n });\n }\n },\n };\n },\n});\n"],"mappings":"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","names":["meta"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/create-rule.ts","../../src/rules/enforce-absolute-imports.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: enforce-absolute-imports\n *\n * Requires alias imports (e.g., @/) for imports that traverse more than a\n * configurable number of directory levels. Prevents fragile relative import\n * paths like ../../../utils/helper.\n *\n * Examples:\n * - Bad: import { x } from '../../utils/helper'\n * - Good: import { x } from '@/utils/helper'\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\n\ntype MessageIds = \"preferAbsoluteImport\";\ntype Options = [\n {\n /** Maximum allowed parent directory traversals (default: 1, allows ../ but not ../../) */\n maxRelativeDepth?: number;\n /** The alias prefix to suggest (default: \"@/\") */\n aliasPrefix?: string;\n /** Patterns to ignore (e.g., [\"node_modules\", \".css\"]) */\n ignorePaths?: string[];\n }\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"enforce-absolute-imports\",\n version: \"1.0.0\",\n name: \"Enforce Absolute Imports\",\n description:\n \"Require alias imports for paths beyond a configurable directory depth\",\n defaultSeverity: \"warn\",\n category: \"static\",\n icon: \"📦\",\n hint: \"Prevents deep relative imports\",\n defaultEnabled: true,\n defaultOptions: [{ maxRelativeDepth: 1, aliasPrefix: \"@/\" }],\n optionSchema: {\n fields: [\n {\n key: \"maxRelativeDepth\",\n label: \"Maximum relative depth\",\n type: \"number\",\n defaultValue: 1,\n description:\n \"Maximum number of parent directory traversals allowed (../ counts as 1)\",\n },\n {\n key: \"aliasPrefix\",\n label: \"Alias prefix\",\n type: \"text\",\n defaultValue: \"@/\",\n description: \"The path alias prefix to use (e.g., @/, ~/)\",\n },\n ],\n },\n docs: `\n## What it does\n\nEnforces the use of path aliases (like \\`@/\\`) for imports that traverse multiple\nparent directories. This prevents fragile relative imports that are hard to\nmaintain and refactor.\n\n## Why it's useful\n\n- **Maintainability**: Absolute imports don't break when files move\n- **Readability**: Clear indication of where imports come from\n- **Consistency**: Standardizes import style across the codebase\n- **Refactoring**: Easier to move files without updating import paths\n\n## Examples\n\n### ❌ Incorrect (with maxRelativeDepth: 1)\n\n\\`\\`\\`tsx\n// Too many parent traversals\nimport { Button } from '../../components/Button';\nimport { utils } from '../../../lib/utils';\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n// Using alias imports\nimport { Button } from '@/components/Button';\nimport { utils } from '@/lib/utils';\n\n// Single parent traversal (within threshold)\nimport { sibling } from '../sibling';\nimport { local } from './local';\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/enforce-absolute-imports\": [\"warn\", {\n maxRelativeDepth: 1, // Allow ../ but not ../../\n aliasPrefix: \"@/\", // Suggested alias prefix\n ignorePaths: [\".css\", \".scss\", \"node_modules\"]\n}]\n\\`\\`\\`\n`,\n});\n\n/**\n * Count the number of parent directory traversals in an import path\n */\nfunction countParentTraversals(importSource: string): number {\n // Match all occurrences of ../ or ..\\\\ (Windows)\n const matches = importSource.match(/\\.\\.\\//g);\n return matches ? matches.length : 0;\n}\n\n/**\n * Check if an import is a relative path\n */\nfunction isRelativeImport(importSource: string): boolean {\n return importSource.startsWith(\"./\") || importSource.startsWith(\"../\");\n}\n\n/**\n * Check if the import should be ignored\n */\nfunction shouldIgnore(importSource: string, ignorePaths: string[]): boolean {\n return ignorePaths.some((pattern) => importSource.includes(pattern));\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"enforce-absolute-imports\",\n meta: {\n type: \"suggestion\",\n docs: {\n description:\n \"Require alias imports for paths beyond a configurable directory depth\",\n },\n messages: {\n preferAbsoluteImport:\n \"Import traverses {{depth}} parent director{{plural}}. Use an alias like '{{aliasPrefix}}...' instead of '{{importSource}}'.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n maxRelativeDepth: {\n type: \"number\",\n minimum: 0,\n description:\n \"Maximum number of parent directory traversals allowed\",\n },\n aliasPrefix: {\n type: \"string\",\n description: \"The path alias prefix to suggest\",\n },\n ignorePaths: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"Patterns to ignore\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [\n {\n maxRelativeDepth: 1,\n aliasPrefix: \"@/\",\n ignorePaths: [],\n },\n ],\n create(context) {\n const options = context.options[0] || {};\n const maxRelativeDepth = options.maxRelativeDepth ?? 1;\n const aliasPrefix = options.aliasPrefix ?? \"@/\";\n const ignorePaths = options.ignorePaths ?? [];\n\n /**\n * Check an import source and report if it exceeds the depth threshold\n */\n function checkImportSource(\n source: string,\n node: TSESTree.StringLiteral\n ): void {\n // Skip non-relative imports (node_modules, aliases, etc.)\n if (!isRelativeImport(source)) {\n return;\n }\n\n // Skip ignored paths\n if (shouldIgnore(source, ignorePaths)) {\n return;\n }\n\n const depth = countParentTraversals(source);\n\n if (depth > maxRelativeDepth) {\n context.report({\n node,\n messageId: \"preferAbsoluteImport\",\n data: {\n depth: String(depth),\n plural: depth === 1 ? \"y\" : \"ies\",\n aliasPrefix,\n importSource: source,\n },\n });\n }\n }\n\n return {\n // Standard import declarations: import { x } from '../../utils'\n ImportDeclaration(node) {\n const source = node.source.value as string;\n checkImportSource(source, node.source);\n },\n\n // Re-exports with source: export { x } from '../../utils'\n ExportNamedDeclaration(node) {\n if (node.source) {\n const source = node.source.value as string;\n checkImportSource(source, node.source);\n }\n },\n\n // Export all: export * from '../../utils'\n ExportAllDeclaration(node) {\n const source = node.source.value as string;\n checkImportSource(source, node.source);\n },\n };\n },\n});\n"],"mappings":"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","names":["meta"]}
1
+ {"version":3,"sources":["../../src/utils/create-rule.ts","../../src/rules/enforce-absolute-imports.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: enforce-absolute-imports\n *\n * Requires alias imports (e.g., @/) for imports that traverse more than a\n * configurable number of directory levels. Prevents fragile relative import\n * paths like ../../../utils/helper.\n *\n * Examples:\n * - Bad: import { x } from '../../utils/helper'\n * - Good: import { x } from '@/utils/helper'\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\n\ntype MessageIds = \"preferAbsoluteImport\";\ntype Options = [\n {\n /** Maximum allowed parent directory traversals (default: 1, allows ../ but not ../../) */\n maxRelativeDepth?: number;\n /** The alias prefix to suggest (default: \"@/\") */\n aliasPrefix?: string;\n /** Patterns to ignore (e.g., [\"node_modules\", \".css\"]) */\n ignorePaths?: string[];\n }\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"enforce-absolute-imports\",\n version: \"1.0.0\",\n name: \"Enforce Absolute Imports\",\n description:\n \"Require alias imports for paths beyond a configurable directory depth\",\n defaultSeverity: \"warn\",\n category: \"static\",\n icon: \"📦\",\n hint: \"Prevents deep relative imports\",\n defaultEnabled: true,\n defaultOptions: [{ maxRelativeDepth: 1, aliasPrefix: \"@/\" }],\n optionSchema: {\n fields: [\n {\n key: \"maxRelativeDepth\",\n label: \"Maximum relative depth\",\n type: \"number\",\n defaultValue: 1,\n description:\n \"Maximum number of parent directory traversals allowed (../ counts as 1)\",\n },\n {\n key: \"aliasPrefix\",\n label: \"Alias prefix\",\n type: \"text\",\n defaultValue: \"@/\",\n description: \"The path alias prefix to use (e.g., @/, ~/)\",\n },\n ],\n },\n docs: `\n## What it does\n\nEnforces the use of path aliases (like \\`@/\\`) for imports that traverse multiple\nparent directories. This prevents fragile relative imports that are hard to\nmaintain and refactor.\n\n## Why it's useful\n\n- **Maintainability**: Absolute imports don't break when files move\n- **Readability**: Clear indication of where imports come from\n- **Consistency**: Standardizes import style across the codebase\n- **Refactoring**: Easier to move files without updating import paths\n\n## Examples\n\n### ❌ Incorrect (with maxRelativeDepth: 1)\n\n\\`\\`\\`tsx\n// Too many parent traversals\nimport { Button } from '../../components/Button';\nimport { utils } from '../../../lib/utils';\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n// Using alias imports\nimport { Button } from '@/components/Button';\nimport { utils } from '@/lib/utils';\n\n// Single parent traversal (within threshold)\nimport { sibling } from '../sibling';\nimport { local } from './local';\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/enforce-absolute-imports\": [\"warn\", {\n maxRelativeDepth: 1, // Allow ../ but not ../../\n aliasPrefix: \"@/\", // Suggested alias prefix\n ignorePaths: [\".css\", \".scss\", \"node_modules\"]\n}]\n\\`\\`\\`\n`,\n});\n\n/**\n * Count the number of parent directory traversals in an import path\n */\nfunction countParentTraversals(importSource: string): number {\n // Match all occurrences of ../ or ..\\\\ (Windows)\n const matches = importSource.match(/\\.\\.\\//g);\n return matches ? matches.length : 0;\n}\n\n/**\n * Check if an import is a relative path\n */\nfunction isRelativeImport(importSource: string): boolean {\n return importSource.startsWith(\"./\") || importSource.startsWith(\"../\");\n}\n\n/**\n * Check if the import should be ignored\n */\nfunction shouldIgnore(importSource: string, ignorePaths: string[]): boolean {\n return ignorePaths.some((pattern) => importSource.includes(pattern));\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"enforce-absolute-imports\",\n meta: {\n type: \"suggestion\",\n docs: {\n description:\n \"Require alias imports for paths beyond a configurable directory depth\",\n },\n messages: {\n preferAbsoluteImport:\n \"Import traverses {{depth}} parent director{{plural}}. Use an alias like '{{aliasPrefix}}...' instead of '{{importSource}}'.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n maxRelativeDepth: {\n type: \"number\",\n minimum: 0,\n description:\n \"Maximum number of parent directory traversals allowed\",\n },\n aliasPrefix: {\n type: \"string\",\n description: \"The path alias prefix to suggest\",\n },\n ignorePaths: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"Patterns to ignore\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [\n {\n maxRelativeDepth: 1,\n aliasPrefix: \"@/\",\n ignorePaths: [],\n },\n ],\n create(context) {\n const options = context.options[0] || {};\n const maxRelativeDepth = options.maxRelativeDepth ?? 1;\n const aliasPrefix = options.aliasPrefix ?? \"@/\";\n const ignorePaths = options.ignorePaths ?? [];\n\n /**\n * Check an import source and report if it exceeds the depth threshold\n */\n function checkImportSource(\n source: string,\n node: TSESTree.StringLiteral\n ): void {\n // Skip non-relative imports (node_modules, aliases, etc.)\n if (!isRelativeImport(source)) {\n return;\n }\n\n // Skip ignored paths\n if (shouldIgnore(source, ignorePaths)) {\n return;\n }\n\n const depth = countParentTraversals(source);\n\n if (depth > maxRelativeDepth) {\n context.report({\n node,\n messageId: \"preferAbsoluteImport\",\n data: {\n depth: String(depth),\n plural: depth === 1 ? \"y\" : \"ies\",\n aliasPrefix,\n importSource: source,\n },\n });\n }\n }\n\n return {\n // Standard import declarations: import { x } from '../../utils'\n ImportDeclaration(node) {\n const source = node.source.value as string;\n checkImportSource(source, node.source);\n },\n\n // Re-exports with source: export { x } from '../../utils'\n ExportNamedDeclaration(node) {\n if (node.source) {\n const source = node.source.value as string;\n checkImportSource(source, node.source);\n }\n },\n\n // Export all: export * from '../../utils'\n ExportAllDeclaration(node) {\n const source = node.source.value as string;\n checkImportSource(source, node.source);\n },\n };\n },\n});\n"],"mappings":"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","names":["meta"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/create-rule.ts","../../src/rules/no-any-in-props.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: no-any-in-props\n *\n * Prevents React components from using `any` type in their props, ensuring\n * type safety at component boundaries.\n *\n * Examples:\n * - Bad: function Component(props: any) {}\n * - Bad: function Component({ x }: { x: any }) {}\n * - Bad: const Component: FC<any> = () => {}\n * - Good: function Component(props: { name: string }) {}\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\n\ntype MessageIds = \"anyInProps\" | \"anyInPropsProperty\";\ntype Options = [\n {\n /** Also check FC<any> and React.FC<any> patterns */\n checkFCGenerics?: boolean;\n /** Allow any in generic defaults (e.g., <T = any>) */\n allowInGenericDefaults?: boolean;\n }\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"no-any-in-props\",\n version: \"1.0.0\",\n name: \"No Any in Props\",\n description: \"Disallow 'any' type in React component props\",\n defaultSeverity: \"error\",\n category: \"static\",\n icon: \"🔒\",\n hint: \"Ensures type safety in component props\",\n defaultEnabled: true,\n defaultOptions: [{ checkFCGenerics: true, allowInGenericDefaults: false }],\n optionSchema: {\n fields: [\n {\n key: \"checkFCGenerics\",\n label: \"Check FC generics\",\n type: \"boolean\",\n defaultValue: true,\n description: \"Check FC<any> and React.FC<any> patterns\",\n },\n {\n key: \"allowInGenericDefaults\",\n label: \"Allow in generic defaults\",\n type: \"boolean\",\n defaultValue: false,\n description: \"Allow any in generic type parameter defaults\",\n },\n ],\n },\n docs: `\n## What it does\n\nPrevents the use of \\`any\\` type in React component props. This ensures type\nsafety at component boundaries, catching type errors at compile time rather\nthan runtime.\n\n## Why it's useful\n\n- **Type Safety**: Catches prop type errors at compile time\n- **Documentation**: Props serve as self-documenting API\n- **Refactoring**: IDE can track prop usage across codebase\n- **Code Quality**: Encourages thoughtful API design\n\n## Examples\n\n### ❌ Incorrect\n\n\\`\\`\\`tsx\n// Direct any annotation\nfunction Component(props: any) {}\n\n// Any in destructured props\nfunction Component({ data }: { data: any }) {}\n\n// FC with any generic\nconst Component: FC<any> = () => {};\n\n// Any in props interface\ninterface Props { value: any }\nfunction Component(props: Props) {}\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n// Properly typed props\nfunction Component(props: { name: string }) {}\n\n// Using unknown for truly unknown types\nfunction Component({ data }: { data: unknown }) {}\n\n// Typed FC\nconst Component: FC<{ count: number }> = () => {};\n\n// Generic component with constraint\nfunction List<T extends object>(props: { items: T[] }) {}\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/no-any-in-props\": [\"error\", {\n checkFCGenerics: true, // Check FC<any> patterns\n allowInGenericDefaults: false // Disallow <T = any>\n}]\n\\`\\`\\`\n`,\n});\n\n/**\n * Check if a name is likely a React component (PascalCase, not a hook)\n */\nfunction isComponentName(name: string): boolean {\n return /^[A-Z][a-zA-Z0-9]*$/.test(name) && !name.startsWith(\"Use\");\n}\n\n/**\n * Check if a type node contains 'any'\n */\nfunction containsAnyType(\n node: TSESTree.TypeNode,\n allowInGenericDefaults: boolean\n): { hasAny: boolean; location: string | null } {\n if (!node) {\n return { hasAny: false, location: null };\n }\n\n switch (node.type) {\n case \"TSAnyKeyword\":\n return { hasAny: true, location: null };\n\n case \"TSTypeLiteral\":\n // Check each property in { prop: any }\n for (const member of node.members) {\n if (\n member.type === \"TSPropertySignature\" &&\n member.typeAnnotation?.typeAnnotation\n ) {\n const result = containsAnyType(\n member.typeAnnotation.typeAnnotation,\n allowInGenericDefaults\n );\n if (result.hasAny) {\n const propName =\n member.key.type === \"Identifier\" ? member.key.name : \"property\";\n return { hasAny: true, location: `property '${propName}'` };\n }\n }\n // Index signature [key: string]: any\n if (\n member.type === \"TSIndexSignature\" &&\n member.typeAnnotation?.typeAnnotation\n ) {\n const result = containsAnyType(\n member.typeAnnotation.typeAnnotation,\n allowInGenericDefaults\n );\n if (result.hasAny) {\n return { hasAny: true, location: \"index signature\" };\n }\n }\n }\n return { hasAny: false, location: null };\n\n case \"TSUnionType\":\n case \"TSIntersectionType\":\n for (const typeNode of node.types) {\n const result = containsAnyType(typeNode, allowInGenericDefaults);\n if (result.hasAny) {\n return result;\n }\n }\n return { hasAny: false, location: null };\n\n case \"TSArrayType\":\n return containsAnyType(node.elementType, allowInGenericDefaults);\n\n case \"TSTypeReference\":\n // Check generic arguments like Array<any>, Record<string, any>\n if (node.typeArguments) {\n for (const param of node.typeArguments.params) {\n const result = containsAnyType(param, allowInGenericDefaults);\n if (result.hasAny) {\n return { hasAny: true, location: \"generic argument\" };\n }\n }\n }\n return { hasAny: false, location: null };\n\n case \"TSFunctionType\":\n // Check return type and parameters\n if (node.returnType?.typeAnnotation) {\n const result = containsAnyType(\n node.returnType.typeAnnotation,\n allowInGenericDefaults\n );\n if (result.hasAny) {\n return { hasAny: true, location: \"function return type\" };\n }\n }\n for (const param of node.params) {\n // Skip TSParameterProperty (doesn't have typeAnnotation) and RestElement\n if (\n param.type !== \"RestElement\" &&\n param.type !== \"TSParameterProperty\" &&\n \"typeAnnotation\" in param &&\n param.typeAnnotation?.typeAnnotation\n ) {\n const result = containsAnyType(\n param.typeAnnotation.typeAnnotation,\n allowInGenericDefaults\n );\n if (result.hasAny) {\n return { hasAny: true, location: \"function parameter\" };\n }\n }\n }\n return { hasAny: false, location: null };\n\n case \"TSTupleType\":\n for (const elementType of node.elementTypes) {\n // Handle both TSNamedTupleMember and regular type nodes\n const typeToCheck =\n elementType.type === \"TSNamedTupleMember\"\n ? elementType.elementType\n : elementType;\n const result = containsAnyType(typeToCheck, allowInGenericDefaults);\n if (result.hasAny) {\n return { hasAny: true, location: \"tuple element\" };\n }\n }\n return { hasAny: false, location: null };\n\n case \"TSConditionalType\":\n // Check all parts of conditional type\n const checkResult = containsAnyType(\n node.checkType,\n allowInGenericDefaults\n );\n if (checkResult.hasAny) return checkResult;\n const extendsResult = containsAnyType(\n node.extendsType,\n allowInGenericDefaults\n );\n if (extendsResult.hasAny) return extendsResult;\n const trueResult = containsAnyType(\n node.trueType,\n allowInGenericDefaults\n );\n if (trueResult.hasAny) return trueResult;\n const falseResult = containsAnyType(\n node.falseType,\n allowInGenericDefaults\n );\n if (falseResult.hasAny) return falseResult;\n return { hasAny: false, location: null };\n\n case \"TSMappedType\":\n if (node.typeAnnotation) {\n return containsAnyType(node.typeAnnotation, allowInGenericDefaults);\n }\n return { hasAny: false, location: null };\n\n default:\n return { hasAny: false, location: null };\n }\n}\n\n/**\n * Get the name of a function or component\n */\nfunction getComponentName(\n node:\n | TSESTree.FunctionDeclaration\n | TSESTree.ArrowFunctionExpression\n | TSESTree.FunctionExpression\n): string | null {\n // Function declaration: function Foo() {}\n if (node.type === \"FunctionDeclaration\" && node.id) {\n return node.id.name;\n }\n\n // Variable declarator: const Foo = () => {}\n const parent = node.parent;\n if (\n parent?.type === \"VariableDeclarator\" &&\n parent.id.type === \"Identifier\"\n ) {\n return parent.id.name;\n }\n\n // forwardRef/memo wrapper: const Foo = forwardRef(() => {})\n if (parent?.type === \"CallExpression\") {\n const callParent = parent.parent;\n if (\n callParent?.type === \"VariableDeclarator\" &&\n callParent.id.type === \"Identifier\"\n ) {\n return callParent.id.name;\n }\n }\n\n // Named function expression: const x = function Foo() {}\n if (node.type === \"FunctionExpression\" && node.id) {\n return node.id.name;\n }\n\n return null;\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"no-any-in-props\",\n meta: {\n type: \"problem\",\n docs: {\n description: \"Disallow 'any' type in React component props\",\n },\n messages: {\n anyInProps:\n \"Component '{{componentName}}' has 'any' type in props. Use a specific type or 'unknown' instead.\",\n anyInPropsProperty:\n \"Component '{{componentName}}' has 'any' type in {{location}}. Use a specific type or 'unknown' instead.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n checkFCGenerics: {\n type: \"boolean\",\n description: \"Check FC<any> and React.FC<any> patterns\",\n },\n allowInGenericDefaults: {\n type: \"boolean\",\n description: \"Allow any in generic type parameter defaults\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [\n {\n checkFCGenerics: true,\n allowInGenericDefaults: false,\n },\n ],\n create(context) {\n const options = context.options[0] || {};\n const checkFCGenerics = options.checkFCGenerics ?? true;\n const allowInGenericDefaults = options.allowInGenericDefaults ?? false;\n\n /**\n * Check a function's first parameter for any type\n */\n function checkFunctionProps(\n node:\n | TSESTree.FunctionDeclaration\n | TSESTree.ArrowFunctionExpression\n | TSESTree.FunctionExpression\n ): void {\n const componentName = getComponentName(node);\n\n // Skip if not a component (not PascalCase)\n if (!componentName || !isComponentName(componentName)) {\n return;\n }\n\n // Check first parameter (props)\n const firstParam = node.params[0];\n if (!firstParam) {\n return;\n }\n\n // Get type annotation\n let typeAnnotation: TSESTree.TypeNode | null = null;\n\n if (firstParam.type === \"Identifier\" && firstParam.typeAnnotation) {\n typeAnnotation = firstParam.typeAnnotation.typeAnnotation;\n } else if (\n firstParam.type === \"ObjectPattern\" &&\n firstParam.typeAnnotation\n ) {\n typeAnnotation = firstParam.typeAnnotation.typeAnnotation;\n }\n\n if (typeAnnotation) {\n const result = containsAnyType(typeAnnotation, allowInGenericDefaults);\n if (result.hasAny) {\n context.report({\n node: firstParam,\n messageId: result.location ? \"anyInPropsProperty\" : \"anyInProps\",\n data: {\n componentName,\n location: result.location || \"props\",\n },\n });\n }\n }\n }\n\n /**\n * Check FC<any> or React.FC<any> patterns\n */\n function checkFCGeneric(node: TSESTree.VariableDeclarator): void {\n if (!checkFCGenerics) {\n return;\n }\n\n // Get variable name\n if (node.id.type !== \"Identifier\") {\n return;\n }\n const componentName = node.id.name;\n\n // Skip if not a component name\n if (!isComponentName(componentName)) {\n return;\n }\n\n // Check type annotation\n const typeAnnotation = node.id.typeAnnotation?.typeAnnotation;\n if (!typeAnnotation || typeAnnotation.type !== \"TSTypeReference\") {\n return;\n }\n\n // Check if it's FC or React.FC\n let isFCType = false;\n if (\n typeAnnotation.typeName.type === \"Identifier\" &&\n [\"FC\", \"FunctionComponent\", \"VFC\"].includes(typeAnnotation.typeName.name)\n ) {\n isFCType = true;\n } else if (\n typeAnnotation.typeName.type === \"TSQualifiedName\" &&\n typeAnnotation.typeName.left.type === \"Identifier\" &&\n typeAnnotation.typeName.left.name === \"React\" &&\n [\"FC\", \"FunctionComponent\", \"VFC\"].includes(\n typeAnnotation.typeName.right.name\n )\n ) {\n isFCType = true;\n }\n\n if (!isFCType || !typeAnnotation.typeArguments) {\n return;\n }\n\n // Check the type argument\n const firstTypeArg = typeAnnotation.typeArguments.params[0];\n if (firstTypeArg) {\n const result = containsAnyType(firstTypeArg, allowInGenericDefaults);\n if (result.hasAny) {\n context.report({\n node: firstTypeArg,\n messageId: result.location ? \"anyInPropsProperty\" : \"anyInProps\",\n data: {\n componentName,\n location: result.location || \"FC type parameter\",\n },\n });\n }\n }\n }\n\n return {\n FunctionDeclaration(node) {\n checkFunctionProps(node);\n },\n\n ArrowFunctionExpression(node) {\n checkFunctionProps(node);\n },\n\n FunctionExpression(node) {\n checkFunctionProps(node);\n },\n\n VariableDeclarator(node) {\n checkFCGeneric(node);\n },\n };\n },\n});\n"],"mappings":"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","names":["meta"]}
1
+ {"version":3,"sources":["../../src/utils/create-rule.ts","../../src/rules/no-any-in-props.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: no-any-in-props\n *\n * Prevents React components from using `any` type in their props, ensuring\n * type safety at component boundaries.\n *\n * Examples:\n * - Bad: function Component(props: any) {}\n * - Bad: function Component({ x }: { x: any }) {}\n * - Bad: const Component: FC<any> = () => {}\n * - Good: function Component(props: { name: string }) {}\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\n\ntype MessageIds = \"anyInProps\" | \"anyInPropsProperty\";\ntype Options = [\n {\n /** Also check FC<any> and React.FC<any> patterns */\n checkFCGenerics?: boolean;\n /** Allow any in generic defaults (e.g., <T = any>) */\n allowInGenericDefaults?: boolean;\n }\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"no-any-in-props\",\n version: \"1.0.0\",\n name: \"No Any in Props\",\n description: \"Disallow 'any' type in React component props\",\n defaultSeverity: \"error\",\n category: \"static\",\n icon: \"🔒\",\n hint: \"Ensures type safety in component props\",\n defaultEnabled: true,\n defaultOptions: [{ checkFCGenerics: true, allowInGenericDefaults: false }],\n optionSchema: {\n fields: [\n {\n key: \"checkFCGenerics\",\n label: \"Check FC generics\",\n type: \"boolean\",\n defaultValue: true,\n description: \"Check FC<any> and React.FC<any> patterns\",\n },\n {\n key: \"allowInGenericDefaults\",\n label: \"Allow in generic defaults\",\n type: \"boolean\",\n defaultValue: false,\n description: \"Allow any in generic type parameter defaults\",\n },\n ],\n },\n docs: `\n## What it does\n\nPrevents the use of \\`any\\` type in React component props. This ensures type\nsafety at component boundaries, catching type errors at compile time rather\nthan runtime.\n\n## Why it's useful\n\n- **Type Safety**: Catches prop type errors at compile time\n- **Documentation**: Props serve as self-documenting API\n- **Refactoring**: IDE can track prop usage across codebase\n- **Code Quality**: Encourages thoughtful API design\n\n## Examples\n\n### ❌ Incorrect\n\n\\`\\`\\`tsx\n// Direct any annotation\nfunction Component(props: any) {}\n\n// Any in destructured props\nfunction Component({ data }: { data: any }) {}\n\n// FC with any generic\nconst Component: FC<any> = () => {};\n\n// Any in props interface\ninterface Props { value: any }\nfunction Component(props: Props) {}\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n// Properly typed props\nfunction Component(props: { name: string }) {}\n\n// Using unknown for truly unknown types\nfunction Component({ data }: { data: unknown }) {}\n\n// Typed FC\nconst Component: FC<{ count: number }> = () => {};\n\n// Generic component with constraint\nfunction List<T extends object>(props: { items: T[] }) {}\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/no-any-in-props\": [\"error\", {\n checkFCGenerics: true, // Check FC<any> patterns\n allowInGenericDefaults: false // Disallow <T = any>\n}]\n\\`\\`\\`\n`,\n});\n\n/**\n * Check if a name is likely a React component (PascalCase, not a hook)\n */\nfunction isComponentName(name: string): boolean {\n return /^[A-Z][a-zA-Z0-9]*$/.test(name) && !name.startsWith(\"Use\");\n}\n\n/**\n * Check if a type node contains 'any'\n */\nfunction containsAnyType(\n node: TSESTree.TypeNode,\n allowInGenericDefaults: boolean\n): { hasAny: boolean; location: string | null } {\n if (!node) {\n return { hasAny: false, location: null };\n }\n\n switch (node.type) {\n case \"TSAnyKeyword\":\n return { hasAny: true, location: null };\n\n case \"TSTypeLiteral\":\n // Check each property in { prop: any }\n for (const member of node.members) {\n if (\n member.type === \"TSPropertySignature\" &&\n member.typeAnnotation?.typeAnnotation\n ) {\n const result = containsAnyType(\n member.typeAnnotation.typeAnnotation,\n allowInGenericDefaults\n );\n if (result.hasAny) {\n const propName =\n member.key.type === \"Identifier\" ? member.key.name : \"property\";\n return { hasAny: true, location: `property '${propName}'` };\n }\n }\n // Index signature [key: string]: any\n if (\n member.type === \"TSIndexSignature\" &&\n member.typeAnnotation?.typeAnnotation\n ) {\n const result = containsAnyType(\n member.typeAnnotation.typeAnnotation,\n allowInGenericDefaults\n );\n if (result.hasAny) {\n return { hasAny: true, location: \"index signature\" };\n }\n }\n }\n return { hasAny: false, location: null };\n\n case \"TSUnionType\":\n case \"TSIntersectionType\":\n for (const typeNode of node.types) {\n const result = containsAnyType(typeNode, allowInGenericDefaults);\n if (result.hasAny) {\n return result;\n }\n }\n return { hasAny: false, location: null };\n\n case \"TSArrayType\":\n return containsAnyType(node.elementType, allowInGenericDefaults);\n\n case \"TSTypeReference\":\n // Check generic arguments like Array<any>, Record<string, any>\n if (node.typeArguments) {\n for (const param of node.typeArguments.params) {\n const result = containsAnyType(param, allowInGenericDefaults);\n if (result.hasAny) {\n return { hasAny: true, location: \"generic argument\" };\n }\n }\n }\n return { hasAny: false, location: null };\n\n case \"TSFunctionType\":\n // Check return type and parameters\n if (node.returnType?.typeAnnotation) {\n const result = containsAnyType(\n node.returnType.typeAnnotation,\n allowInGenericDefaults\n );\n if (result.hasAny) {\n return { hasAny: true, location: \"function return type\" };\n }\n }\n for (const param of node.params) {\n // Skip TSParameterProperty (doesn't have typeAnnotation) and RestElement\n if (\n param.type !== \"RestElement\" &&\n param.type !== \"TSParameterProperty\" &&\n \"typeAnnotation\" in param &&\n param.typeAnnotation?.typeAnnotation\n ) {\n const result = containsAnyType(\n param.typeAnnotation.typeAnnotation,\n allowInGenericDefaults\n );\n if (result.hasAny) {\n return { hasAny: true, location: \"function parameter\" };\n }\n }\n }\n return { hasAny: false, location: null };\n\n case \"TSTupleType\":\n for (const elementType of node.elementTypes) {\n // Handle both TSNamedTupleMember and regular type nodes\n const typeToCheck =\n elementType.type === \"TSNamedTupleMember\"\n ? elementType.elementType\n : elementType;\n const result = containsAnyType(typeToCheck, allowInGenericDefaults);\n if (result.hasAny) {\n return { hasAny: true, location: \"tuple element\" };\n }\n }\n return { hasAny: false, location: null };\n\n case \"TSConditionalType\":\n // Check all parts of conditional type\n const checkResult = containsAnyType(\n node.checkType,\n allowInGenericDefaults\n );\n if (checkResult.hasAny) return checkResult;\n const extendsResult = containsAnyType(\n node.extendsType,\n allowInGenericDefaults\n );\n if (extendsResult.hasAny) return extendsResult;\n const trueResult = containsAnyType(\n node.trueType,\n allowInGenericDefaults\n );\n if (trueResult.hasAny) return trueResult;\n const falseResult = containsAnyType(\n node.falseType,\n allowInGenericDefaults\n );\n if (falseResult.hasAny) return falseResult;\n return { hasAny: false, location: null };\n\n case \"TSMappedType\":\n if (node.typeAnnotation) {\n return containsAnyType(node.typeAnnotation, allowInGenericDefaults);\n }\n return { hasAny: false, location: null };\n\n default:\n return { hasAny: false, location: null };\n }\n}\n\n/**\n * Get the name of a function or component\n */\nfunction getComponentName(\n node:\n | TSESTree.FunctionDeclaration\n | TSESTree.ArrowFunctionExpression\n | TSESTree.FunctionExpression\n): string | null {\n // Function declaration: function Foo() {}\n if (node.type === \"FunctionDeclaration\" && node.id) {\n return node.id.name;\n }\n\n // Variable declarator: const Foo = () => {}\n const parent = node.parent;\n if (\n parent?.type === \"VariableDeclarator\" &&\n parent.id.type === \"Identifier\"\n ) {\n return parent.id.name;\n }\n\n // forwardRef/memo wrapper: const Foo = forwardRef(() => {})\n if (parent?.type === \"CallExpression\") {\n const callParent = parent.parent;\n if (\n callParent?.type === \"VariableDeclarator\" &&\n callParent.id.type === \"Identifier\"\n ) {\n return callParent.id.name;\n }\n }\n\n // Named function expression: const x = function Foo() {}\n if (node.type === \"FunctionExpression\" && node.id) {\n return node.id.name;\n }\n\n return null;\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"no-any-in-props\",\n meta: {\n type: \"problem\",\n docs: {\n description: \"Disallow 'any' type in React component props\",\n },\n messages: {\n anyInProps:\n \"Component '{{componentName}}' has 'any' type in props. Use a specific type or 'unknown' instead.\",\n anyInPropsProperty:\n \"Component '{{componentName}}' has 'any' type in {{location}}. Use a specific type or 'unknown' instead.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n checkFCGenerics: {\n type: \"boolean\",\n description: \"Check FC<any> and React.FC<any> patterns\",\n },\n allowInGenericDefaults: {\n type: \"boolean\",\n description: \"Allow any in generic type parameter defaults\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [\n {\n checkFCGenerics: true,\n allowInGenericDefaults: false,\n },\n ],\n create(context) {\n const options = context.options[0] || {};\n const checkFCGenerics = options.checkFCGenerics ?? true;\n const allowInGenericDefaults = options.allowInGenericDefaults ?? false;\n\n /**\n * Check a function's first parameter for any type\n */\n function checkFunctionProps(\n node:\n | TSESTree.FunctionDeclaration\n | TSESTree.ArrowFunctionExpression\n | TSESTree.FunctionExpression\n ): void {\n const componentName = getComponentName(node);\n\n // Skip if not a component (not PascalCase)\n if (!componentName || !isComponentName(componentName)) {\n return;\n }\n\n // Check first parameter (props)\n const firstParam = node.params[0];\n if (!firstParam) {\n return;\n }\n\n // Get type annotation\n let typeAnnotation: TSESTree.TypeNode | null = null;\n\n if (firstParam.type === \"Identifier\" && firstParam.typeAnnotation) {\n typeAnnotation = firstParam.typeAnnotation.typeAnnotation;\n } else if (\n firstParam.type === \"ObjectPattern\" &&\n firstParam.typeAnnotation\n ) {\n typeAnnotation = firstParam.typeAnnotation.typeAnnotation;\n }\n\n if (typeAnnotation) {\n const result = containsAnyType(typeAnnotation, allowInGenericDefaults);\n if (result.hasAny) {\n context.report({\n node: firstParam,\n messageId: result.location ? \"anyInPropsProperty\" : \"anyInProps\",\n data: {\n componentName,\n location: result.location || \"props\",\n },\n });\n }\n }\n }\n\n /**\n * Check FC<any> or React.FC<any> patterns\n */\n function checkFCGeneric(node: TSESTree.VariableDeclarator): void {\n if (!checkFCGenerics) {\n return;\n }\n\n // Get variable name\n if (node.id.type !== \"Identifier\") {\n return;\n }\n const componentName = node.id.name;\n\n // Skip if not a component name\n if (!isComponentName(componentName)) {\n return;\n }\n\n // Check type annotation\n const typeAnnotation = node.id.typeAnnotation?.typeAnnotation;\n if (!typeAnnotation || typeAnnotation.type !== \"TSTypeReference\") {\n return;\n }\n\n // Check if it's FC or React.FC\n let isFCType = false;\n if (\n typeAnnotation.typeName.type === \"Identifier\" &&\n [\"FC\", \"FunctionComponent\", \"VFC\"].includes(typeAnnotation.typeName.name)\n ) {\n isFCType = true;\n } else if (\n typeAnnotation.typeName.type === \"TSQualifiedName\" &&\n typeAnnotation.typeName.left.type === \"Identifier\" &&\n typeAnnotation.typeName.left.name === \"React\" &&\n [\"FC\", \"FunctionComponent\", \"VFC\"].includes(\n typeAnnotation.typeName.right.name\n )\n ) {\n isFCType = true;\n }\n\n if (!isFCType || !typeAnnotation.typeArguments) {\n return;\n }\n\n // Check the type argument\n const firstTypeArg = typeAnnotation.typeArguments.params[0];\n if (firstTypeArg) {\n const result = containsAnyType(firstTypeArg, allowInGenericDefaults);\n if (result.hasAny) {\n context.report({\n node: firstTypeArg,\n messageId: result.location ? \"anyInPropsProperty\" : \"anyInProps\",\n data: {\n componentName,\n location: result.location || \"FC type parameter\",\n },\n });\n }\n }\n }\n\n return {\n FunctionDeclaration(node) {\n checkFunctionProps(node);\n },\n\n ArrowFunctionExpression(node) {\n checkFunctionProps(node);\n },\n\n FunctionExpression(node) {\n checkFunctionProps(node);\n },\n\n VariableDeclarator(node) {\n checkFCGeneric(node);\n },\n };\n },\n});\n"],"mappings":"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","names":["meta"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/create-rule.ts","../../src/rules/no-direct-store-import.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: no-direct-store-import\n *\n * Forbids direct Zustand store imports - prefer using hooks via context.\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\n\ntype MessageIds = \"noDirectImport\";\ntype Options = [\n {\n storePattern?: string;\n }\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"no-direct-store-import\",\n version: \"1.0.0\",\n name: \"No Direct Store Import\",\n description: \"Forbid direct Zustand store imports (use context hooks)\",\n defaultSeverity: \"warn\",\n category: \"static\",\n icon: \"🏪\",\n hint: \"Encourages testable store access\",\n defaultEnabled: true,\n defaultOptions: [{ storePattern: \"use*Store\" }],\n optionSchema: {\n fields: [\n {\n key: \"storePattern\",\n label: \"Glob pattern for store files\",\n type: \"text\",\n defaultValue: \"use*Store\",\n placeholder: \"use*Store\",\n description: \"Pattern to match store file names\",\n },\n ],\n },\n docs: `\n## What it does\n\nPrevents direct imports of Zustand stores, encouraging the use of context-based hooks\nfor better dependency injection and testability.\n\n## Why it's useful\n\n- **Testability**: Context-based access allows easy mocking in tests\n- **Flexibility**: Store implementation can change without updating all consumers\n- **Dependency Injection**: Stores can be provided at different levels of the component tree\n- **Server Components**: Helps avoid accidentally importing stores in server components\n\n## Examples\n\n### ❌ Incorrect\n\n\\`\\`\\`tsx\n// Directly importing the store\nimport { useAuthStore } from '../stores/auth-store';\nimport { useCartStore } from '@/stores/useCartStore';\n\nfunction MyComponent() {\n const user = useAuthStore((s) => s.user);\n}\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n// Using context-provided hooks\nimport { useAuth } from '../contexts/auth-context';\nimport { useCart } from '@/hooks/useCart';\n\nfunction MyComponent() {\n const { user } = useAuth();\n}\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/no-direct-store-import\": [\"warn\", {\n storePattern: \"use*Store\" // Pattern to match store names\n}]\n\\`\\`\\`\n\n## Notes\n\n- The pattern uses glob syntax (\\`*\\` matches any characters)\n- Only triggers for imports from paths containing \"store\"\n- Works with both named and default imports\n`,\n});\n\n// Convert glob pattern to regex\nfunction patternToRegex(pattern: string): RegExp {\n const escaped = pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\*/g, \".*\")\n .replace(/\\?/g, \".\");\n return new RegExp(`^${escaped}$`);\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"no-direct-store-import\",\n meta: {\n type: \"problem\",\n docs: {\n description:\n \"Forbid direct Zustand store imports (use hooks via context)\",\n },\n messages: {\n noDirectImport:\n \"Avoid importing store '{{name}}' directly. Use the store via a context hook instead.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n storePattern: {\n type: \"string\",\n description: \"Glob pattern for store names\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{ storePattern: \"use*Store\" }],\n create(context) {\n const options = context.options[0] || {};\n const pattern = options.storePattern || \"use*Store\";\n const regex = patternToRegex(pattern);\n\n return {\n ImportDeclaration(node) {\n // Check if importing from a store file\n const source = node.source.value as string;\n if (!source.includes(\"store\")) return;\n\n // Check imported specifiers\n for (const specifier of node.specifiers) {\n if (specifier.type === \"ImportSpecifier\") {\n const importedName =\n specifier.imported.type === \"Identifier\"\n ? specifier.imported.name\n : specifier.imported.value;\n\n if (regex.test(importedName)) {\n context.report({\n node: specifier,\n messageId: \"noDirectImport\",\n data: { name: importedName },\n });\n }\n }\n\n if (specifier.type === \"ImportDefaultSpecifier\") {\n const localName = specifier.local.name;\n if (regex.test(localName)) {\n context.report({\n node: specifier,\n messageId: \"noDirectImport\",\n data: { name: localName },\n });\n }\n }\n }\n },\n };\n },\n});\n"],"mappings":";AAIA,SAAS,mBAAmB;AAErB,IAAM,aAAa,YAAY;AAAA,EACpC,CAAC,SACC,uFAAuF,IAAI;AAC/F;AAqLO,SAAS,eAAeA,OAA0B;AACvD,SAAOA;AACT;;;AC9KO,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,gBAAgB,CAAC,EAAE,cAAc,YAAY,CAAC;AAAA,EAC9C,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,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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsDR,CAAC;AAGD,SAAS,eAAe,SAAyB;AAC/C,QAAM,UAAU,QACb,QAAQ,qBAAqB,MAAM,EACnC,QAAQ,OAAO,IAAI,EACnB,QAAQ,OAAO,GAAG;AACrB,SAAO,IAAI,OAAO,IAAI,OAAO,GAAG;AAClC;AAEA,IAAO,iCAAQ,WAAgC;AAAA,EAC7C,MAAM;AAAA,EACN,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aACE;AAAA,IACJ;AAAA,IACA,UAAU;AAAA,MACR,gBACE;AAAA,IACJ;AAAA,IACA,QAAQ;AAAA,MACN;AAAA,QACE,MAAM;AAAA,QACN,YAAY;AAAA,UACV,cAAc;AAAA,YACZ,MAAM;AAAA,YACN,aAAa;AAAA,UACf;AAAA,QACF;AAAA,QACA,sBAAsB;AAAA,MACxB;AAAA,IACF;AAAA,EACF;AAAA,EACA,gBAAgB,CAAC,EAAE,cAAc,YAAY,CAAC;AAAA,EAC9C,OAAO,SAAS;AACd,UAAM,UAAU,QAAQ,QAAQ,CAAC,KAAK,CAAC;AACvC,UAAM,UAAU,QAAQ,gBAAgB;AACxC,UAAM,QAAQ,eAAe,OAAO;AAEpC,WAAO;AAAA,MACL,kBAAkB,MAAM;AAEtB,cAAM,SAAS,KAAK,OAAO;AAC3B,YAAI,CAAC,OAAO,SAAS,OAAO,EAAG;AAG/B,mBAAW,aAAa,KAAK,YAAY;AACvC,cAAI,UAAU,SAAS,mBAAmB;AACxC,kBAAM,eACJ,UAAU,SAAS,SAAS,eACxB,UAAU,SAAS,OACnB,UAAU,SAAS;AAEzB,gBAAI,MAAM,KAAK,YAAY,GAAG;AAC5B,sBAAQ,OAAO;AAAA,gBACb,MAAM;AAAA,gBACN,WAAW;AAAA,gBACX,MAAM,EAAE,MAAM,aAAa;AAAA,cAC7B,CAAC;AAAA,YACH;AAAA,UACF;AAEA,cAAI,UAAU,SAAS,0BAA0B;AAC/C,kBAAM,YAAY,UAAU,MAAM;AAClC,gBAAI,MAAM,KAAK,SAAS,GAAG;AACzB,sBAAQ,OAAO;AAAA,gBACb,MAAM;AAAA,gBACN,WAAW;AAAA,gBACX,MAAM,EAAE,MAAM,UAAU;AAAA,cAC1B,CAAC;AAAA,YACH;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF,CAAC;","names":["meta"]}
1
+ {"version":3,"sources":["../../src/utils/create-rule.ts","../../src/rules/no-direct-store-import.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: no-direct-store-import\n *\n * Forbids direct Zustand store imports - prefer using hooks via context.\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\n\ntype MessageIds = \"noDirectImport\";\ntype Options = [\n {\n storePattern?: string;\n }\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"no-direct-store-import\",\n version: \"1.0.0\",\n name: \"No Direct Store Import\",\n description: \"Forbid direct Zustand store imports (use context hooks)\",\n defaultSeverity: \"warn\",\n category: \"static\",\n icon: \"🏪\",\n hint: \"Encourages testable store access\",\n defaultEnabled: true,\n defaultOptions: [{ storePattern: \"use*Store\" }],\n optionSchema: {\n fields: [\n {\n key: \"storePattern\",\n label: \"Glob pattern for store files\",\n type: \"text\",\n defaultValue: \"use*Store\",\n placeholder: \"use*Store\",\n description: \"Pattern to match store file names\",\n },\n ],\n },\n docs: `\n## What it does\n\nPrevents direct imports of Zustand stores, encouraging the use of context-based hooks\nfor better dependency injection and testability.\n\n## Why it's useful\n\n- **Testability**: Context-based access allows easy mocking in tests\n- **Flexibility**: Store implementation can change without updating all consumers\n- **Dependency Injection**: Stores can be provided at different levels of the component tree\n- **Server Components**: Helps avoid accidentally importing stores in server components\n\n## Examples\n\n### ❌ Incorrect\n\n\\`\\`\\`tsx\n// Directly importing the store\nimport { useAuthStore } from '../stores/auth-store';\nimport { useCartStore } from '@/stores/useCartStore';\n\nfunction MyComponent() {\n const user = useAuthStore((s) => s.user);\n}\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n// Using context-provided hooks\nimport { useAuth } from '../contexts/auth-context';\nimport { useCart } from '@/hooks/useCart';\n\nfunction MyComponent() {\n const { user } = useAuth();\n}\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/no-direct-store-import\": [\"warn\", {\n storePattern: \"use*Store\" // Pattern to match store names\n}]\n\\`\\`\\`\n\n## Notes\n\n- The pattern uses glob syntax (\\`*\\` matches any characters)\n- Only triggers for imports from paths containing \"store\"\n- Works with both named and default imports\n`,\n});\n\n// Convert glob pattern to regex\nfunction patternToRegex(pattern: string): RegExp {\n const escaped = pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\*/g, \".*\")\n .replace(/\\?/g, \".\");\n return new RegExp(`^${escaped}$`);\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"no-direct-store-import\",\n meta: {\n type: \"problem\",\n docs: {\n description:\n \"Forbid direct Zustand store imports (use hooks via context)\",\n },\n messages: {\n noDirectImport:\n \"Avoid importing store '{{name}}' directly. Use the store via a context hook instead.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n storePattern: {\n type: \"string\",\n description: \"Glob pattern for store names\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{ storePattern: \"use*Store\" }],\n create(context) {\n const options = context.options[0] || {};\n const pattern = options.storePattern || \"use*Store\";\n const regex = patternToRegex(pattern);\n\n return {\n ImportDeclaration(node) {\n // Check if importing from a store file\n const source = node.source.value as string;\n if (!source.includes(\"store\")) return;\n\n // Check imported specifiers\n for (const specifier of node.specifiers) {\n if (specifier.type === \"ImportSpecifier\") {\n const importedName =\n specifier.imported.type === \"Identifier\"\n ? specifier.imported.name\n : specifier.imported.value;\n\n if (regex.test(importedName)) {\n context.report({\n node: specifier,\n messageId: \"noDirectImport\",\n data: { name: importedName },\n });\n }\n }\n\n if (specifier.type === \"ImportDefaultSpecifier\") {\n const localName = specifier.local.name;\n if (regex.test(localName)) {\n context.report({\n node: specifier,\n messageId: \"noDirectImport\",\n data: { name: localName },\n });\n }\n }\n }\n },\n };\n },\n});\n"],"mappings":"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","names":["meta"]}