unslop-ui 0.1.0
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/README.md +84 -0
- package/dist/chunk-34KLV5KL.js +1051 -0
- package/dist/chunk-34KLV5KL.js.map +1 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +63 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +113 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config/load-config.ts","../src/config/default-config.ts","../src/config/schema.ts","../src/utils/tailwind.ts","../src/rules/helpers.ts","../src/rules/arbitrary-tailwind-values.ts","../src/rules/card-lasagna.ts","../src/rules/generic-ai-hero.ts","../src/rules/gradient-soup.ts","../src/rules/inconsistent-icon-size.ts","../src/rules/muted-everything.ts","../src/rules/random-radius.ts","../src/rules/random-shadow.ts","../src/utils/source-location.ts","../src/rules/raw-hex-colors.ts","../src/rules/shadcn-default-look.ts","../src/rules/token-bypass.ts","../src/rules/index.ts","../src/scanner/scan-project.ts","../src/core/score.ts","../src/core/rule-engine.ts","../src/core/types.ts","../src/utils/paths.ts","../src/scanner/extract-classnames.ts","../src/scanner/extract-jsx.ts","../src/scanner/file-discovery.ts","../src/scanner/parse-file.ts","../src/reporters/json.ts","../src/reporters/terminal.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { createJiti } from \"jiti\";\nimport { defaultConfig } from \"./default-config\";\nimport { configSchema } from \"./schema\";\nimport type { ResolvedUnslopConfig, UnslopConfig } from \"../core/types\";\n\nconst CONFIG_FILES = [\n \"unslop.config.ts\",\n \"unslop.config.mts\",\n \"unslop.config.js\",\n \"unslop.config.mjs\",\n \"unslop.config.cjs\",\n \"unslop.config.json\",\n];\n\nexport interface LoadConfigOptions {\n cwd: string;\n configPath?: string;\n}\n\nexport interface LoadedConfig {\n config: ResolvedUnslopConfig;\n configPath?: string;\n}\n\nexport async function loadConfig(options: LoadConfigOptions): Promise<LoadedConfig> {\n const configPath = options.configPath\n ? path.resolve(options.cwd, options.configPath)\n : findConfigFile(options.cwd);\n\n if (!configPath) {\n return { config: defaultConfig };\n }\n\n if (!existsSync(configPath)) {\n throw new Error(`Config file not found: ${configPath}`);\n }\n\n const rawConfig = await importConfig(configPath);\n const parsed = configSchema.parse(rawConfig ?? {});\n\n return {\n config: mergeConfig(parsed),\n configPath,\n };\n}\n\nfunction findConfigFile(cwd: string): string | undefined {\n for (const fileName of CONFIG_FILES) {\n const candidate = path.join(cwd, fileName);\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n\n return undefined;\n}\n\nasync function importConfig(configPath: string): Promise<UnslopConfig> {\n if (configPath.endsWith(\".json\")) {\n return JSON.parse(readFileSync(configPath, \"utf8\")) as UnslopConfig;\n }\n\n const jiti = createJiti(import.meta.url);\n const loaded = await jiti.import(configPath, { default: true });\n return (loaded ?? {}) as UnslopConfig;\n}\n\nfunction mergeConfig(config: UnslopConfig): ResolvedUnslopConfig {\n return {\n include: config.include ?? defaultConfig.include,\n ignore: config.ignore ?? defaultConfig.ignore,\n stack: {\n ...defaultConfig.stack,\n ...config.stack,\n },\n rules: {\n ...defaultConfig.rules,\n ...config.rules,\n },\n };\n}\n","import type { ResolvedUnslopConfig } from \"../core/types\";\n\nexport const defaultConfig: ResolvedUnslopConfig = {\n include: [\n \"app/**/*.{ts,tsx,js,jsx}\",\n \"pages/**/*.{ts,tsx,js,jsx}\",\n \"components/**/*.{ts,tsx,js,jsx}\",\n \"src/**/*.{ts,tsx,js,jsx}\",\n ],\n ignore: [\n \"node_modules/**\",\n \".next/**\",\n \"dist/**\",\n \"build/**\",\n \"coverage/**\",\n \"*.test.*\",\n \"*.spec.*\",\n ],\n stack: {\n react: true,\n tailwind: true,\n shadcn: true,\n },\n rules: {\n \"ui-slop/token-bypass\": \"warn\",\n \"ui-slop/arbitrary-tailwind-values\": \"warn\",\n \"ui-slop/raw-hex-colors\": \"error\",\n \"ui-slop/gradient-soup\": \"warn\",\n \"ui-slop/card-lasagna\": \"warn\",\n \"ui-slop/muted-everything\": \"warn\",\n \"ui-slop/random-radius\": \"warn\",\n \"ui-slop/random-shadow\": \"warn\",\n \"ui-slop/shadcn-default-look\": \"warn\",\n \"ui-slop/generic-ai-hero\": \"warn\",\n \"ui-slop/inconsistent-icon-size\": \"warn\",\n },\n};\n","import { z } from \"zod\";\n\nexport const severitySchema = z.enum([\"off\", \"warn\", \"error\"]);\n\nexport const configSchema = z\n .object({\n include: z.array(z.string()).optional(),\n ignore: z.array(z.string()).optional(),\n stack: z\n .object({\n react: z.boolean().optional(),\n tailwind: z.boolean().optional(),\n shadcn: z.boolean().optional(),\n })\n .optional(),\n rules: z.record(z.string(), severitySchema).optional(),\n })\n .strict();\n","const PALETTE_COLORS = [\n \"slate\",\n \"gray\",\n \"zinc\",\n \"neutral\",\n \"stone\",\n \"red\",\n \"orange\",\n \"amber\",\n \"yellow\",\n \"lime\",\n \"green\",\n \"emerald\",\n \"teal\",\n \"cyan\",\n \"sky\",\n \"blue\",\n \"indigo\",\n \"violet\",\n \"purple\",\n \"fuchsia\",\n \"pink\",\n \"rose\",\n];\n\nconst COLOR_SCALE = \"(?:50|100|200|300|400|500|600|700|800|900|950)\";\nconst PALETTE_PATTERN = new RegExp(\n `^(?:bg|text|border|from|via|to|ring|fill|stroke)-(?:${PALETTE_COLORS.join(\"|\")})-${COLOR_SCALE}$`,\n);\n\nconst MUTED_TEXT_PATTERN = /^(?:text-muted-foreground|text-(?:gray|slate|zinc|neutral)-[45]00)$/;\n\nexport function splitClasses(value: string): string[] {\n return value\n .split(/\\s+/)\n .map((part) => part.trim())\n .filter(Boolean);\n}\n\nexport function isPaletteColorClass(className: string): boolean {\n return PALETTE_PATTERN.test(stripVariantPrefixes(className));\n}\n\nexport function isMutedTextClass(className: string): boolean {\n return MUTED_TEXT_PATTERN.test(stripVariantPrefixes(className));\n}\n\nexport function stripVariantPrefixes(className: string): string {\n const arbitraryVariantIndex = className.lastIndexOf(\"]:\");\n if (arbitraryVariantIndex >= 0) {\n return className.slice(arbitraryVariantIndex + 2);\n }\n\n const parts = className.split(\":\");\n return parts[parts.length - 1] ?? className;\n}\n\nexport function uniqueClasses(classes: string[]): string[] {\n return [...new Set(classes)];\n}\n","import type { Finding, Rule, RuleContext } from \"../core/types\";\n\nexport function createFinding(\n rule: Rule,\n context: RuleContext,\n options: Omit<Finding, \"ruleId\" | \"severity\" | \"filePath\" | \"category\">,\n): Finding {\n return {\n ruleId: rule.id,\n severity: rule.defaultSeverity === \"error\" ? \"error\" : \"warn\",\n filePath: context.filePath,\n category: rule.category,\n ...options,\n };\n}\n\nexport function allClasses(context: RuleContext): string[] {\n return context.classNames.flatMap((usage) => usage.classes);\n}\n\nexport function firstUsageLine(context: RuleContext, predicate: (className: string) => boolean) {\n const usage = context.classNames.find((item) => item.classes.some(predicate));\n return { line: usage?.line, column: usage?.column };\n}\n","import type { Rule } from \"../core/types\";\nimport { stripVariantPrefixes } from \"../utils/tailwind\";\nimport { allClasses, createFinding, firstUsageLine } from \"./helpers\";\n\nexport const arbitraryTailwindValuesRule: Rule = {\n id: \"ui-slop/arbitrary-tailwind-values\",\n name: \"Arbitrary Tailwind values\",\n category: \"design-system\",\n defaultSeverity: \"warn\",\n run(context) {\n const arbitraryClasses = allClasses(context).filter((className) =>\n /\\[[^\\]]+\\]/.test(stripVariantPrefixes(className)),\n );\n\n if (arbitraryClasses.length < 3) return [];\n\n const location = firstUsageLine(context, (className) => /\\[[^\\]]+\\]/.test(className));\n const stronger = arbitraryClasses.length >= 8;\n\n return [\n createFinding(this, context, {\n ...location,\n message: stronger\n ? `Found ${arbitraryClasses.length} arbitrary Tailwind values. This strongly suggests non-systematic spacing, sizing, or color choices.`\n : `Found ${arbitraryClasses.length} arbitrary Tailwind values. This often indicates generated or non-systematic UI.`,\n confidence: stronger ? \"high\" : \"medium\",\n suggestion: \"Prefer design tokens, Tailwind theme values, or a small documented exception.\",\n }),\n ];\n },\n};\n","import type { Rule } from \"../core/types\";\nimport { stripVariantPrefixes } from \"../utils/tailwind\";\nimport { createFinding } from \"./helpers\";\n\nexport const cardLasagnaRule: Rule = {\n id: \"ui-slop/card-lasagna\",\n name: \"Card lasagna\",\n category: \"layout\",\n defaultSeverity: \"warn\",\n run(context) {\n const cardLike = context.jsx.filter((element) => getCardSignals(element.classes).size >= 2);\n const deepCardLike = cardLike.filter((element) => element.depth >= 4);\n\n if (cardLike.length < 6 && deepCardLike.length < 3) return [];\n\n const first = cardLike[0];\n\n return [\n createFinding(this, context, {\n line: first?.line,\n column: first?.column,\n message: `Nested card-heavy layout detected: ${cardLike.length} card-like wrappers use stacked border, radius, shadow, background, or padding treatments.`,\n confidence: cardLike.length >= 9 || deepCardLike.length >= 3 ? \"high\" : \"medium\",\n suggestion: \"Flatten the hierarchy or reduce repeated borders, shadows, and rounded containers.\",\n }),\n ];\n },\n};\n\nfunction getCardSignals(classes: string[]): Set<string> {\n const signals = new Set<string>();\n\n for (const rawClass of classes) {\n const className = stripVariantPrefixes(rawClass);\n if (className === \"card\" || className === \"bg-card\" || className === \"bg-white\") {\n signals.add(\"background\");\n }\n if (className === \"border\" || className.startsWith(\"border-\")) {\n signals.add(\"border\");\n }\n if (className === \"rounded\" || className.startsWith(\"rounded-\")) {\n signals.add(\"radius\");\n }\n if (className === \"shadow\" || className.startsWith(\"shadow-\")) {\n signals.add(\"shadow\");\n }\n if (/^p[trblxy]?-(?:\\d+|\\[[^\\]]+\\])$/.test(className)) {\n signals.add(\"padding\");\n }\n }\n\n return signals;\n}\n","import type { Rule } from \"../core/types\";\nimport { stripVariantPrefixes } from \"../utils/tailwind\";\nimport { allClasses, createFinding } from \"./helpers\";\n\nconst GENERIC_WORDS = [\n \"ai-powered\",\n \"supercharge\",\n \"transform\",\n \"beautiful\",\n \"modern\",\n \"seamless\",\n \"workflow\",\n \"ship faster\",\n];\n\nexport const genericAiHeroRule: Rule = {\n id: \"ui-slop/generic-ai-hero\",\n name: \"Generic AI hero\",\n category: \"ai-slop\",\n defaultSeverity: \"warn\",\n run(context) {\n const classes = allClasses(context).map(stripVariantPrefixes);\n const sourceText = context.jsx.map((element) => element.text).join(\" \").toLowerCase();\n const sourceLower = context.source.toLowerCase();\n let score = 0;\n\n if (classes.includes(\"text-center\") && classes.some((className) => /^max-w-[34]xl$/.test(className))) {\n score += 2;\n }\n if (classes.some((className) => /^text-(?:5xl|6xl|7xl)$/.test(className)) || classes.includes(\"tracking-tight\")) {\n score += 1;\n }\n if (classes.includes(\"bg-clip-text\") && classes.includes(\"text-transparent\")) {\n score += 2;\n }\n if (classes.some((className) => /^(?:rounded-full|border|px-[23]|py-1)$/.test(className)) && /badge|pill/i.test(context.source)) {\n score += 1;\n }\n if ((sourceLower.match(/<button|<Button|role=[\"']button|href=/g) ?? []).length >= 2) {\n score += 1;\n }\n if (GENERIC_WORDS.some((word) => sourceText.includes(word))) {\n score += 2;\n }\n\n if (score < 5) return [];\n\n const firstHero = context.jsx.find((element) => [\"section\", \"main\", \"header\"].includes(element.elementName));\n\n return [\n createFinding(this, context, {\n line: firstHero?.line,\n column: firstHero?.column,\n message:\n \"Hero matches a common AI-generated SaaS pattern. Make the layout and copy more specific to the product.\",\n confidence: score >= 7 ? \"high\" : \"medium\",\n suggestion: \"Replace generic badge/headline/gradient/dual-CTA structure with product-specific proof and layout.\",\n }),\n ];\n },\n};\n","import type { Rule } from \"../core/types\";\nimport { stripVariantPrefixes } from \"../utils/tailwind\";\nimport { allClasses, createFinding, firstUsageLine } from \"./helpers\";\n\nconst AI_GRADIENT_COLORS = /^(?:from|via|to)-(?:purple|pink|blue|indigo|violet|fuchsia|cyan)-/;\n\nexport const gradientSoupRule: Rule = {\n id: \"ui-slop/gradient-soup\",\n name: \"Gradient soup\",\n category: \"ai-slop\",\n defaultSeverity: \"warn\",\n run(context) {\n const classes = allClasses(context).map(stripVariantPrefixes);\n const gradientUtilities = classes.filter(\n (className) => className.startsWith(\"bg-gradient-to-\") || /^(?:from|via|to)-/.test(className),\n );\n const glowUtilities = classes.filter(\n (className) => /^(?:blur-[23]xl|blur-\\[[^\\]]+\\]|opacity-[3-9]0)$/.test(className),\n );\n const hasAiGradientColors = classes.some((className) => AI_GRADIENT_COLORS.test(className));\n\n if (gradientUtilities.length < 3 && !(gradientUtilities.length >= 2 && glowUtilities.length > 0)) {\n return [];\n }\n\n return [\n createFinding(this, context, {\n ...firstUsageLine(context, (className) =>\n stripVariantPrefixes(className).startsWith(\"bg-gradient-to-\"),\n ),\n message: hasAiGradientColors && glowUtilities.length > 0\n ? \"Gradient-heavy styling with glow utilities detected. This is a common AI-generated SaaS UI pattern.\"\n : \"Gradient-heavy styling detected. This is a common AI-generated SaaS UI pattern.\",\n confidence: hasAiGradientColors ? \"high\" : \"medium\",\n suggestion: \"Use gradients sparingly and tie color choices back to the product visual system.\",\n }),\n ];\n },\n};\n","import type { Rule } from \"../core/types\";\nimport { stripVariantPrefixes } from \"../utils/tailwind\";\nimport { allClasses, createFinding, firstUsageLine } from \"./helpers\";\n\nexport const inconsistentIconSizeRule: Rule = {\n id: \"ui-slop/inconsistent-icon-size\",\n name: \"Inconsistent icon size\",\n category: \"components\",\n defaultSeverity: \"warn\",\n run(context) {\n const classes = allClasses(context).map(stripVariantPrefixes);\n const directSizes = classes\n .map((className) => /^size-(\\d+)$/.exec(className)?.[1])\n .filter((value): value is string => Boolean(value));\n const heightSizes = classes\n .map((className) => /^h-(\\d+)$/.exec(className)?.[1])\n .filter((value): value is string => Boolean(value));\n const widthSizes = classes\n .map((className) => /^w-(\\d+)$/.exec(className)?.[1])\n .filter((value): value is string => Boolean(value));\n const pairedSizes = heightSizes.filter((size) => widthSizes.includes(size));\n const uniqueSizes = [...new Set([...directSizes, ...pairedSizes])];\n\n if (uniqueSizes.length < 3) return [];\n\n return [\n createFinding(this, context, {\n ...firstUsageLine(context, (className) => /^(?:size|h|w)-\\d+$/.test(stripVariantPrefixes(className))),\n message: `Inconsistent icon sizing detected (${uniqueSizes.map((size) => `${size}`).join(\", \")}). Use a smaller set of icon sizes for better rhythm.`,\n confidence: uniqueSizes.length >= 4 ? \"high\" : \"medium\",\n suggestion: \"Standardize icon sizes by role, such as 16px for dense controls and 20px for primary actions.\",\n }),\n ];\n },\n};\n","import type { Rule } from \"../core/types\";\nimport { isMutedTextClass } from \"../utils/tailwind\";\nimport { allClasses, createFinding, firstUsageLine } from \"./helpers\";\n\nexport const mutedEverythingRule: Rule = {\n id: \"ui-slop/muted-everything\",\n name: \"Muted everything\",\n category: \"typography\",\n defaultSeverity: \"warn\",\n run(context) {\n const mutedTextClasses = allClasses(context).filter(isMutedTextClass);\n const textBearingElements = context.jsx.filter((element) => element.text.length > 0);\n const mutedRatio = textBearingElements.length > 0 ? mutedTextClasses.length / textBearingElements.length : 0;\n\n if (mutedTextClasses.length < 4 && !(mutedTextClasses.length >= 3 && mutedRatio >= 0.5)) {\n return [];\n }\n\n return [\n createFinding(this, context, {\n ...firstUsageLine(context, isMutedTextClass),\n message: `Muted text is overused (${mutedTextClasses.length} muted text utilities). Primary explanatory copy usually needs stronger contrast.`,\n confidence: mutedRatio >= 0.5 ? \"high\" : \"medium\",\n suggestion: \"Reserve muted text for secondary metadata, helper copy, and de-emphasized labels.\",\n }),\n ];\n },\n};\n","import type { Rule } from \"../core/types\";\nimport { stripVariantPrefixes } from \"../utils/tailwind\";\nimport { allClasses, createFinding, firstUsageLine } from \"./helpers\";\n\nexport const randomRadiusRule: Rule = {\n id: \"ui-slop/random-radius\",\n name: \"Random radius\",\n category: \"design-system\",\n defaultSeverity: \"warn\",\n run(context) {\n const radiusClasses = [\n ...new Set(\n allClasses(context)\n .map(stripVariantPrefixes)\n .filter((className) =>\n /^rounded(?:$|-(?:none|sm|md|lg|xl|2xl|3xl|full|\\[[^\\]]+\\]|[trbl][trbl]?-.+))/.test(\n className,\n ),\n ),\n ),\n ];\n\n if (radiusClasses.length <= 4) return [];\n\n return [\n createFinding(this, context, {\n ...firstUsageLine(context, (className) => stripVariantPrefixes(className).startsWith(\"rounded\")),\n message: `Too many border-radius variants found (${radiusClasses.length}). This creates inconsistent UI rhythm.`,\n confidence: radiusClasses.length >= 7 ? \"high\" : \"medium\",\n suggestion: \"Standardize around a small radius scale for repeated surfaces and controls.\",\n }),\n ];\n },\n};\n","import type { Rule } from \"../core/types\";\nimport { stripVariantPrefixes } from \"../utils/tailwind\";\nimport { allClasses, createFinding, firstUsageLine } from \"./helpers\";\n\nexport const randomShadowRule: Rule = {\n id: \"ui-slop/random-shadow\",\n name: \"Random shadow\",\n category: \"design-system\",\n defaultSeverity: \"warn\",\n run(context) {\n const shadowClasses = allClasses(context)\n .map(stripVariantPrefixes)\n .filter((className) => /^shadow(?:$|-(?:sm|md|lg|xl|2xl|inner|none|\\[[^\\]]+\\]))$/.test(className));\n const uniqueShadowClasses = [...new Set(shadowClasses)];\n const heavyShadowCount = shadowClasses.filter((className) => [\"shadow-xl\", \"shadow-2xl\"].includes(className)).length;\n\n if (uniqueShadowClasses.length <= 3 && heavyShadowCount < 2) return [];\n\n return [\n createFinding(this, context, {\n ...firstUsageLine(context, (className) => stripVariantPrefixes(className).startsWith(\"shadow\")),\n message: \"Inconsistent or heavy shadow usage detected.\",\n confidence: uniqueShadowClasses.length >= 5 || heavyShadowCount >= 3 ? \"high\" : \"medium\",\n suggestion: \"Use a smaller shadow scale and avoid heavy shadows on repeated surfaces.\",\n }),\n ];\n },\n};\n","export function getLineAndColumn(source: string, index: number): { line: number; column: number } {\n let line = 1;\n let column = 1;\n\n for (let i = 0; i < index; i += 1) {\n if (source.charCodeAt(i) === 10) {\n line += 1;\n column = 1;\n } else {\n column += 1;\n }\n }\n\n return { line, column };\n}\n\nexport function firstRegexLocation(\n source: string,\n pattern: RegExp,\n): { line: number; column: number } | undefined {\n const flags = pattern.flags.replaceAll(\"g\", \"\").replaceAll(\"y\", \"\");\n const regex = new RegExp(pattern.source, flags);\n const match = regex.exec(source);\n if (!match || match.index === undefined) return undefined;\n return getLineAndColumn(source, match.index);\n}\n","import type { Rule } from \"../core/types\";\nimport { firstRegexLocation } from \"../utils/source-location\";\nimport { createFinding } from \"./helpers\";\n\nconst HEX_COLOR_PATTERN = /#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\\b/g;\n\nexport const rawHexColorsRule: Rule = {\n id: \"ui-slop/raw-hex-colors\",\n name: \"Raw hex colors\",\n category: \"design-system\",\n defaultSeverity: \"error\",\n run(context) {\n HEX_COLOR_PATTERN.lastIndex = 0;\n const matches = [...context.source.matchAll(HEX_COLOR_PATTERN)];\n if (matches.length === 0) return [];\n\n return [\n createFinding(this, context, {\n ...firstRegexLocation(context.source, HEX_COLOR_PATTERN),\n message:\n matches.length === 1\n ? \"Raw hex color found. Prefer semantic tokens or Tailwind theme colors.\"\n : `Found ${matches.length} raw hex colors. Prefer semantic tokens or Tailwind theme colors.`,\n confidence: \"high\",\n suggestion: \"Move color choices into semantic tokens such as bg-card, text-foreground, or primary.\",\n }),\n ];\n },\n};\n","import type { Rule } from \"../core/types\";\nimport { stripVariantPrefixes } from \"../utils/tailwind\";\nimport { createFinding } from \"./helpers\";\n\nexport const shadcnDefaultLookRule: Rule = {\n id: \"ui-slop/shadcn-default-look\",\n name: \"shadcn default look\",\n category: \"components\",\n defaultSeverity: \"warn\",\n run(context) {\n const defaultishElements = context.jsx.filter((element) => {\n const classes = new Set(element.classes.map(stripVariantPrefixes));\n let score = 0;\n if (classes.has(\"rounded-lg\")) score += 1;\n if (classes.has(\"border\")) score += 1;\n if (classes.has(\"bg-card\")) score += 1;\n if (classes.has(\"text-card-foreground\")) score += 1;\n if (classes.has(\"shadow-sm\")) score += 1;\n if (classes.has(\"text-muted-foreground\")) score += 1;\n if (classes.has(\"space-y-1.5\")) score += 1;\n return score >= 3;\n });\n\n const mutedDefaults = context.classNames.filter((usage) =>\n usage.classes.map(stripVariantPrefixes).includes(\"text-muted-foreground\"),\n );\n\n if (defaultishElements.length < 2 || mutedDefaults.length < 2) return [];\n\n const first = defaultishElements[0];\n\n return [\n createFinding(this, context, {\n line: first?.line,\n column: first?.column,\n message:\n \"UI appears close to default shadcn styling. Consider applying product-specific visual language.\",\n confidence: defaultishElements.length >= 4 ? \"high\" : \"medium\",\n suggestion: \"Keep the component behavior, but tune spacing, type, density, and surface treatments.\",\n }),\n ];\n },\n};\n","import type { Rule } from \"../core/types\";\nimport { isPaletteColorClass } from \"../utils/tailwind\";\nimport { allClasses, createFinding, firstUsageLine } from \"./helpers\";\n\nexport const tokenBypassRule: Rule = {\n id: \"ui-slop/token-bypass\",\n name: \"Token bypass\",\n category: \"design-system\",\n defaultSeverity: \"warn\",\n run(context) {\n const directPaletteColors = allClasses(context).filter(isPaletteColorClass);\n const uniquePaletteColors = [...new Set(directPaletteColors)];\n\n if (uniquePaletteColors.length < 5) return [];\n\n return [\n createFinding(this, context, {\n ...firstUsageLine(context, isPaletteColorClass),\n message: `Tailwind palette colors are used directly ${uniquePaletteColors.length} times. Prefer semantic design tokens like bg-card, text-muted-foreground, border-border, primary, or secondary.`,\n confidence: uniquePaletteColors.length >= 9 ? \"high\" : \"medium\",\n suggestion: \"Map repeated palette choices to semantic tokens in your theme.\",\n }),\n ];\n },\n};\n","import type { Rule } from \"../core/types\";\nimport { arbitraryTailwindValuesRule } from \"./arbitrary-tailwind-values\";\nimport { cardLasagnaRule } from \"./card-lasagna\";\nimport { genericAiHeroRule } from \"./generic-ai-hero\";\nimport { gradientSoupRule } from \"./gradient-soup\";\nimport { inconsistentIconSizeRule } from \"./inconsistent-icon-size\";\nimport { mutedEverythingRule } from \"./muted-everything\";\nimport { randomRadiusRule } from \"./random-radius\";\nimport { randomShadowRule } from \"./random-shadow\";\nimport { rawHexColorsRule } from \"./raw-hex-colors\";\nimport { shadcnDefaultLookRule } from \"./shadcn-default-look\";\nimport { tokenBypassRule } from \"./token-bypass\";\n\nexport const allRules: Rule[] = [\n rawHexColorsRule,\n arbitraryTailwindValuesRule,\n tokenBypassRule,\n gradientSoupRule,\n cardLasagnaRule,\n mutedEverythingRule,\n randomRadiusRule,\n randomShadowRule,\n shadcnDefaultLookRule,\n genericAiHeroRule,\n inconsistentIconSizeRule,\n];\n","import { readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { loadConfig } from \"../config/load-config\";\nimport { calculateScore, getScoreLabel } from \"../core/score\";\nimport { runRules } from \"../core/rule-engine\";\nimport type { ProjectContext, ScanResult } from \"../core/types\";\nimport { TOOL_NAME, TOOL_VERSION } from \"../core/types\";\nimport { allRules } from \"../rules\";\nimport { relativePosixPath } from \"../utils/paths\";\nimport { extractClassNameUsages } from \"./extract-classnames\";\nimport { extractJsxUsages } from \"./extract-jsx\";\nimport { discoverFiles } from \"./file-discovery\";\nimport { parseFile } from \"./parse-file\";\n\nexport interface ScanProjectOptions {\n cwd?: string;\n configPath?: string;\n}\n\nexport async function scanProject(\n targetPath = \".\",\n options: ScanProjectOptions = {},\n): Promise<ScanResult> {\n const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();\n const root = path.resolve(cwd, targetPath);\n const loaded = await loadConfig({\n cwd: root,\n configPath: options.configPath\n ? path.isAbsolute(options.configPath)\n ? options.configPath\n : path.resolve(cwd, options.configPath)\n : undefined,\n });\n const files = await discoverFiles(root, loaded.config);\n const project: ProjectContext = {\n root,\n config: loaded.config,\n files: files.map((file) => relativePosixPath(root, file)),\n };\n const findings = [];\n\n for (const absolutePath of files) {\n const source = await readFile(absolutePath, \"utf8\");\n const filePath = relativePosixPath(root, absolutePath);\n const parsed = parseFile(source, filePath);\n const classNames = parsed.ast ? extractClassNameUsages(parsed.ast, filePath) : [];\n const jsx = parsed.ast ? extractJsxUsages(parsed.ast, filePath) : [];\n\n findings.push(\n ...runRules(\n {\n filePath,\n absolutePath,\n source,\n classNames,\n jsx,\n project,\n },\n allRules,\n ),\n );\n }\n\n const score = calculateScore(findings);\n\n return {\n tool: TOOL_NAME,\n version: TOOL_VERSION,\n root,\n score,\n label: getScoreLabel(score),\n filesScanned: files.length,\n findingCount: findings.length,\n findings,\n };\n}\n","import type { Finding } from \"./types\";\n\nexport function calculateScore(findings: Finding[]): number {\n const errorCount = findings.filter((finding) => finding.severity === \"error\").length;\n const warnCount = findings.filter((finding) => finding.severity === \"warn\").length;\n const lowConfidenceCount = findings.filter((finding) => finding.confidence === \"low\").length;\n\n return Math.max(0, 100 - errorCount * 8 - warnCount * 3 - lowConfidenceCount);\n}\n\nexport function getScoreLabel(score: number): string {\n if (score >= 90) return \"Clean\";\n if (score >= 75) return \"Minor slop\";\n if (score >= 60) return \"Noticeable slop\";\n if (score >= 40) return \"Heavy slop\";\n return \"Slop emergency\";\n}\n","import type { Finding, Rule, RuleContext } from \"./types\";\n\nexport function runRules(context: RuleContext, rules: Rule[]): Finding[] {\n const findings: Finding[] = [];\n\n for (const rule of rules) {\n const configuredSeverity = context.project.config.rules[rule.id] ?? rule.defaultSeverity;\n if (configuredSeverity === \"off\") {\n continue;\n }\n\n for (const finding of rule.run(context)) {\n findings.push({\n ...finding,\n severity: configuredSeverity,\n });\n }\n }\n\n return findings.sort((a, b) => {\n const severityDelta = severityRank(b.severity) - severityRank(a.severity);\n if (severityDelta !== 0) return severityDelta;\n return `${a.filePath}:${a.line ?? 0}:${a.ruleId}`.localeCompare(\n `${b.filePath}:${b.line ?? 0}:${b.ruleId}`,\n );\n });\n}\n\nfunction severityRank(severity: Finding[\"severity\"]): number {\n return severity === \"error\" ? 2 : 1;\n}\n","import type { Severity } from \"./severities\";\n\nexport const TOOL_NAME = \"unslop-ui\";\nexport const TOOL_VERSION = \"0.1.0\";\n\nexport type Category =\n | \"design-system\"\n | \"ai-slop\"\n | \"layout\"\n | \"typography\"\n | \"components\";\n\nexport type Confidence = \"low\" | \"medium\" | \"high\";\n\nexport interface Finding {\n ruleId: string;\n severity: Exclude<Severity, \"off\">;\n message: string;\n filePath: string;\n line?: number;\n column?: number;\n category: Category;\n confidence: Confidence;\n suggestion?: string;\n}\n\nexport interface ClassNameUsage {\n value: string;\n classes: string[];\n filePath: string;\n line?: number;\n column?: number;\n elementName?: string;\n}\n\nexport interface JsxUsage {\n elementName: string;\n classes: string[];\n className?: string;\n filePath: string;\n line?: number;\n column?: number;\n depth: number;\n text: string;\n}\n\nexport interface StackConfig {\n react: boolean;\n tailwind: boolean;\n shadcn: boolean;\n}\n\nexport interface UnslopConfig {\n include?: string[];\n ignore?: string[];\n stack?: Partial<StackConfig>;\n rules?: Record<string, Severity>;\n}\n\nexport interface ResolvedUnslopConfig {\n include: string[];\n ignore: string[];\n stack: StackConfig;\n rules: Record<string, Severity>;\n}\n\nexport interface ProjectContext {\n root: string;\n config: ResolvedUnslopConfig;\n files: string[];\n}\n\nexport interface RuleContext {\n filePath: string;\n absolutePath: string;\n source: string;\n classNames: ClassNameUsage[];\n jsx: JsxUsage[];\n project: ProjectContext;\n}\n\nexport interface Rule {\n id: string;\n name: string;\n category: Category;\n defaultSeverity: Severity;\n run(context: RuleContext): Finding[];\n}\n\nexport interface ScanResult {\n tool: typeof TOOL_NAME;\n version: typeof TOOL_VERSION;\n root: string;\n score: number;\n label: string;\n filesScanned: number;\n findingCount: number;\n findings: Finding[];\n}\n","import path from \"node:path\";\n\nexport function toPosixPath(filePath: string): string {\n return filePath.split(path.sep).join(\"/\");\n}\n\nexport function relativePosixPath(root: string, filePath: string): string {\n return toPosixPath(path.relative(root, filePath));\n}\n","import type { File } from \"@babel/types\";\nimport type { ClassNameUsage } from \"../core/types\";\nimport { splitClasses } from \"../utils/tailwind\";\n\nconst CLASS_HELPERS = new Set([\"cn\", \"clsx\", \"classnames\", \"cva\"]);\n\nexport function extractClassNameUsages(ast: File, filePath: string): ClassNameUsage[] {\n const usages: ClassNameUsage[] = [];\n\n walk(ast, (node) => {\n if (!isRecord(node) || node.type !== \"JSXOpeningElement\") return;\n\n const elementName = getJsxName(node.name);\n const attributes = Array.isArray(node.attributes) ? node.attributes : [];\n\n for (const attribute of attributes) {\n if (!isRecord(attribute) || attribute.type !== \"JSXAttribute\") continue;\n if (!isRecord(attribute.name) || attribute.name.name !== \"className\") continue;\n\n const values = extractClassNameValues(attribute.value);\n for (const value of values) {\n const classes = splitClasses(value);\n if (classes.length === 0) continue;\n\n usages.push({\n value,\n classes,\n filePath,\n line: getLine(attribute),\n column: getColumn(attribute),\n elementName,\n });\n }\n }\n });\n\n return usages;\n}\n\nexport function extractClassNameValues(node: unknown): string[] {\n if (!isRecord(node)) return [];\n\n switch (node.type) {\n case \"StringLiteral\":\n return typeof node.value === \"string\" ? [node.value] : [];\n case \"JSXExpressionContainer\":\n return extractClassNameValues(node.expression);\n case \"TemplateLiteral\":\n return [\n ...getTemplateQuasis(node),\n ...getArray(node.expressions).flatMap((expression) => extractClassNameValues(expression)),\n ];\n case \"CallExpression\": {\n const calleeName = getCalleeName(node.callee);\n if (!calleeName || !CLASS_HELPERS.has(calleeName)) return [];\n return getArray(node.arguments).flatMap((argument) => extractClassNameValues(argument));\n }\n case \"ConditionalExpression\":\n return [\n ...extractClassNameValues(node.consequent),\n ...extractClassNameValues(node.alternate),\n ];\n case \"LogicalExpression\":\n return extractClassNameValues(node.left).concat(extractClassNameValues(node.right));\n case \"ArrayExpression\":\n return getArray(node.elements).flatMap((element) => extractClassNameValues(element));\n case \"ObjectExpression\":\n return getArray(node.properties).flatMap((property) => {\n if (!isRecord(property) || property.type !== \"ObjectProperty\") return [];\n return extractObjectKey(property.key);\n });\n case \"BinaryExpression\":\n if (node.operator !== \"+\") return [];\n return extractClassNameValues(node.left).concat(extractClassNameValues(node.right));\n default:\n return [];\n }\n}\n\nfunction walk(node: unknown, visitor: (node: unknown) => void): void {\n if (!isRecord(node)) return;\n\n visitor(node);\n\n for (const [key, value] of Object.entries(node)) {\n if (shouldSkipKey(key)) continue;\n\n if (Array.isArray(value)) {\n for (const item of value) {\n if (isAstNode(item)) walk(item, visitor);\n }\n } else if (isAstNode(value)) {\n walk(value, visitor);\n }\n }\n}\n\nfunction extractObjectKey(node: unknown): string[] {\n if (!isRecord(node)) return [];\n if (node.type === \"StringLiteral\" && typeof node.value === \"string\") return [node.value];\n if (node.type === \"Identifier\" && typeof node.name === \"string\") return [node.name];\n return [];\n}\n\nfunction getTemplateQuasis(node: Record<string, unknown>): string[] {\n return getArray(node.quasis)\n .map((quasi) => {\n if (!isRecord(quasi) || !isRecord(quasi.value)) return \"\";\n return typeof quasi.value.cooked === \"string\" ? quasi.value.cooked : \"\";\n })\n .filter(Boolean);\n}\n\nfunction getCalleeName(node: unknown): string | undefined {\n if (!isRecord(node)) return undefined;\n if (node.type === \"Identifier\" && typeof node.name === \"string\") return node.name;\n if (node.type === \"MemberExpression\" && isRecord(node.property)) {\n return typeof node.property.name === \"string\" ? node.property.name : undefined;\n }\n return undefined;\n}\n\nfunction getJsxName(node: unknown): string | undefined {\n if (!isRecord(node)) return undefined;\n if (node.type === \"JSXIdentifier\" && typeof node.name === \"string\") return node.name;\n if (node.type === \"JSXMemberExpression\") {\n const objectName = getJsxName(node.object);\n const propertyName = getJsxName(node.property);\n return [objectName, propertyName].filter(Boolean).join(\".\");\n }\n return undefined;\n}\n\nfunction getLine(node: Record<string, unknown>): number | undefined {\n return isRecord(node.loc) && isRecord(node.loc.start) && typeof node.loc.start.line === \"number\"\n ? node.loc.start.line\n : undefined;\n}\n\nfunction getColumn(node: Record<string, unknown>): number | undefined {\n return isRecord(node.loc) && isRecord(node.loc.start) && typeof node.loc.start.column === \"number\"\n ? node.loc.start.column + 1\n : undefined;\n}\n\nfunction shouldSkipKey(key: string): boolean {\n return [\n \"loc\",\n \"start\",\n \"end\",\n \"extra\",\n \"leadingComments\",\n \"trailingComments\",\n \"innerComments\",\n ].includes(key);\n}\n\nfunction isAstNode(value: unknown): value is Record<string, unknown> {\n return isRecord(value) && typeof value.type === \"string\";\n}\n\nfunction getArray(value: unknown): unknown[] {\n return Array.isArray(value) ? value : [];\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n","import type { File } from \"@babel/types\";\nimport type { JsxUsage } from \"../core/types\";\nimport { splitClasses } from \"../utils/tailwind\";\nimport { extractClassNameValues } from \"./extract-classnames\";\n\nexport function extractJsxUsages(ast: File, filePath: string): JsxUsage[] {\n const usages: JsxUsage[] = [];\n walkJsx(ast, filePath, usages, 0);\n return usages;\n}\n\nfunction walkJsx(node: unknown, filePath: string, usages: JsxUsage[], depth: number): void {\n if (!isRecord(node)) return;\n\n if (node.type === \"JSXElement\") {\n const opening = isRecord(node.openingElement) ? node.openingElement : undefined;\n const className = opening ? getClassName(opening) : undefined;\n const classes = className ? splitClasses(className) : [];\n\n usages.push({\n elementName: opening ? getJsxName(opening.name) ?? \"unknown\" : \"unknown\",\n classes,\n className,\n filePath,\n line: opening ? getLine(opening) : undefined,\n column: opening ? getColumn(opening) : undefined,\n depth,\n text: collectText(node),\n });\n\n for (const child of getArray(node.children)) {\n walkJsx(child, filePath, usages, depth + 1);\n }\n\n return;\n }\n\n for (const [key, value] of Object.entries(node)) {\n if (shouldSkipKey(key)) continue;\n\n if (Array.isArray(value)) {\n for (const item of value) {\n if (isAstNode(item)) walkJsx(item, filePath, usages, depth);\n }\n } else if (isAstNode(value)) {\n walkJsx(value, filePath, usages, depth);\n }\n }\n}\n\nfunction getClassName(openingElement: Record<string, unknown>): string | undefined {\n const attributes = getArray(openingElement.attributes);\n\n for (const attribute of attributes) {\n if (!isRecord(attribute) || attribute.type !== \"JSXAttribute\") continue;\n if (!isRecord(attribute.name) || attribute.name.name !== \"className\") continue;\n return extractClassNameValues(attribute.value).join(\" \").trim() || undefined;\n }\n\n return undefined;\n}\n\nfunction collectText(node: unknown): string {\n if (!isRecord(node)) return \"\";\n\n if (node.type === \"JSXElement\") {\n return getArray(node.children)\n .map((child) => collectText(child))\n .join(\" \")\n .replace(/\\s+/g, \" \")\n .trim();\n }\n\n if (node.type === \"JSXExpressionContainer\") {\n return collectText(node.expression);\n }\n\n if (node.type === \"JSXText\" && typeof node.value === \"string\") {\n return node.value;\n }\n\n if (node.type === \"StringLiteral\" && typeof node.value === \"string\") {\n return node.value;\n }\n\n if (node.type === \"TemplateLiteral\") {\n return getArray(node.quasis)\n .map((quasi) => {\n if (!isRecord(quasi) || !isRecord(quasi.value)) return \"\";\n return typeof quasi.value.cooked === \"string\" ? quasi.value.cooked : \"\";\n })\n .join(\" \");\n }\n\n let text = \"\";\n for (const [key, value] of Object.entries(node)) {\n if (shouldSkipKey(key)) continue;\n\n if (Array.isArray(value)) {\n text += ` ${value.map((item) => collectText(item)).join(\" \")}`;\n } else if (isAstNode(value)) {\n text += ` ${collectText(value)}`;\n }\n }\n\n return text.replace(/\\s+/g, \" \").trim();\n}\n\nfunction getJsxName(node: unknown): string | undefined {\n if (!isRecord(node)) return undefined;\n if (node.type === \"JSXIdentifier\" && typeof node.name === \"string\") return node.name;\n if (node.type === \"JSXMemberExpression\") {\n const objectName = getJsxName(node.object);\n const propertyName = getJsxName(node.property);\n return [objectName, propertyName].filter(Boolean).join(\".\");\n }\n return undefined;\n}\n\nfunction getLine(node: Record<string, unknown>): number | undefined {\n return isRecord(node.loc) && isRecord(node.loc.start) && typeof node.loc.start.line === \"number\"\n ? node.loc.start.line\n : undefined;\n}\n\nfunction getColumn(node: Record<string, unknown>): number | undefined {\n return isRecord(node.loc) && isRecord(node.loc.start) && typeof node.loc.start.column === \"number\"\n ? node.loc.start.column + 1\n : undefined;\n}\n\nfunction shouldSkipKey(key: string): boolean {\n return [\n \"loc\",\n \"start\",\n \"end\",\n \"extra\",\n \"leadingComments\",\n \"trailingComments\",\n \"innerComments\",\n ].includes(key);\n}\n\nfunction isAstNode(value: unknown): value is Record<string, unknown> {\n return isRecord(value) && typeof value.type === \"string\";\n}\n\nfunction getArray(value: unknown): unknown[] {\n return Array.isArray(value) ? value : [];\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n","import fg from \"fast-glob\";\nimport type { ResolvedUnslopConfig } from \"../core/types\";\nimport { toPosixPath } from \"../utils/paths\";\n\nexport async function discoverFiles(root: string, config: ResolvedUnslopConfig): Promise<string[]> {\n const files = await fg(config.include, {\n cwd: root,\n absolute: true,\n onlyFiles: true,\n ignore: config.ignore,\n dot: false,\n });\n\n return files.sort((a, b) => toPosixPath(a).localeCompare(toPosixPath(b)));\n}\n","import { parse } from \"@babel/parser\";\nimport type { File } from \"@babel/types\";\n\nexport interface ParsedFile {\n ast: File | null;\n error?: Error;\n}\n\nexport function parseFile(source: string, filePath: string): ParsedFile {\n try {\n return {\n ast: parse(source, {\n sourceFilename: filePath,\n sourceType: \"unambiguous\",\n errorRecovery: true,\n plugins: [\"jsx\", \"typescript\", \"decorators-legacy\"],\n }),\n };\n } catch (error) {\n return {\n ast: null,\n error: error instanceof Error ? error : new Error(String(error)),\n };\n }\n}\n","import type { ScanResult } from \"../core/types\";\n\nexport function formatJsonReport(result: ScanResult): string {\n return `${JSON.stringify(\n {\n tool: result.tool,\n version: result.version,\n score: result.score,\n label: result.label,\n filesScanned: result.filesScanned,\n findingCount: result.findingCount,\n findings: result.findings.map((finding) => ({\n ruleId: finding.ruleId,\n severity: finding.severity,\n category: finding.category,\n confidence: finding.confidence,\n filePath: finding.filePath,\n line: finding.line,\n column: finding.column,\n message: finding.message,\n suggestion: finding.suggestion,\n })),\n },\n null,\n 2,\n )}\\n`;\n}\n","import pc from \"picocolors\";\nimport type { Category, Finding, ScanResult } from \"../core/types\";\n\nexport interface TerminalReportOptions {\n maxFindings?: number;\n quiet?: boolean;\n}\n\nconst CATEGORY_LABELS: Record<Category, string> = {\n \"design-system\": \"Design system\",\n \"ai-slop\": \"AI slop\",\n layout: \"Layout\",\n typography: \"Typography\",\n components: \"Components\",\n};\n\nexport function formatTerminalReport(\n result: ScanResult,\n options: TerminalReportOptions = {},\n): string {\n if (options.quiet) {\n return `Unslop UI: ${result.score}/100 - ${result.label}; ${result.findingCount} findings in ${result.filesScanned} files\\n`;\n }\n\n const maxFindings = options.maxFindings ?? 20;\n const lines: string[] = [\n pc.bold(\"Unslop UI\"),\n \"\",\n `Score: ${colorScore(result.score, `${result.score}/100`)} - ${result.label}`,\n `Files scanned: ${result.filesScanned}`,\n `Findings: ${result.findingCount}`,\n ];\n\n if (result.findings.length === 0) {\n lines.push(\"\", pc.green(\"No findings. The UI surface looks controlled.\"));\n return `${lines.join(\"\\n\")}\\n`;\n }\n\n const errors = result.findings.filter((finding) => finding.severity === \"error\");\n const warnings = result.findings.filter((finding) => finding.severity === \"warn\");\n\n appendFindingGroup(lines, \"High impact\", errors, maxFindings);\n appendFindingGroup(lines, \"Warnings\", warnings, Math.max(0, maxFindings - errors.length));\n\n const hiddenCount = Math.max(0, result.findings.length - maxFindings);\n if (hiddenCount > 0) {\n lines.push(\"\", pc.dim(`Showing ${maxFindings} of ${result.findings.length} findings. Re-run with --verbose to show all.`));\n }\n\n lines.push(\"\", pc.bold(\"Summary by category\"));\n for (const [category, count] of getCategoryCounts(result.findings)) {\n lines.push(` ${CATEGORY_LABELS[category]}: ${count}`);\n }\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nfunction appendFindingGroup(\n lines: string[],\n title: string,\n findings: Finding[],\n maxCount: number,\n): void {\n if (findings.length === 0 || maxCount <= 0) return;\n\n lines.push(\"\", pc.bold(title));\n for (const finding of findings.slice(0, maxCount)) {\n lines.push(` ${formatSeverity(finding.severity)} ${finding.ruleId}`);\n lines.push(` ${finding.filePath}${finding.line ? `:${finding.line}` : \"\"}`);\n lines.push(` ${finding.message}`);\n if (finding.suggestion) {\n lines.push(` ${pc.dim(finding.suggestion)}`);\n }\n lines.push(\"\");\n }\n\n if (lines[lines.length - 1] === \"\") {\n lines.pop();\n }\n}\n\nfunction formatSeverity(severity: Finding[\"severity\"]): string {\n return severity === \"error\" ? pc.red(\"error\") : pc.yellow(\"warn\");\n}\n\nfunction colorScore(score: number, value: string): string {\n if (score >= 90) return pc.green(value);\n if (score >= 75) return pc.cyan(value);\n if (score >= 60) return pc.yellow(value);\n return pc.red(value);\n}\n\nfunction getCategoryCounts(findings: Finding[]): Array<[Category, number]> {\n const counts = new Map<Category, number>();\n for (const finding of findings) {\n counts.set(finding.category, (counts.get(finding.category) ?? 0) + 1);\n }\n\n return [...counts.entries()].sort((a, b) => b[1] - a[1]);\n}\n"],"mappings":"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","names":["path","path","isRecord","getJsxName","getLine","getColumn","getArray","shouldSkipKey","isAstNode","path"]}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
formatJsonReport,
|
|
4
|
+
formatTerminalReport,
|
|
5
|
+
scanProject
|
|
6
|
+
} from "./chunk-34KLV5KL.js";
|
|
7
|
+
|
|
8
|
+
// src/cli.ts
|
|
9
|
+
import { pathToFileURL } from "url";
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
|
|
12
|
+
// src/core/severities.ts
|
|
13
|
+
function isFailingSeverity(severity, strict) {
|
|
14
|
+
return severity === "error" || strict && severity === "warn";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/cli.ts
|
|
18
|
+
async function runCli(args, io = {}) {
|
|
19
|
+
const stdout = io.stdout ?? process.stdout;
|
|
20
|
+
const stderr = io.stderr ?? process.stderr;
|
|
21
|
+
const normalizedArgs = args[0] === "--" ? args.slice(1) : args;
|
|
22
|
+
const program = new Command();
|
|
23
|
+
program.name("unslop-ui").description("Catch UI slop before it ships.").argument("[target]", "Project directory to scan", ".").option("--json", "Output structured JSON only").option("--config <path>", "Path to an unslop config file").option("--strict", "Treat warnings as build-failing").option("--quiet", "Print only a one-line summary").option("--verbose", "Show all findings in terminal output").exitOverride().configureOutput({
|
|
24
|
+
writeOut: (message) => stdout.write(message),
|
|
25
|
+
writeErr: (message) => stderr.write(message)
|
|
26
|
+
});
|
|
27
|
+
try {
|
|
28
|
+
await program.parseAsync(normalizedArgs, { from: "user" });
|
|
29
|
+
const options = program.opts();
|
|
30
|
+
const target = program.args[0] ?? ".";
|
|
31
|
+
const result = await scanProject(target, {
|
|
32
|
+
cwd: process.cwd(),
|
|
33
|
+
configPath: options.config
|
|
34
|
+
});
|
|
35
|
+
if (options.json) {
|
|
36
|
+
stdout.write(formatJsonReport(result));
|
|
37
|
+
} else {
|
|
38
|
+
stdout.write(
|
|
39
|
+
formatTerminalReport(result, {
|
|
40
|
+
quiet: options.quiet,
|
|
41
|
+
maxFindings: options.verbose ? Number.POSITIVE_INFINITY : 20
|
|
42
|
+
})
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return result.findings.some((finding) => isFailingSeverity(finding.severity, Boolean(options.strict))) ? 1 : 0;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error instanceof Error && "exitCode" in error) {
|
|
48
|
+
return Number(error.exitCode);
|
|
49
|
+
}
|
|
50
|
+
stderr.write(`Unslop UI failed: ${error instanceof Error ? error.message : String(error)}
|
|
51
|
+
`);
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
|
|
56
|
+
runCli(process.argv.slice(2)).then((exitCode) => {
|
|
57
|
+
process.exitCode = exitCode;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export {
|
|
61
|
+
runCli
|
|
62
|
+
};
|
|
63
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/core/severities.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { pathToFileURL } from \"node:url\";\nimport { Command } from \"commander\";\nimport { isFailingSeverity } from \"./core/severities\";\nimport { scanProject } from \"./scanner/scan-project\";\nimport { formatJsonReport } from \"./reporters/json\";\nimport { formatTerminalReport } from \"./reporters/terminal\";\n\nexport interface CliIo {\n stdout?: Pick<NodeJS.WriteStream, \"write\">;\n stderr?: Pick<NodeJS.WriteStream, \"write\">;\n}\n\nexport async function runCli(args: string[], io: CliIo = {}): Promise<number> {\n const stdout = io.stdout ?? process.stdout;\n const stderr = io.stderr ?? process.stderr;\n const normalizedArgs = args[0] === \"--\" ? args.slice(1) : args;\n const program = new Command();\n\n program\n .name(\"unslop-ui\")\n .description(\"Catch UI slop before it ships.\")\n .argument(\"[target]\", \"Project directory to scan\", \".\")\n .option(\"--json\", \"Output structured JSON only\")\n .option(\"--config <path>\", \"Path to an unslop config file\")\n .option(\"--strict\", \"Treat warnings as build-failing\")\n .option(\"--quiet\", \"Print only a one-line summary\")\n .option(\"--verbose\", \"Show all findings in terminal output\")\n .exitOverride()\n .configureOutput({\n writeOut: (message) => stdout.write(message),\n writeErr: (message) => stderr.write(message),\n });\n\n try {\n await program.parseAsync(normalizedArgs, { from: \"user\" });\n const options = program.opts<{\n json?: boolean;\n config?: string;\n strict?: boolean;\n quiet?: boolean;\n verbose?: boolean;\n }>();\n const target = program.args[0] ?? \".\";\n const result = await scanProject(target, {\n cwd: process.cwd(),\n configPath: options.config,\n });\n\n if (options.json) {\n stdout.write(formatJsonReport(result));\n } else {\n stdout.write(\n formatTerminalReport(result, {\n quiet: options.quiet,\n maxFindings: options.verbose ? Number.POSITIVE_INFINITY : 20,\n }),\n );\n }\n\n return result.findings.some((finding) => isFailingSeverity(finding.severity, Boolean(options.strict)))\n ? 1\n : 0;\n } catch (error) {\n if (error instanceof Error && \"exitCode\" in error) {\n return Number((error as Error & { exitCode: number }).exitCode);\n }\n\n stderr.write(`Unslop UI failed: ${error instanceof Error ? error.message : String(error)}\\n`);\n return 1;\n }\n}\n\nif (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {\n runCli(process.argv.slice(2)).then((exitCode) => {\n process.exitCode = exitCode;\n });\n}\n","export const severities = [\"off\", \"warn\", \"error\"] as const;\n\nexport type Severity = (typeof severities)[number];\n\nexport function isFailingSeverity(severity: Severity, strict: boolean): boolean {\n return severity === \"error\" || (strict && severity === \"warn\");\n}\n"],"mappings":";;;;;;;;AACA,SAAS,qBAAqB;AAC9B,SAAS,eAAe;;;ACEjB,SAAS,kBAAkB,UAAoB,QAA0B;AAC9E,SAAO,aAAa,WAAY,UAAU,aAAa;AACzD;;;ADOA,eAAsB,OAAO,MAAgB,KAAY,CAAC,GAAoB;AAC5E,QAAM,SAAS,GAAG,UAAU,QAAQ;AACpC,QAAM,SAAS,GAAG,UAAU,QAAQ;AACpC,QAAM,iBAAiB,KAAK,CAAC,MAAM,OAAO,KAAK,MAAM,CAAC,IAAI;AAC1D,QAAM,UAAU,IAAI,QAAQ;AAE5B,UACG,KAAK,WAAW,EAChB,YAAY,gCAAgC,EAC5C,SAAS,YAAY,6BAA6B,GAAG,EACrD,OAAO,UAAU,6BAA6B,EAC9C,OAAO,mBAAmB,+BAA+B,EACzD,OAAO,YAAY,iCAAiC,EACpD,OAAO,WAAW,+BAA+B,EACjD,OAAO,aAAa,sCAAsC,EAC1D,aAAa,EACb,gBAAgB;AAAA,IACf,UAAU,CAAC,YAAY,OAAO,MAAM,OAAO;AAAA,IAC3C,UAAU,CAAC,YAAY,OAAO,MAAM,OAAO;AAAA,EAC7C,CAAC;AAEH,MAAI;AACF,UAAM,QAAQ,WAAW,gBAAgB,EAAE,MAAM,OAAO,CAAC;AACzD,UAAM,UAAU,QAAQ,KAMrB;AACH,UAAM,SAAS,QAAQ,KAAK,CAAC,KAAK;AAClC,UAAM,SAAS,MAAM,YAAY,QAAQ;AAAA,MACvC,KAAK,QAAQ,IAAI;AAAA,MACjB,YAAY,QAAQ;AAAA,IACtB,CAAC;AAED,QAAI,QAAQ,MAAM;AAChB,aAAO,MAAM,iBAAiB,MAAM,CAAC;AAAA,IACvC,OAAO;AACL,aAAO;AAAA,QACL,qBAAqB,QAAQ;AAAA,UAC3B,OAAO,QAAQ;AAAA,UACf,aAAa,QAAQ,UAAU,OAAO,oBAAoB;AAAA,QAC5D,CAAC;AAAA,MACH;AAAA,IACF;AAEA,WAAO,OAAO,SAAS,KAAK,CAAC,YAAY,kBAAkB,QAAQ,UAAU,QAAQ,QAAQ,MAAM,CAAC,CAAC,IACjG,IACA;AAAA,EACN,SAAS,OAAO;AACd,QAAI,iBAAiB,SAAS,cAAc,OAAO;AACjD,aAAO,OAAQ,MAAuC,QAAQ;AAAA,IAChE;AAEA,WAAO,MAAM,qBAAqB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,CAAI;AAC5F,WAAO;AAAA,EACT;AACF;AAEA,IAAI,QAAQ,KAAK,CAAC,KAAK,cAAc,QAAQ,KAAK,CAAC,CAAC,EAAE,SAAS,YAAY,KAAK;AAC9E,SAAO,QAAQ,KAAK,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,aAAa;AAC/C,YAAQ,WAAW;AAAA,EACrB,CAAC;AACH;","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
declare const severities: readonly ["off", "warn", "error"];
|
|
2
|
+
type Severity = (typeof severities)[number];
|
|
3
|
+
|
|
4
|
+
declare const TOOL_NAME = "unslop-ui";
|
|
5
|
+
declare const TOOL_VERSION = "0.1.0";
|
|
6
|
+
type Category = "design-system" | "ai-slop" | "layout" | "typography" | "components";
|
|
7
|
+
type Confidence = "low" | "medium" | "high";
|
|
8
|
+
interface Finding {
|
|
9
|
+
ruleId: string;
|
|
10
|
+
severity: Exclude<Severity, "off">;
|
|
11
|
+
message: string;
|
|
12
|
+
filePath: string;
|
|
13
|
+
line?: number;
|
|
14
|
+
column?: number;
|
|
15
|
+
category: Category;
|
|
16
|
+
confidence: Confidence;
|
|
17
|
+
suggestion?: string;
|
|
18
|
+
}
|
|
19
|
+
interface ClassNameUsage {
|
|
20
|
+
value: string;
|
|
21
|
+
classes: string[];
|
|
22
|
+
filePath: string;
|
|
23
|
+
line?: number;
|
|
24
|
+
column?: number;
|
|
25
|
+
elementName?: string;
|
|
26
|
+
}
|
|
27
|
+
interface JsxUsage {
|
|
28
|
+
elementName: string;
|
|
29
|
+
classes: string[];
|
|
30
|
+
className?: string;
|
|
31
|
+
filePath: string;
|
|
32
|
+
line?: number;
|
|
33
|
+
column?: number;
|
|
34
|
+
depth: number;
|
|
35
|
+
text: string;
|
|
36
|
+
}
|
|
37
|
+
interface StackConfig {
|
|
38
|
+
react: boolean;
|
|
39
|
+
tailwind: boolean;
|
|
40
|
+
shadcn: boolean;
|
|
41
|
+
}
|
|
42
|
+
interface UnslopConfig {
|
|
43
|
+
include?: string[];
|
|
44
|
+
ignore?: string[];
|
|
45
|
+
stack?: Partial<StackConfig>;
|
|
46
|
+
rules?: Record<string, Severity>;
|
|
47
|
+
}
|
|
48
|
+
interface ResolvedUnslopConfig {
|
|
49
|
+
include: string[];
|
|
50
|
+
ignore: string[];
|
|
51
|
+
stack: StackConfig;
|
|
52
|
+
rules: Record<string, Severity>;
|
|
53
|
+
}
|
|
54
|
+
interface ProjectContext {
|
|
55
|
+
root: string;
|
|
56
|
+
config: ResolvedUnslopConfig;
|
|
57
|
+
files: string[];
|
|
58
|
+
}
|
|
59
|
+
interface RuleContext {
|
|
60
|
+
filePath: string;
|
|
61
|
+
absolutePath: string;
|
|
62
|
+
source: string;
|
|
63
|
+
classNames: ClassNameUsage[];
|
|
64
|
+
jsx: JsxUsage[];
|
|
65
|
+
project: ProjectContext;
|
|
66
|
+
}
|
|
67
|
+
interface Rule {
|
|
68
|
+
id: string;
|
|
69
|
+
name: string;
|
|
70
|
+
category: Category;
|
|
71
|
+
defaultSeverity: Severity;
|
|
72
|
+
run(context: RuleContext): Finding[];
|
|
73
|
+
}
|
|
74
|
+
interface ScanResult {
|
|
75
|
+
tool: typeof TOOL_NAME;
|
|
76
|
+
version: typeof TOOL_VERSION;
|
|
77
|
+
root: string;
|
|
78
|
+
score: number;
|
|
79
|
+
label: string;
|
|
80
|
+
filesScanned: number;
|
|
81
|
+
findingCount: number;
|
|
82
|
+
findings: Finding[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface LoadConfigOptions {
|
|
86
|
+
cwd: string;
|
|
87
|
+
configPath?: string;
|
|
88
|
+
}
|
|
89
|
+
interface LoadedConfig {
|
|
90
|
+
config: ResolvedUnslopConfig;
|
|
91
|
+
configPath?: string;
|
|
92
|
+
}
|
|
93
|
+
declare function loadConfig(options: LoadConfigOptions): Promise<LoadedConfig>;
|
|
94
|
+
|
|
95
|
+
interface ScanProjectOptions {
|
|
96
|
+
cwd?: string;
|
|
97
|
+
configPath?: string;
|
|
98
|
+
}
|
|
99
|
+
declare function scanProject(targetPath?: string, options?: ScanProjectOptions): Promise<ScanResult>;
|
|
100
|
+
|
|
101
|
+
declare const allRules: Rule[];
|
|
102
|
+
|
|
103
|
+
declare function formatJsonReport(result: ScanResult): string;
|
|
104
|
+
|
|
105
|
+
interface TerminalReportOptions {
|
|
106
|
+
maxFindings?: number;
|
|
107
|
+
quiet?: boolean;
|
|
108
|
+
}
|
|
109
|
+
declare function formatTerminalReport(result: ScanResult, options?: TerminalReportOptions): string;
|
|
110
|
+
|
|
111
|
+
declare function defineConfig<T extends UnslopConfig>(config: T): T;
|
|
112
|
+
|
|
113
|
+
export { type ClassNameUsage, type Finding, type JsxUsage, type ResolvedUnslopConfig, type Rule, type RuleContext, type ScanResult, type UnslopConfig, allRules, defineConfig, formatJsonReport, formatTerminalReport, loadConfig, scanProject };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
allRules,
|
|
3
|
+
formatJsonReport,
|
|
4
|
+
formatTerminalReport,
|
|
5
|
+
loadConfig,
|
|
6
|
+
scanProject
|
|
7
|
+
} from "./chunk-34KLV5KL.js";
|
|
8
|
+
|
|
9
|
+
// src/index.ts
|
|
10
|
+
function defineConfig(config) {
|
|
11
|
+
return config;
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
allRules,
|
|
15
|
+
defineConfig,
|
|
16
|
+
formatJsonReport,
|
|
17
|
+
formatTerminalReport,
|
|
18
|
+
loadConfig,
|
|
19
|
+
scanProject
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export { loadConfig } from \"./config/load-config\";\nexport { scanProject } from \"./scanner/scan-project\";\nexport { allRules } from \"./rules\";\nexport { formatJsonReport } from \"./reporters/json\";\nexport { formatTerminalReport } from \"./reporters/terminal\";\nexport type {\n ClassNameUsage,\n Finding,\n JsxUsage,\n ResolvedUnslopConfig,\n Rule,\n RuleContext,\n ScanResult,\n UnslopConfig,\n} from \"./core/types\";\n\nexport function defineConfig<T extends import(\"./core/types\").UnslopConfig>(config: T): T {\n return config;\n}\n"],"mappings":";;;;;;;;;AAgBO,SAAS,aAA4D,QAAc;AACxF,SAAO;AACT;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "unslop-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Catch UI slop before it ships.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"homepage": "https://github.com/LachyFS/unslop-ui#readme",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/LachyFS/unslop-ui/issues"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/LachyFS/unslop-ui.git"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"react",
|
|
16
|
+
"tailwind",
|
|
17
|
+
"shadcn",
|
|
18
|
+
"cli",
|
|
19
|
+
"ui",
|
|
20
|
+
"design-system"
|
|
21
|
+
],
|
|
22
|
+
"bin": {
|
|
23
|
+
"unslop-ui": "dist/cli.js"
|
|
24
|
+
},
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"dev": "tsx src/cli.ts",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"unslop-ui": "tsx src/cli.ts"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@babel/parser": "^7.28.5",
|
|
43
|
+
"@babel/types": "^7.28.5",
|
|
44
|
+
"commander": "^14.0.2",
|
|
45
|
+
"fast-glob": "^3.3.3",
|
|
46
|
+
"jiti": "^2.6.1",
|
|
47
|
+
"picocolors": "^1.1.1",
|
|
48
|
+
"zod": "^4.2.1"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^24.10.3",
|
|
52
|
+
"tsup": "^8.5.1",
|
|
53
|
+
"tsx": "^4.21.0",
|
|
54
|
+
"typescript": "^5.9.3",
|
|
55
|
+
"vitest": "^4.0.15"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=20"
|
|
59
|
+
}
|
|
60
|
+
}
|