uilint-eslint 0.2.17 → 0.2.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/rules/consistent-dark-mode.js +8 -4
- package/dist/rules/consistent-dark-mode.js.map +1 -1
- package/dist/rules/consistent-spacing.js +8 -4
- package/dist/rules/consistent-spacing.js.map +1 -1
- package/dist/rules/enforce-absolute-imports.js +8 -4
- package/dist/rules/enforce-absolute-imports.js.map +1 -1
- package/dist/rules/no-any-in-props.js +8 -4
- package/dist/rules/no-any-in-props.js.map +1 -1
- package/dist/rules/no-arbitrary-tailwind.js +8 -4
- package/dist/rules/no-arbitrary-tailwind.js.map +1 -1
- package/dist/rules/no-direct-store-import.js +8 -4
- package/dist/rules/no-direct-store-import.js.map +1 -1
- package/dist/rules/no-mixed-component-libraries.js +197 -9
- package/dist/rules/no-mixed-component-libraries.js.map +1 -1
- package/dist/rules/no-prop-drilling-depth.js +20 -7
- package/dist/rules/no-prop-drilling-depth.js.map +1 -1
- package/dist/rules/no-secrets-in-code.js +8 -4
- package/dist/rules/no-secrets-in-code.js.map +1 -1
- package/dist/rules/prefer-zustand-state-management.js +8 -4
- package/dist/rules/prefer-zustand-state-management.js.map +1 -1
- package/dist/rules/require-input-validation.js +8 -4
- package/dist/rules/require-input-validation.js.map +1 -1
- package/dist/rules/semantic-vision.js +11 -5
- package/dist/rules/semantic-vision.js.map +1 -1
- package/dist/rules/semantic.js +9 -5
- package/dist/rules/semantic.js.map +1 -1
- package/dist/rules/zustand-use-selectors.js +8 -4
- package/dist/rules/zustand-use-selectors.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-6EI7LWV5.js +0 -14
- package/dist/chunk-6EI7LWV5.js.map +0 -1
- package/dist/chunk-MFIU3O2I.js +0 -201
- package/dist/chunk-MFIU3O2I.js.map +0 -1
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// src/utils/create-rule.ts
|
|
2
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
3
|
+
var createRule = ESLintUtils.RuleCreator(
|
|
4
|
+
(name) => `https://github.com/peter-suggate/uilint/blob/main/packages/uilint-eslint/docs/rules/${name}.md`
|
|
5
|
+
);
|
|
6
|
+
function defineRuleMeta(meta2) {
|
|
7
|
+
return meta2;
|
|
8
|
+
}
|
|
5
9
|
|
|
6
10
|
// src/rules/consistent-dark-mode.ts
|
|
7
11
|
var meta = defineRuleMeta({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/rules/consistent-dark-mode.ts"],"sourcesContent":["/**\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 name: \"Consistent Dark Mode\",\n description: \"Ensure consistent dark: theming (error on mix, warn on missing)\",\n defaultSeverity: \"error\",\n category: \"static\",\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- **Supports semantic colors**: Automatically ignores shadcn/CSS variable colors that handle dark mode internally\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 colors (automatically themed)\n<div className=\"bg-background text-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- Semantic colors (like shadcn's \\`background\\`, \\`foreground\\`, \\`primary\\`, etc.) are exempt\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// Known non-color utilities that use color prefixes\n// These are utilities like text-lg (font size), text-center (alignment), etc.\nconst NON_COLOR_UTILITIES = new Set([\n // Exempt values (colorless or inherited) - don't need dark variants\n \"transparent\",\n \"inherit\",\n \"current\",\n \"auto\",\n \"none\",\n // text- utilities that aren't colors\n \"xs\",\n \"sm\",\n \"base\",\n \"lg\",\n \"xl\",\n \"2xl\",\n \"3xl\",\n \"4xl\",\n \"5xl\",\n \"6xl\",\n \"7xl\",\n \"8xl\",\n \"9xl\",\n \"left\",\n \"center\",\n \"right\",\n \"justify\",\n \"start\",\n \"end\",\n \"wrap\",\n \"nowrap\",\n \"balance\",\n \"pretty\",\n \"ellipsis\",\n \"clip\",\n // border- utilities that aren't colors\n \"0\",\n \"2\",\n \"4\",\n \"8\",\n \"solid\",\n \"dashed\",\n \"dotted\",\n \"double\",\n \"hidden\",\n \"collapse\",\n \"separate\",\n // shadow- utilities that aren't colors\n // Note: \"sm\", \"lg\", \"xl\", \"2xl\" already included above\n \"md\",\n \"inner\",\n // ring- utilities that aren't colors\n // Note: \"0\", \"2\", \"4\", \"8\" already included above\n \"1\",\n \"inset\",\n // outline- utilities that aren't colors\n // Note: numeric values already included\n \"offset-0\",\n \"offset-1\",\n \"offset-2\",\n \"offset-4\",\n \"offset-8\",\n // decoration- utilities that aren't colors\n // Note: \"solid\", \"double\", \"dotted\", \"dashed\" already included\n \"wavy\",\n \"from-font\",\n \"clone\",\n \"slice\",\n // divide- utilities that aren't colors\n \"x\",\n \"y\",\n \"x-0\",\n \"x-2\",\n \"x-4\",\n \"x-8\",\n \"y-0\",\n \"y-2\",\n \"y-4\",\n \"y-8\",\n \"x-reverse\",\n \"y-reverse\",\n // gradient direction utilities (from-, via-, to- prefixes)\n \"t\",\n \"tr\",\n \"r\",\n \"br\",\n \"b\",\n \"bl\",\n \"l\",\n \"tl\",\n]);\n\n// Semantic color names used by theming systems like shadcn\n// These are CSS variable-based colors that handle dark mode automatically\nconst SEMANTIC_COLOR_NAMES = new Set([\n // Core shadcn colors\n \"background\",\n \"foreground\",\n // Component colors\n \"card\",\n \"card-foreground\",\n \"popover\",\n \"popover-foreground\",\n \"primary\",\n \"primary-foreground\",\n \"secondary\",\n \"secondary-foreground\",\n \"muted\",\n \"muted-foreground\",\n \"accent\",\n \"accent-foreground\",\n \"destructive\",\n \"destructive-foreground\",\n // Form/UI colors\n \"border\",\n \"input\",\n \"ring\",\n // Sidebar colors (shadcn sidebar component)\n \"sidebar\",\n \"sidebar-foreground\",\n \"sidebar-border\",\n \"sidebar-primary\",\n \"sidebar-primary-foreground\",\n \"sidebar-accent\",\n \"sidebar-accent-foreground\",\n \"sidebar-ring\",\n]);\n\n// Pattern for semantic chart colors (chart-1, chart-2, etc.)\nconst CHART_COLOR_PATTERN = /^chart-\\d+$/;\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 a semantic/themed color (e.g., shadcn)\n * These colors use CSS variables that automatically handle dark mode\n */\nfunction isSemanticColor(value: string): boolean {\n // Check for exact semantic color names\n if (SEMANTIC_COLOR_NAMES.has(value)) {\n return true;\n }\n\n // Check for chart colors (chart-1, chart-2, etc.)\n if (CHART_COLOR_PATTERN.test(value)) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Check if the value after the prefix looks like a color value\n * Uses an exclusion-based approach: anything that's not a known non-color utility\n * and not a semantic color is treated as a potential color.\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 // Check if it's a semantic/themed color (exempt from dark mode requirements)\n if (isSemanticColor(value)) {\n return false;\n }\n\n // Check if it's a known non-color utility\n if (NON_COLOR_UTILITIES.has(value)) {\n return false;\n }\n\n // Treat everything else as a potential color\n // This catches:\n // - Standard Tailwind colors: blue-500, slate-900, white, black\n // - Custom colors defined in tailwind.config: brand, primary (non-shadcn), custom-blue\n // - Arbitrary values: [#fff], [rgb(255,0,0)], [var(--my-color)]\n // - Opacity modifiers: blue-500/50, white/80\n return true;\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":[]}
|
|
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 * 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., \"no-arbitrary-tailwind\") - must match filename */\n id: 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 /** 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/**\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 name: \"Consistent Dark Mode\",\n description: \"Ensure consistent dark: theming (error on mix, warn on missing)\",\n defaultSeverity: \"error\",\n category: \"static\",\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- **Supports semantic colors**: Automatically ignores shadcn/CSS variable colors that handle dark mode internally\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 colors (automatically themed)\n<div className=\"bg-background text-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- Semantic colors (like shadcn's \\`background\\`, \\`foreground\\`, \\`primary\\`, etc.) are exempt\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// Known non-color utilities that use color prefixes\n// These are utilities like text-lg (font size), text-center (alignment), etc.\nconst NON_COLOR_UTILITIES = new Set([\n // Exempt values (colorless or inherited) - don't need dark variants\n \"transparent\",\n \"inherit\",\n \"current\",\n \"auto\",\n \"none\",\n // text- utilities that aren't colors\n \"xs\",\n \"sm\",\n \"base\",\n \"lg\",\n \"xl\",\n \"2xl\",\n \"3xl\",\n \"4xl\",\n \"5xl\",\n \"6xl\",\n \"7xl\",\n \"8xl\",\n \"9xl\",\n \"left\",\n \"center\",\n \"right\",\n \"justify\",\n \"start\",\n \"end\",\n \"wrap\",\n \"nowrap\",\n \"balance\",\n \"pretty\",\n \"ellipsis\",\n \"clip\",\n // border- utilities that aren't colors\n \"0\",\n \"2\",\n \"4\",\n \"8\",\n \"solid\",\n \"dashed\",\n \"dotted\",\n \"double\",\n \"hidden\",\n \"collapse\",\n \"separate\",\n // shadow- utilities that aren't colors\n // Note: \"sm\", \"lg\", \"xl\", \"2xl\" already included above\n \"md\",\n \"inner\",\n // ring- utilities that aren't colors\n // Note: \"0\", \"2\", \"4\", \"8\" already included above\n \"1\",\n \"inset\",\n // outline- utilities that aren't colors\n // Note: numeric values already included\n \"offset-0\",\n \"offset-1\",\n \"offset-2\",\n \"offset-4\",\n \"offset-8\",\n // decoration- utilities that aren't colors\n // Note: \"solid\", \"double\", \"dotted\", \"dashed\" already included\n \"wavy\",\n \"from-font\",\n \"clone\",\n \"slice\",\n // divide- utilities that aren't colors\n \"x\",\n \"y\",\n \"x-0\",\n \"x-2\",\n \"x-4\",\n \"x-8\",\n \"y-0\",\n \"y-2\",\n \"y-4\",\n \"y-8\",\n \"x-reverse\",\n \"y-reverse\",\n // gradient direction utilities (from-, via-, to- prefixes)\n \"t\",\n \"tr\",\n \"r\",\n \"br\",\n \"b\",\n \"bl\",\n \"l\",\n \"tl\",\n]);\n\n// Semantic color names used by theming systems like shadcn\n// These are CSS variable-based colors that handle dark mode automatically\nconst SEMANTIC_COLOR_NAMES = new Set([\n // Core shadcn colors\n \"background\",\n \"foreground\",\n // Component colors\n \"card\",\n \"card-foreground\",\n \"popover\",\n \"popover-foreground\",\n \"primary\",\n \"primary-foreground\",\n \"secondary\",\n \"secondary-foreground\",\n \"muted\",\n \"muted-foreground\",\n \"accent\",\n \"accent-foreground\",\n \"destructive\",\n \"destructive-foreground\",\n // Form/UI colors\n \"border\",\n \"input\",\n \"ring\",\n // Sidebar colors (shadcn sidebar component)\n \"sidebar\",\n \"sidebar-foreground\",\n \"sidebar-border\",\n \"sidebar-primary\",\n \"sidebar-primary-foreground\",\n \"sidebar-accent\",\n \"sidebar-accent-foreground\",\n \"sidebar-ring\",\n]);\n\n// Pattern for semantic chart colors (chart-1, chart-2, etc.)\nconst CHART_COLOR_PATTERN = /^chart-\\d+$/;\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 a semantic/themed color (e.g., shadcn)\n * These colors use CSS variables that automatically handle dark mode\n */\nfunction isSemanticColor(value: string): boolean {\n // Check for exact semantic color names\n if (SEMANTIC_COLOR_NAMES.has(value)) {\n return true;\n }\n\n // Check for chart colors (chart-1, chart-2, etc.)\n if (CHART_COLOR_PATTERN.test(value)) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Check if the value after the prefix looks like a color value\n * Uses an exclusion-based approach: anything that's not a known non-color utility\n * and not a semantic color is treated as a potential color.\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 // Check if it's a semantic/themed color (exempt from dark mode requirements)\n if (isSemanticColor(value)) {\n return false;\n }\n\n // Check if it's a known non-color utility\n if (NON_COLOR_UTILITIES.has(value)) {\n return false;\n }\n\n // Treat everything else as a potential color\n // This catches:\n // - Standard Tailwind colors: blue-500, slate-900, white, black\n // - Custom colors defined in tailwind.config: brand, primary (non-shadcn), custom-blue\n // - Arbitrary values: [#fff], [rgb(255,0,0)], [var(--my-color)]\n // - Opacity modifiers: blue-500/50, white/80\n return true;\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,7 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// src/utils/create-rule.ts
|
|
2
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
3
|
+
var createRule = ESLintUtils.RuleCreator(
|
|
4
|
+
(name) => `https://github.com/peter-suggate/uilint/blob/main/packages/uilint-eslint/docs/rules/${name}.md`
|
|
5
|
+
);
|
|
6
|
+
function defineRuleMeta(meta2) {
|
|
7
|
+
return meta2;
|
|
8
|
+
}
|
|
5
9
|
|
|
6
10
|
// src/rules/consistent-spacing.ts
|
|
7
11
|
var meta = defineRuleMeta({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/rules/consistent-spacing.ts"],"sourcesContent":["/**\n * Rule: consistent-spacing\n *\n * Enforces use of spacing scale values in gap, padding, margin utilities.\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\n\ntype MessageIds = \"invalidSpacing\";\ntype Options = [\n {\n scale?: number[];\n }\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"consistent-spacing\",\n name: \"Consistent Spacing\",\n description: \"Enforce spacing scale (no magic numbers in gap/padding)\",\n defaultSeverity: \"warn\",\n category: \"static\",\n defaultOptions: [{ scale: [0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16] }],\n optionSchema: {\n fields: [\n {\n key: \"scale\",\n label: \"Allowed spacing values\",\n type: \"text\",\n defaultValue: [0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16],\n placeholder: \"0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16\",\n description: \"Comma-separated list of allowed spacing values\",\n },\n ],\n },\n docs: `\n## What it does\n\nEnsures all spacing utilities (padding, margin, gap, etc.) use values from a defined scale.\nThis prevents \"magic number\" spacing values that create visual inconsistency.\n\n## Why it's useful\n\n- **Visual rhythm**: Consistent spacing creates a professional, cohesive feel\n- **Design system compliance**: Enforces your spacing tokens\n- **Easier maintenance**: Spacing changes can be made systematically\n\n## Examples\n\n### ❌ Incorrect\n\n\\`\\`\\`tsx\n<div className=\"p-7\"> // 7 isn't in the default scale\n<div className=\"gap-13\"> // 13 isn't in the default scale\n<div className=\"mt-9\"> // 9 isn't in the default scale\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n<div className=\"p-8\"> // 8 is in the scale\n<div className=\"gap-12\"> // 12 is in the scale\n<div className=\"mt-10\"> // 10 is in the scale\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/consistent-spacing\": [\"warn\", {\n scale: [0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24]\n}]\n\\`\\`\\`\n\nThe default scale is Tailwind's standard spacing values. Customize it to match your design system.\n`,\n});\n\n// Default Tailwind spacing scale\nconst DEFAULT_SCALE = [\n 0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16, 20, 24,\n 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 72, 80, 96,\n];\n\n// Spacing utilities that take numeric values\nconst SPACING_PREFIXES = [\n \"p-\",\n \"px-\",\n \"py-\",\n \"pt-\",\n \"pr-\",\n \"pb-\",\n \"pl-\",\n \"ps-\",\n \"pe-\",\n \"m-\",\n \"mx-\",\n \"my-\",\n \"mt-\",\n \"mr-\",\n \"mb-\",\n \"ml-\",\n \"ms-\",\n \"me-\",\n \"gap-\",\n \"gap-x-\",\n \"gap-y-\",\n \"space-x-\",\n \"space-y-\",\n \"inset-\",\n \"inset-x-\",\n \"inset-y-\",\n \"top-\",\n \"right-\",\n \"bottom-\",\n \"left-\",\n \"w-\",\n \"h-\",\n \"min-w-\",\n \"min-h-\",\n \"max-w-\",\n \"max-h-\",\n \"size-\",\n];\n\n// Build regex to match spacing utilities with numeric values\nfunction buildSpacingRegex(): RegExp {\n const prefixes = SPACING_PREFIXES.map((p) => p.replace(\"-\", \"\\\\-\")).join(\"|\");\n // Match prefix followed by a number (possibly with decimal)\n return new RegExp(`\\\\b(${prefixes})(\\\\d+\\\\.?\\\\d*)\\\\b`, \"g\");\n}\n\nconst SPACING_REGEX = buildSpacingRegex();\n\nexport default createRule<Options, MessageIds>({\n name: \"consistent-spacing\",\n meta: {\n type: \"suggestion\",\n docs: {\n description: \"Enforce spacing scale (no magic numbers in gap/padding)\",\n },\n messages: {\n invalidSpacing:\n \"Spacing value '{{value}}' is not in the allowed scale. Use one of: {{allowed}}\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n scale: {\n type: \"array\",\n items: { type: \"number\" },\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{ scale: DEFAULT_SCALE }],\n create(context) {\n const options = context.options[0] || {};\n const scale = new Set((options.scale || DEFAULT_SCALE).map(String));\n const scaleList = [...scale].slice(0, 10).join(\", \") + \"...\";\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 if (value?.type === \"Literal\" && typeof value.value === \"string\") {\n checkSpacing(context, node, value.value, scale, scaleList);\n }\n\n if (value?.type === \"JSXExpressionContainer\") {\n const expr = value.expression;\n if (expr.type === \"Literal\" && typeof expr.value === \"string\") {\n checkSpacing(context, node, expr.value, scale, scaleList);\n }\n if (expr.type === \"TemplateLiteral\") {\n for (const quasi of expr.quasis) {\n checkSpacing(context, node, quasi.value.raw, scale, scaleList);\n }\n }\n }\n }\n },\n\n // Check cn(), clsx(), classnames() calls\n CallExpression(node) {\n if (node.callee.type !== \"Identifier\") return;\n const name = node.callee.name;\n\n if (name === \"cn\" || name === \"clsx\" || name === \"classnames\") {\n for (const arg of node.arguments) {\n if (arg.type === \"Literal\" && typeof arg.value === \"string\") {\n checkSpacing(context, arg, arg.value, scale, scaleList);\n }\n if (arg.type === \"TemplateLiteral\") {\n for (const quasi of arg.quasis) {\n checkSpacing(context, quasi, quasi.value.raw, scale, scaleList);\n }\n }\n }\n }\n },\n };\n },\n});\n\nfunction checkSpacing(\n context: Parameters<\n ReturnType<typeof createRule<Options, \"invalidSpacing\">>[\"create\"]\n >[0],\n node: TSESTree.Node,\n classString: string,\n scale: Set<string>,\n scaleList: string\n) {\n // Reset regex state\n SPACING_REGEX.lastIndex = 0;\n\n let match;\n while ((match = SPACING_REGEX.exec(classString)) !== null) {\n const [, , value] = match;\n if (value && !scale.has(value)) {\n context.report({\n node,\n messageId: \"invalidSpacing\",\n data: { value, allowed: scaleList },\n });\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/create-rule.ts","../../src/rules/consistent-spacing.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 * 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., \"no-arbitrary-tailwind\") - must match filename */\n id: 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 /** 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/**\n * Helper to define rule metadata with type safety\n */\nexport function defineRuleMeta(meta: RuleMeta): RuleMeta {\n return meta;\n}\n","/**\n * Rule: consistent-spacing\n *\n * Enforces use of spacing scale values in gap, padding, margin utilities.\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\n\ntype MessageIds = \"invalidSpacing\";\ntype Options = [\n {\n scale?: number[];\n }\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"consistent-spacing\",\n name: \"Consistent Spacing\",\n description: \"Enforce spacing scale (no magic numbers in gap/padding)\",\n defaultSeverity: \"warn\",\n category: \"static\",\n defaultOptions: [{ scale: [0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16] }],\n optionSchema: {\n fields: [\n {\n key: \"scale\",\n label: \"Allowed spacing values\",\n type: \"text\",\n defaultValue: [0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16],\n placeholder: \"0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16\",\n description: \"Comma-separated list of allowed spacing values\",\n },\n ],\n },\n docs: `\n## What it does\n\nEnsures all spacing utilities (padding, margin, gap, etc.) use values from a defined scale.\nThis prevents \"magic number\" spacing values that create visual inconsistency.\n\n## Why it's useful\n\n- **Visual rhythm**: Consistent spacing creates a professional, cohesive feel\n- **Design system compliance**: Enforces your spacing tokens\n- **Easier maintenance**: Spacing changes can be made systematically\n\n## Examples\n\n### ❌ Incorrect\n\n\\`\\`\\`tsx\n<div className=\"p-7\"> // 7 isn't in the default scale\n<div className=\"gap-13\"> // 13 isn't in the default scale\n<div className=\"mt-9\"> // 9 isn't in the default scale\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n<div className=\"p-8\"> // 8 is in the scale\n<div className=\"gap-12\"> // 12 is in the scale\n<div className=\"mt-10\"> // 10 is in the scale\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/consistent-spacing\": [\"warn\", {\n scale: [0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24]\n}]\n\\`\\`\\`\n\nThe default scale is Tailwind's standard spacing values. Customize it to match your design system.\n`,\n});\n\n// Default Tailwind spacing scale\nconst DEFAULT_SCALE = [\n 0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16, 20, 24,\n 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 72, 80, 96,\n];\n\n// Spacing utilities that take numeric values\nconst SPACING_PREFIXES = [\n \"p-\",\n \"px-\",\n \"py-\",\n \"pt-\",\n \"pr-\",\n \"pb-\",\n \"pl-\",\n \"ps-\",\n \"pe-\",\n \"m-\",\n \"mx-\",\n \"my-\",\n \"mt-\",\n \"mr-\",\n \"mb-\",\n \"ml-\",\n \"ms-\",\n \"me-\",\n \"gap-\",\n \"gap-x-\",\n \"gap-y-\",\n \"space-x-\",\n \"space-y-\",\n \"inset-\",\n \"inset-x-\",\n \"inset-y-\",\n \"top-\",\n \"right-\",\n \"bottom-\",\n \"left-\",\n \"w-\",\n \"h-\",\n \"min-w-\",\n \"min-h-\",\n \"max-w-\",\n \"max-h-\",\n \"size-\",\n];\n\n// Build regex to match spacing utilities with numeric values\nfunction buildSpacingRegex(): RegExp {\n const prefixes = SPACING_PREFIXES.map((p) => p.replace(\"-\", \"\\\\-\")).join(\"|\");\n // Match prefix followed by a number (possibly with decimal)\n return new RegExp(`\\\\b(${prefixes})(\\\\d+\\\\.?\\\\d*)\\\\b`, \"g\");\n}\n\nconst SPACING_REGEX = buildSpacingRegex();\n\nexport default createRule<Options, MessageIds>({\n name: \"consistent-spacing\",\n meta: {\n type: \"suggestion\",\n docs: {\n description: \"Enforce spacing scale (no magic numbers in gap/padding)\",\n },\n messages: {\n invalidSpacing:\n \"Spacing value '{{value}}' is not in the allowed scale. Use one of: {{allowed}}\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n scale: {\n type: \"array\",\n items: { type: \"number\" },\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{ scale: DEFAULT_SCALE }],\n create(context) {\n const options = context.options[0] || {};\n const scale = new Set((options.scale || DEFAULT_SCALE).map(String));\n const scaleList = [...scale].slice(0, 10).join(\", \") + \"...\";\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 if (value?.type === \"Literal\" && typeof value.value === \"string\") {\n checkSpacing(context, node, value.value, scale, scaleList);\n }\n\n if (value?.type === \"JSXExpressionContainer\") {\n const expr = value.expression;\n if (expr.type === \"Literal\" && typeof expr.value === \"string\") {\n checkSpacing(context, node, expr.value, scale, scaleList);\n }\n if (expr.type === \"TemplateLiteral\") {\n for (const quasi of expr.quasis) {\n checkSpacing(context, node, quasi.value.raw, scale, scaleList);\n }\n }\n }\n }\n },\n\n // Check cn(), clsx(), classnames() calls\n CallExpression(node) {\n if (node.callee.type !== \"Identifier\") return;\n const name = node.callee.name;\n\n if (name === \"cn\" || name === \"clsx\" || name === \"classnames\") {\n for (const arg of node.arguments) {\n if (arg.type === \"Literal\" && typeof arg.value === \"string\") {\n checkSpacing(context, arg, arg.value, scale, scaleList);\n }\n if (arg.type === \"TemplateLiteral\") {\n for (const quasi of arg.quasis) {\n checkSpacing(context, quasi, quasi.value.raw, scale, scaleList);\n }\n }\n }\n }\n },\n };\n },\n});\n\nfunction checkSpacing(\n context: Parameters<\n ReturnType<typeof createRule<Options, \"invalidSpacing\">>[\"create\"]\n >[0],\n node: TSESTree.Node,\n classString: string,\n scale: Set<string>,\n scaleList: string\n) {\n // Reset regex state\n SPACING_REGEX.lastIndex = 0;\n\n let match;\n while ((match = SPACING_REGEX.exec(classString)) !== null) {\n const [, , value] = match;\n if (value && !scale.has(value)) {\n context.report({\n node,\n messageId: \"invalidSpacing\",\n data: { value, allowed: scaleList },\n });\n }\n }\n}\n"],"mappings":"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","names":["meta"]}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// src/utils/create-rule.ts
|
|
2
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
3
|
+
var createRule = ESLintUtils.RuleCreator(
|
|
4
|
+
(name) => `https://github.com/peter-suggate/uilint/blob/main/packages/uilint-eslint/docs/rules/${name}.md`
|
|
5
|
+
);
|
|
6
|
+
function defineRuleMeta(meta2) {
|
|
7
|
+
return meta2;
|
|
8
|
+
}
|
|
5
9
|
|
|
6
10
|
// src/rules/enforce-absolute-imports.ts
|
|
7
11
|
var meta = defineRuleMeta({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/rules/enforce-absolute-imports.ts"],"sourcesContent":["/**\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\";\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 name: \"Enforce Absolute Imports\",\n description:\n \"Require alias imports for paths beyond a configurable directory depth\",\n defaultSeverity: \"warn\",\n category: \"static\",\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: { loc?: { start: { line: number; column: number } } }\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: node as Parameters<typeof context.report>[0][\"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":[]}
|
|
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 * 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., \"no-arbitrary-tailwind\") - must match filename */\n id: 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 /** 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/**\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\";\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 name: \"Enforce Absolute Imports\",\n description:\n \"Require alias imports for paths beyond a configurable directory depth\",\n defaultSeverity: \"warn\",\n category: \"static\",\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: { loc?: { start: { line: number; column: number } } }\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: node as Parameters<typeof context.report>[0][\"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,7 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// src/utils/create-rule.ts
|
|
2
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
3
|
+
var createRule = ESLintUtils.RuleCreator(
|
|
4
|
+
(name) => `https://github.com/peter-suggate/uilint/blob/main/packages/uilint-eslint/docs/rules/${name}.md`
|
|
5
|
+
);
|
|
6
|
+
function defineRuleMeta(meta2) {
|
|
7
|
+
return meta2;
|
|
8
|
+
}
|
|
5
9
|
|
|
6
10
|
// src/rules/no-any-in-props.ts
|
|
7
11
|
var meta = defineRuleMeta({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/rules/no-any-in-props.ts"],"sourcesContent":["/**\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 name: \"No Any in Props\",\n description: \"Disallow 'any' type in React component props\",\n defaultSeverity: \"error\",\n category: \"static\",\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 if (\n param.typeAnnotation?.typeAnnotation &&\n param.type !== \"RestElement\"\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":[]}
|
|
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 * 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., \"no-arbitrary-tailwind\") - must match filename */\n id: 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 /** 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/**\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 name: \"No Any in Props\",\n description: \"Disallow 'any' type in React component props\",\n defaultSeverity: \"error\",\n category: \"static\",\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 if (\n param.typeAnnotation?.typeAnnotation &&\n param.type !== \"RestElement\"\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,7 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// src/utils/create-rule.ts
|
|
2
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
3
|
+
var createRule = ESLintUtils.RuleCreator(
|
|
4
|
+
(name) => `https://github.com/peter-suggate/uilint/blob/main/packages/uilint-eslint/docs/rules/${name}.md`
|
|
5
|
+
);
|
|
6
|
+
function defineRuleMeta(meta2) {
|
|
7
|
+
return meta2;
|
|
8
|
+
}
|
|
5
9
|
|
|
6
10
|
// src/rules/no-arbitrary-tailwind.ts
|
|
7
11
|
var meta = defineRuleMeta({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/rules/no-arbitrary-tailwind.ts"],"sourcesContent":["/**\n * Rule: no-arbitrary-tailwind\n *\n * Forbids arbitrary Tailwind values like w-[123px], bg-[#fff], etc.\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\n\ntype MessageIds = \"noArbitraryValue\";\ntype Options = [];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"no-arbitrary-tailwind\",\n name: \"No Arbitrary Tailwind\",\n description: \"Forbid arbitrary values like w-[123px], bg-[#fff]\",\n defaultSeverity: \"error\",\n category: \"static\",\n docs: `\n## What it does\n\nPrevents the use of arbitrary Tailwind CSS values (bracket notation) in your codebase.\nArbitrary values like \\`w-[123px]\\`, \\`bg-[#ff5500]\\`, or \\`text-[14px]\\` bypass Tailwind's\ndesign system and can lead to inconsistent UI.\n\n## Why it's useful\n\n- **Consistency**: Forces use of your design system's spacing, colors, and typography scales\n- **Maintainability**: Changes to design tokens automatically propagate everywhere\n- **Performance**: Arbitrary values generate extra CSS that can't be deduplicated\n\n## Examples\n\n### ❌ Incorrect\n\n\\`\\`\\`tsx\n<div className=\"w-[123px] h-[456px]\"> // Arbitrary dimensions\n<div className=\"bg-[#ff5500]\"> // Arbitrary color\n<div className=\"text-[14px]\"> // Arbitrary font size\n<div className=\"p-[7px]\"> // Arbitrary padding\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n<div className=\"w-32 h-96\"> // Use spacing scale\n<div className=\"bg-orange-500\"> // Use color palette\n<div className=\"text-sm\"> // Use typography scale\n<div className=\"p-2\"> // Use spacing scale\n\\`\\`\\`\n\n## Notes\n\n- Data attributes like \\`data-[state=open]\\` are allowed as they don't affect styling\n- If you need a truly custom value, consider extending your Tailwind config instead\n`,\n});\n\n// Regex to match arbitrary Tailwind values: word-[anything]\nconst ARBITRARY_VALUE_REGEX = /\\b[\\w-]+-\\[[^\\]]+\\]/g;\n\nexport default createRule<Options, MessageIds>({\n name: \"no-arbitrary-tailwind\",\n meta: {\n type: \"problem\",\n docs: {\n description: \"Forbid arbitrary Tailwind values like w-[123px]\",\n },\n messages: {\n noArbitraryValue:\n \"Avoid arbitrary Tailwind value '{{value}}'. Use the spacing/color scale instead.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\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 checkClassString(context, 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 checkClassString(context, expr, expr.value);\n }\n\n // Template literal: className={`...`}\n if (expr.type === \"TemplateLiteral\") {\n for (const quasi of expr.quasis) {\n checkClassString(context, quasi, quasi.value.raw);\n }\n }\n }\n }\n },\n\n // Check cn(), clsx(), classnames() calls\n CallExpression(node) {\n if (node.callee.type !== \"Identifier\") return;\n const name = node.callee.name;\n\n if (name === \"cn\" || name === \"clsx\" || name === \"classnames\") {\n for (const arg of node.arguments) {\n if (arg.type === \"Literal\" && typeof arg.value === \"string\") {\n checkClassString(context, arg, arg.value);\n }\n if (arg.type === \"TemplateLiteral\") {\n for (const quasi of arg.quasis) {\n checkClassString(context, quasi, quasi.value.raw);\n }\n }\n }\n }\n },\n };\n },\n});\n\nfunction checkClassString(\n context: Parameters<\n ReturnType<typeof createRule<[], \"noArbitraryValue\">>[\"create\"]\n >[0],\n node: TSESTree.Node,\n classString: string\n) {\n const matches = classString.matchAll(ARBITRARY_VALUE_REGEX);\n\n for (const match of matches) {\n // Ignore data attributes (e.g., data-[value], data-test-[something])\n if (match[0].startsWith(\"data-\")) {\n continue;\n }\n\n context.report({\n node,\n messageId: \"noArbitraryValue\",\n data: { value: match[0] },\n });\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/utils/create-rule.ts","../../src/rules/no-arbitrary-tailwind.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 * 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., \"no-arbitrary-tailwind\") - must match filename */\n id: 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 /** 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/**\n * Helper to define rule metadata with type safety\n */\nexport function defineRuleMeta(meta: RuleMeta): RuleMeta {\n return meta;\n}\n","/**\n * Rule: no-arbitrary-tailwind\n *\n * Forbids arbitrary Tailwind values like w-[123px], bg-[#fff], etc.\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\n\ntype MessageIds = \"noArbitraryValue\";\ntype Options = [];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"no-arbitrary-tailwind\",\n name: \"No Arbitrary Tailwind\",\n description: \"Forbid arbitrary values like w-[123px], bg-[#fff]\",\n defaultSeverity: \"error\",\n category: \"static\",\n docs: `\n## What it does\n\nPrevents the use of arbitrary Tailwind CSS values (bracket notation) in your codebase.\nArbitrary values like \\`w-[123px]\\`, \\`bg-[#ff5500]\\`, or \\`text-[14px]\\` bypass Tailwind's\ndesign system and can lead to inconsistent UI.\n\n## Why it's useful\n\n- **Consistency**: Forces use of your design system's spacing, colors, and typography scales\n- **Maintainability**: Changes to design tokens automatically propagate everywhere\n- **Performance**: Arbitrary values generate extra CSS that can't be deduplicated\n\n## Examples\n\n### ❌ Incorrect\n\n\\`\\`\\`tsx\n<div className=\"w-[123px] h-[456px]\"> // Arbitrary dimensions\n<div className=\"bg-[#ff5500]\"> // Arbitrary color\n<div className=\"text-[14px]\"> // Arbitrary font size\n<div className=\"p-[7px]\"> // Arbitrary padding\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n<div className=\"w-32 h-96\"> // Use spacing scale\n<div className=\"bg-orange-500\"> // Use color palette\n<div className=\"text-sm\"> // Use typography scale\n<div className=\"p-2\"> // Use spacing scale\n\\`\\`\\`\n\n## Notes\n\n- Data attributes like \\`data-[state=open]\\` are allowed as they don't affect styling\n- If you need a truly custom value, consider extending your Tailwind config instead\n`,\n});\n\n// Regex to match arbitrary Tailwind values: word-[anything]\nconst ARBITRARY_VALUE_REGEX = /\\b[\\w-]+-\\[[^\\]]+\\]/g;\n\nexport default createRule<Options, MessageIds>({\n name: \"no-arbitrary-tailwind\",\n meta: {\n type: \"problem\",\n docs: {\n description: \"Forbid arbitrary Tailwind values like w-[123px]\",\n },\n messages: {\n noArbitraryValue:\n \"Avoid arbitrary Tailwind value '{{value}}'. Use the spacing/color scale instead.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\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 checkClassString(context, 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 checkClassString(context, expr, expr.value);\n }\n\n // Template literal: className={`...`}\n if (expr.type === \"TemplateLiteral\") {\n for (const quasi of expr.quasis) {\n checkClassString(context, quasi, quasi.value.raw);\n }\n }\n }\n }\n },\n\n // Check cn(), clsx(), classnames() calls\n CallExpression(node) {\n if (node.callee.type !== \"Identifier\") return;\n const name = node.callee.name;\n\n if (name === \"cn\" || name === \"clsx\" || name === \"classnames\") {\n for (const arg of node.arguments) {\n if (arg.type === \"Literal\" && typeof arg.value === \"string\") {\n checkClassString(context, arg, arg.value);\n }\n if (arg.type === \"TemplateLiteral\") {\n for (const quasi of arg.quasis) {\n checkClassString(context, quasi, quasi.value.raw);\n }\n }\n }\n }\n },\n };\n },\n});\n\nfunction checkClassString(\n context: Parameters<\n ReturnType<typeof createRule<[], \"noArbitraryValue\">>[\"create\"]\n >[0],\n node: TSESTree.Node,\n classString: string\n) {\n const matches = classString.matchAll(ARBITRARY_VALUE_REGEX);\n\n for (const match of matches) {\n // Ignore data attributes (e.g., data-[value], data-test-[something])\n if (match[0].startsWith(\"data-\")) {\n continue;\n }\n\n context.report({\n node,\n messageId: \"noArbitraryValue\",\n data: { value: match[0] },\n });\n }\n}\n"],"mappings":";AAIA,SAAS,mBAAmB;AAErB,IAAM,aAAa,YAAY;AAAA,EACpC,CAAC,SACC,uFAAuF,IAAI;AAC/F;AA2EO,SAAS,eAAeA,OAA0B;AACvD,SAAOA;AACT;;;ACvEO,IAAM,OAAO,eAAe;AAAA,EACjC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,aAAa;AAAA,EACb,iBAAiB;AAAA,EACjB,UAAU;AAAA,EACV,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;AAsCR,CAAC;AAGD,IAAM,wBAAwB;AAE9B,IAAO,gCAAQ,WAAgC;AAAA,EAC7C,MAAM;AAAA,EACN,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aAAa;AAAA,IACf;AAAA,IACA,UAAU;AAAA,MACR,kBACE;AAAA,IACJ;AAAA,IACA,QAAQ,CAAC;AAAA,EACX;AAAA,EACA,gBAAgB,CAAC;AAAA,EACjB,OAAO,SAAS;AACd,WAAO;AAAA;AAAA,MAEL,aAAa,MAAM;AACjB,YACE,KAAK,KAAK,SAAS,oBAClB,KAAK,KAAK,SAAS,eAAe,KAAK,KAAK,SAAS,UACtD;AACA,gBAAM,QAAQ,KAAK;AAGnB,cAAI,OAAO,SAAS,aAAa,OAAO,MAAM,UAAU,UAAU;AAChE,6BAAiB,SAAS,OAAO,MAAM,KAAK;AAAA,UAC9C;AAGA,cAAI,OAAO,SAAS,0BAA0B;AAC5C,kBAAM,OAAO,MAAM;AAGnB,gBAAI,KAAK,SAAS,aAAa,OAAO,KAAK,UAAU,UAAU;AAC7D,+BAAiB,SAAS,MAAM,KAAK,KAAK;AAAA,YAC5C;AAGA,gBAAI,KAAK,SAAS,mBAAmB;AACnC,yBAAW,SAAS,KAAK,QAAQ;AAC/B,iCAAiB,SAAS,OAAO,MAAM,MAAM,GAAG;AAAA,cAClD;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA;AAAA,MAGA,eAAe,MAAM;AACnB,YAAI,KAAK,OAAO,SAAS,aAAc;AACvC,cAAM,OAAO,KAAK,OAAO;AAEzB,YAAI,SAAS,QAAQ,SAAS,UAAU,SAAS,cAAc;AAC7D,qBAAW,OAAO,KAAK,WAAW;AAChC,gBAAI,IAAI,SAAS,aAAa,OAAO,IAAI,UAAU,UAAU;AAC3D,+BAAiB,SAAS,KAAK,IAAI,KAAK;AAAA,YAC1C;AACA,gBAAI,IAAI,SAAS,mBAAmB;AAClC,yBAAW,SAAS,IAAI,QAAQ;AAC9B,iCAAiB,SAAS,OAAO,MAAM,MAAM,GAAG;AAAA,cAClD;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF,CAAC;AAED,SAAS,iBACP,SAGA,MACA,aACA;AACA,QAAM,UAAU,YAAY,SAAS,qBAAqB;AAE1D,aAAW,SAAS,SAAS;AAE3B,QAAI,MAAM,CAAC,EAAE,WAAW,OAAO,GAAG;AAChC;AAAA,IACF;AAEA,YAAQ,OAAO;AAAA,MACb;AAAA,MACA,WAAW;AAAA,MACX,MAAM,EAAE,OAAO,MAAM,CAAC,EAAE;AAAA,IAC1B,CAAC;AAAA,EACH;AACF;","names":["meta"]}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// src/utils/create-rule.ts
|
|
2
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
3
|
+
var createRule = ESLintUtils.RuleCreator(
|
|
4
|
+
(name) => `https://github.com/peter-suggate/uilint/blob/main/packages/uilint-eslint/docs/rules/${name}.md`
|
|
5
|
+
);
|
|
6
|
+
function defineRuleMeta(meta2) {
|
|
7
|
+
return meta2;
|
|
8
|
+
}
|
|
5
9
|
|
|
6
10
|
// src/rules/no-direct-store-import.ts
|
|
7
11
|
var meta = defineRuleMeta({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/rules/no-direct-store-import.ts"],"sourcesContent":["/**\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 name: \"No Direct Store Import\",\n description: \"Forbid direct Zustand store imports (use context hooks)\",\n defaultSeverity: \"warn\",\n category: \"static\",\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":"
|
|
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 * 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., \"no-arbitrary-tailwind\") - must match filename */\n id: 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 /** 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/**\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 name: \"No Direct Store Import\",\n description: \"Forbid direct Zustand store imports (use context hooks)\",\n defaultSeverity: \"warn\",\n category: \"static\",\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;AA2EO,SAAS,eAAeA,OAA0B;AACvD,SAAOA;AACT;;;ACpEO,IAAM,OAAO,eAAe;AAAA,EACjC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,aAAa;AAAA,EACb,iBAAiB;AAAA,EACjB,UAAU;AAAA,EACV,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"]}
|