Harden CLI detection for framework files, multi-file projects, and dev servers

Tier 1: Add Vue/Svelte <style> block extraction and CSS-in-JS template literal
detection (styled-components, emotion) so anti-patterns inside framework-specific
syntax are caught. Enable multi-line context for CSS files so cross-line patterns
like gradient-text are detected.

Tier 2: Build a lightweight import graph when scanning directories. Findings are
annotated with importedBy context (e.g. "imported by App.tsx") in both human and
JSON output.

Tier 3: Detect framework config files (Next.js, Vite, SvelteKit, Nuxt, Astro,
Angular, Remix), probe the dev server port with HTTP fingerprinting to distinguish
the expected framework from unrelated services, and suggest URL-based scanning for
more accurate results.

Adds realistic Next.js project fixtures (Tailwind, CSS Modules, styled-components)
plus Vue, Svelte, JSX, and CSS-in-JS unit fixtures. 158 tests, 356 assertions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-04-03 15:02:54 -07:00
co-authored by Claude Opus 4.6
parent dd559cd1e9
commit 8d05411c3e
40 changed files with 1881 additions and 15 deletions
@@ -1945,31 +1945,108 @@ const REGEX_ANALYZERS = [
},
];
function detectText(content, filePath) {
const findings = [];
const lines = content.split('\n');
// ---------------------------------------------------------------------------
// Style block extraction (Vue/Svelte <style> blocks)
// ---------------------------------------------------------------------------
function extractStyleBlocks(content, ext) {
ext = ext.toLowerCase();
if (ext !== '.vue' && ext !== '.svelte') return [];
const blocks = [];
const re = /<style[^>]*>([\s\S]*?)<\/style>/gi;
let m;
while ((m = re.exec(content)) !== null) {
const before = content.substring(0, m.index);
const startLine = before.split('\n').length + 1;
blocks.push({ content: m[1], startLine });
}
return blocks;
}
// ---------------------------------------------------------------------------
// CSS-in-JS extraction (styled-components, emotion)
// ---------------------------------------------------------------------------
const CSS_IN_JS_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx']);
function extractCSSinJS(content, ext) {
ext = ext.toLowerCase();
if (!CSS_IN_JS_EXTENSIONS.has(ext)) return [];
const blocks = [];
const re = /(?:styled(?:\.\w+|\([^)]+\))|css)\s*`([\s\S]*?)`/g;
let m;
while ((m = re.exec(content)) !== null) {
const before = content.substring(0, m.index);
const startLine = before.split('\n').length;
blocks.push({ content: m[1], startLine });
}
return blocks;
}
function runRegexMatchers(lines, filePath, lineOffset = 0, blockContext = null) {
const findings = [];
for (const matcher of REGEX_MATCHERS) {
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
matcher.regex.lastIndex = 0;
let m;
while ((m = matcher.regex.exec(line)) !== null) {
if (matcher.test(m, line)) {
findings.push(finding(matcher.id, filePath, matcher.fmt(m, line), i + 1));
// For extracted blocks, use nearby lines as context for multi-line CSS patterns
const context = blockContext
? lines.slice(Math.max(0, i - 3), Math.min(lines.length, i + 4)).join(' ')
: line;
if (matcher.test(m, context)) {
findings.push(finding(matcher.id, filePath, matcher.fmt(m, context), i + 1 + lineOffset));
}
}
}
}
return findings;
}
function detectText(content, filePath) {
const findings = [];
const lines = content.split('\n');
const ext = filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
// Run regex matchers on the full file content (catches Tailwind classes, inline styles)
// Enable block context for CSS files where related properties span multiple lines
const cssLike = new Set(['.css', '.scss', '.less']);
findings.push(...runRegexMatchers(lines, filePath, 0, cssLike.has(ext) || null));
// Extract and scan <style> blocks from Vue/Svelte SFCs
const styleBlocks = extractStyleBlocks(content, ext);
for (const block of styleBlocks) {
const blockLines = block.content.split('\n');
findings.push(...runRegexMatchers(blockLines, filePath, block.startLine - 1, true));
}
// Extract and scan CSS-in-JS template literals
const cssJsBlocks = extractCSSinJS(content, ext);
for (const block of cssJsBlocks) {
const blockLines = block.content.split('\n');
findings.push(...runRegexMatchers(blockLines, filePath, block.startLine - 1, true));
}
// Deduplicate findings (same antipattern + similar snippet, within 2 lines)
const deduped = [];
for (const f of findings) {
const isDupe = deduped.some(d =>
d.antipattern === f.antipattern &&
d.snippet === f.snippet &&
Math.abs(d.line - f.line) <= 2
);
if (!isDupe) deduped.push(f);
}
// Page-level analyzers only run on full pages
if (isFullPage(content)) {
for (const analyzer of REGEX_ANALYZERS) {
findings.push(...analyzer(content, filePath));
deduped.push(...analyzer(content, filePath));
}
}
return findings;
return deduped;
}
// ---------------------------------------------------------------------------
@@ -2016,7 +2093,8 @@ function formatFindings(findings, jsonMode) {
}
const out = [];
for (const [file, items] of Object.entries(grouped)) {
out.push(`\n${file}`);
const importNote = items[0]?.importedBy?.length ? ` (imported by ${items[0].importedBy.join(', ')})` : '';
out.push(`\n${file}${importNote}`);
for (const item of items) {
out.push(` ${item.line ? `line ${item.line}: ` : ''}[${item.antipattern}] ${item.snippet}`);
out.push(`${item.description}`);
@@ -2045,6 +2123,159 @@ async function handleStdin() {
return detectText(input, '<stdin>');
}
// ---------------------------------------------------------------------------
// Import graph (multi-file awareness)
// ---------------------------------------------------------------------------
function resolveImport(specifier, fromDir, fileSet) {
if (!/^[./]/.test(specifier)) return null; // skip bare specifiers
const base = path.resolve(fromDir, specifier);
if (fileSet.has(base)) return base;
for (const ext of SCANNABLE_EXTENSIONS) {
const withExt = base + ext;
if (fileSet.has(withExt)) return withExt;
}
// index file convention
for (const ext of SCANNABLE_EXTENSIONS) {
const indexFile = path.join(base, 'index' + ext);
if (fileSet.has(indexFile)) return indexFile;
}
return null;
}
function buildImportGraph(files) {
const fileSet = new Set(files);
const graph = new Map();
for (const file of files) {
const content = fs.readFileSync(file, 'utf-8');
const dir = path.dirname(file);
const imports = new Set();
// ES imports: import ... from '...' and import '...'
const esRe = /import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g;
let m;
while ((m = esRe.exec(content)) !== null) {
const resolved = resolveImport(m[1], dir, fileSet);
if (resolved) imports.add(resolved);
}
// CSS @import
const cssRe = /@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g;
while ((m = cssRe.exec(content)) !== null) {
const resolved = resolveImport(m[1], dir, fileSet);
if (resolved) imports.add(resolved);
}
// SCSS @use / @forward
const scssRe = /@(?:use|forward)\s+['"]([^'"]+)['"]/g;
while ((m = scssRe.exec(content)) !== null) {
const resolved = resolveImport(m[1], dir, fileSet);
if (resolved) imports.add(resolved);
}
graph.set(file, imports);
}
return graph;
}
// ---------------------------------------------------------------------------
// Framework dev server detection
// ---------------------------------------------------------------------------
const FRAMEWORK_CONFIGS = [
{ name: 'Next.js', files: ['next.config.js', 'next.config.mjs', 'next.config.ts'], defaultPort: 3000,
portRe: /port\s*[:=]\s*(\d+)/,
fingerprint: { header: 'x-powered-by', value: /next/i } },
{ name: 'SvelteKit', files: ['svelte.config.js', 'svelte.config.ts'], defaultPort: 5173,
portRe: /port\s*[:=]\s*(\d+)/,
fingerprint: { header: 'x-sveltekit-page', value: null } },
{ name: 'Nuxt', files: ['nuxt.config.js', 'nuxt.config.ts'], defaultPort: 3000,
portRe: /port\s*[:=]\s*(\d+)/,
fingerprint: { header: 'x-powered-by', value: /nuxt/i } },
{ name: 'Vite', files: ['vite.config.js', 'vite.config.ts', 'vite.config.mjs'], defaultPort: 5173,
portRe: /port\s*[:=]\s*(\d+)/,
fingerprint: { body: /@vite\/client/ } },
{ name: 'Astro', files: ['astro.config.js', 'astro.config.ts', 'astro.config.mjs'], defaultPort: 4321,
portRe: /port\s*[:=]\s*(\d+)/,
fingerprint: { body: /astro/i } },
{ name: 'Angular', files: ['angular.json'], defaultPort: 4200,
portRe: /"port"\s*:\s*(\d+)/,
fingerprint: { body: /ng-version/i } },
{ name: 'Remix', files: ['remix.config.js', 'remix.config.ts'], defaultPort: 3000,
portRe: /port\s*[:=]\s*(\d+)/,
fingerprint: { header: 'x-powered-by', value: /remix/i } },
];
function detectFrameworkConfig(dir) {
let entries;
try { entries = fs.readdirSync(dir); } catch { return null; }
const entrySet = new Set(entries);
for (const cfg of FRAMEWORK_CONFIGS) {
const match = cfg.files.find(f => entrySet.has(f));
if (!match) continue;
const configPath = path.join(dir, match);
let port = cfg.defaultPort;
try {
const content = fs.readFileSync(configPath, 'utf-8');
const portMatch = content.match(cfg.portRe);
if (portMatch) port = parseInt(portMatch[1], 10);
} catch { /* use default */ }
return { name: cfg.name, port, configPath, fingerprint: cfg.fingerprint };
}
return null;
}
/**
* Check if a port is listening and optionally verify it matches the expected framework.
* Returns { listening: true, matched: true/false } or { listening: false }.
*/
async function isPortListening(port, fingerprint = null) {
if (!fingerprint) {
// Simple TCP probe fallback
const net = await import('node:net');
return new Promise((resolve) => {
const sock = net.default.createConnection({ port, host: '127.0.0.1' });
sock.setTimeout(500);
sock.on('connect', () => { sock.destroy(); resolve({ listening: true, matched: true }); });
sock.on('error', () => resolve({ listening: false }));
sock.on('timeout', () => { sock.destroy(); resolve({ listening: false }); });
});
}
// HTTP probe with fingerprint matching
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 2000);
const res = await fetch(`http://localhost:${port}/`, { signal: controller.signal, redirect: 'follow' });
clearTimeout(timeout);
// Check header fingerprint
if (fingerprint.header) {
const val = res.headers.get(fingerprint.header);
if (val && (!fingerprint.value || fingerprint.value.test(val))) {
return { listening: true, matched: true };
}
}
// Check body fingerprint
if (fingerprint.body) {
const body = await res.text();
if (fingerprint.body.test(body)) {
return { listening: true, matched: true };
}
}
// Port is listening but doesn't match the expected framework
return { listening: true, matched: false };
} catch {
return { listening: false };
}
}
// ---------------------------------------------------------------------------
// CLI
// ---------------------------------------------------------------------------
@@ -2101,14 +2332,63 @@ async function main() {
catch { process.stderr.write(`Warning: cannot access ${target}\n`); continue; }
if (stat.isDirectory()) {
for (const file of walkDir(resolved)) {
const ext = path.extname(file).toLowerCase();
if (!fastMode && HTML_EXTENSIONS.has(ext)) {
allFindings.push(...await detectHtml(file));
} else {
allFindings.push(...detectText(fs.readFileSync(file, 'utf-8'), file));
// Check for framework dev server config (skip in JSON mode to avoid polluting output)
if (!jsonMode) {
const fwConfig = detectFrameworkConfig(resolved);
if (fwConfig) {
const probe = await isPortListening(fwConfig.port, fwConfig.fingerprint);
if (probe.listening && probe.matched) {
process.stderr.write(
`\n${fwConfig.name} dev server detected on localhost:${fwConfig.port}.\n` +
`For more accurate results, scan the running site:\n` +
` npx impeccable detect http://localhost:${fwConfig.port}\n\n`
);
} else if (probe.listening && !probe.matched) {
process.stderr.write(
`\n${fwConfig.name} project detected (${path.basename(fwConfig.configPath)}).\n` +
`Port ${fwConfig.port} is in use by another service. Start the ${fwConfig.name} dev server and scan via URL for best results.\n\n`
);
} else {
process.stderr.write(
`\n${fwConfig.name} project detected (${path.basename(fwConfig.configPath)}).\n` +
`Start the dev server and scan via URL for best results:\n` +
` npx impeccable detect http://localhost:${fwConfig.port}\n\n`
);
}
}
}
const files = walkDir(resolved);
// Build import graph for multi-file awareness
const graph = buildImportGraph(files);
// Build reverse map: file -> set of files that import it
const importedByMap = new Map();
for (const [importer, imports] of graph) {
for (const imported of imports) {
if (!importedByMap.has(imported)) importedByMap.set(imported, new Set());
importedByMap.get(imported).add(importer);
}
}
for (const file of files) {
const ext = path.extname(file).toLowerCase();
let fileFindings;
if (!fastMode && HTML_EXTENSIONS.has(ext)) {
fileFindings = await detectHtml(file);
} else {
fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file);
}
// Annotate findings with import context
const importers = importedByMap.get(file);
if (importers && importers.size > 0) {
const importerNames = [...importers].map(f => path.basename(f));
for (const f of fileFindings) {
f.importedBy = importerNames;
}
}
allFindings.push(...fileFindings);
}
} else if (stat.isFile()) {
const ext = path.extname(resolved).toLowerCase();
if (!fastMode && HTML_EXTENSIONS.has(ext)) {
@@ -2148,6 +2428,9 @@ export {
checkElementBorders, checkElementMotion, checkElementGlow, checkPageTypography, checkPageLayout, isNeutralColor, isFullPage,
detectHtml, detectUrl, detectText,
walkDir, formatFindings, SCANNABLE_EXTENSIONS, SKIP_DIRS,
extractStyleBlocks, extractCSSinJS,
buildImportGraph, resolveImport,
detectFrameworkConfig, isPortListening, FRAMEWORK_CONFIGS,
main as detectCli,
};
+642 -1
View File
@@ -4,8 +4,10 @@ import path from 'path';
import { spawnSync } from 'child_process';
import {
ANTIPATTERNS, checkElementBorders, checkElementMotion, checkElementGlow, isNeutralColor, isFullPage,
detectText,
detectText, extractStyleBlocks, extractCSSinJS,
walkDir, SCANNABLE_EXTENSIONS,
buildImportGraph, resolveImport,
detectFrameworkConfig, isPortListening, FRAMEWORK_CONFIGS,
} from '../source/skills/critique/scripts/detect-antipatterns.mjs';
const FIXTURES = path.join(import.meta.dir, 'fixtures', 'antipatterns');
@@ -588,3 +590,642 @@ describe('CLI', () => {
expect(stderr).toContain('Warning');
});
});
// ---------------------------------------------------------------------------
// Tier 1: Vue/Svelte <style> block extraction
// ---------------------------------------------------------------------------
describe('extractStyleBlocks', () => {
test('extracts single <style> block from Vue SFC', () => {
const vue = `<template><div>hi</div></template>
<style scoped>
.card { border-left: 4px solid blue; }
</style>`;
const blocks = extractStyleBlocks(vue, '.vue');
expect(blocks.length).toBe(1);
expect(blocks[0].content).toContain('border-left: 4px solid blue');
expect(blocks[0].startLine).toBeGreaterThan(1);
});
test('extracts multiple <style> blocks', () => {
const vue = `<template><div>hi</div></template>
<style>
.a { color: red; }
</style>
<style scoped>
.b { color: blue; }
</style>`;
const blocks = extractStyleBlocks(vue, '.vue');
expect(blocks.length).toBe(2);
});
test('extracts <style> from Svelte', () => {
const svelte = `<div>hi</div>
<style>
.sidebar { border-right: 4px solid #8b5cf6; }
</style>`;
const blocks = extractStyleBlocks(svelte, '.svelte');
expect(blocks.length).toBe(1);
expect(blocks[0].content).toContain('border-right: 4px solid');
});
test('returns empty for non-Vue/Svelte files', () => {
const jsx = 'export function Card() { return <div>hi</div>; }';
expect(extractStyleBlocks(jsx, '.jsx')).toHaveLength(0);
expect(extractStyleBlocks(jsx, '.tsx')).toHaveLength(0);
});
test('returns empty when no <style> blocks exist', () => {
const vue = '<template><div>hi</div></template><script>export default {}</script>';
expect(extractStyleBlocks(vue, '.vue')).toHaveLength(0);
});
});
// ---------------------------------------------------------------------------
// Tier 1: CSS-in-JS extraction
// ---------------------------------------------------------------------------
describe('extractCSSinJS', () => {
test('extracts styled-components template literal', () => {
const tsx = "const Card = styled.div`\n border-left: 4px solid blue;\n padding: 16px;\n`;";
const blocks = extractCSSinJS(tsx, '.tsx');
expect(blocks.length).toBeGreaterThanOrEqual(1);
expect(blocks.some(b => b.content.includes('border-left: 4px solid'))).toBe(true);
});
test('extracts styled(Component) template literal', () => {
const tsx = "const Box = styled(BaseBox)`\n border-right: 5px solid #8b5cf6;\n`;";
const blocks = extractCSSinJS(tsx, '.tsx');
expect(blocks.length).toBeGreaterThanOrEqual(1);
expect(blocks.some(b => b.content.includes('border-right: 5px solid'))).toBe(true);
});
test('extracts emotion css template literal', () => {
const tsx = "const style = css`\n animation: bounce 1s infinite;\n`;";
const blocks = extractCSSinJS(tsx, '.tsx');
expect(blocks.length).toBeGreaterThanOrEqual(1);
expect(blocks.some(b => b.content.includes('animation: bounce'))).toBe(true);
});
test('returns empty for non-JS files', () => {
expect(extractCSSinJS('.card { color: red; }', '.css')).toHaveLength(0);
expect(extractCSSinJS('<div>hi</div>', '.html')).toHaveLength(0);
});
test('returns empty when no CSS-in-JS patterns exist', () => {
const tsx = "function Card() { return <div className='p-4'>hi</div>; }";
expect(extractCSSinJS(tsx, '.tsx')).toHaveLength(0);
});
});
// ---------------------------------------------------------------------------
// Tier 1: detectText on Vue/Svelte files (style blocks + template classes)
// ---------------------------------------------------------------------------
describe('detectText -- Vue SFC', () => {
test('detects side-tab in <style> block', () => {
const vue = `<template><div class="card">hi</div></template>
<style scoped>
.card { border-left: 4px solid #3b82f6; border-radius: 12px; }
</style>`;
const f = detectText(vue, 'Card.vue');
expect(f.some(r => r.antipattern === 'side-tab')).toBe(true);
});
test('detects overused font in <style> block', () => {
const vue = `<template><div>hi</div></template>
<style>
body { font-family: 'Inter', sans-serif; }
</style>`;
const f = detectText(vue, 'App.vue');
expect(f.some(r => r.antipattern === 'overused-font')).toBe(true);
});
test('detects bounce animation in <style> block', () => {
const vue = `<template><div>hi</div></template>
<style>
.item { animation: bounce 1s infinite; }
</style>`;
const f = detectText(vue, 'Card.vue');
expect(f.some(r => r.antipattern === 'bounce-easing')).toBe(true);
});
test('detects gradient-text in <style> block', () => {
const vue = `<template><div>hi</div></template>
<style>
h1 { background: linear-gradient(to right, purple, cyan); -webkit-background-clip: text; background-clip: text; }
</style>`;
const f = detectText(vue, 'Hero.vue');
expect(f.some(r => r.antipattern === 'gradient-text')).toBe(true);
});
test('detects Tailwind anti-patterns in <template>', () => {
const vue = `<template>
<div class="border-l-4 border-blue-500 rounded-lg">card</div>
</template>`;
const f = detectText(vue, 'Card.vue');
expect(f.some(r => r.antipattern === 'side-tab')).toBe(true);
});
});
describe('detectText -- Svelte', () => {
test('detects side-tab in <style> block', () => {
const svelte = `<div>hi</div>
<style>
.sidebar { border-right: 4px solid #8b5cf6; border-radius: 16px; }
</style>`;
const f = detectText(svelte, 'Sidebar.svelte');
expect(f.some(r => r.antipattern === 'side-tab')).toBe(true);
});
test('detects overused font in <style> block', () => {
const svelte = `<div>hi</div>
<style>
.app { font-family: 'Roboto', sans-serif; }
</style>`;
const f = detectText(svelte, 'App.svelte');
expect(f.some(r => r.antipattern === 'overused-font')).toBe(true);
});
test('detects layout transition in <style> block', () => {
const svelte = `<div>hi</div>
<style>
.panel { transition: height 0.4s ease; }
</style>`;
const f = detectText(svelte, 'Panel.svelte');
expect(f.some(r => r.antipattern === 'layout-transition')).toBe(true);
});
});
// ---------------------------------------------------------------------------
// Tier 1: detectText on CSS-in-JS files
// ---------------------------------------------------------------------------
describe('detectText -- CSS-in-JS', () => {
test('detects side-tab in styled-components', () => {
const tsx = "const Card = styled.div`\n border-left: 4px solid #3b82f6;\n border-radius: 12px;\n`;";
const f = detectText(tsx, 'Card.tsx');
expect(f.some(r => r.antipattern === 'side-tab')).toBe(true);
});
test('detects bounce in emotion css', () => {
const tsx = "const style = css`\n animation: bounce 1s infinite;\n`;";
const f = detectText(tsx, 'anim.ts');
expect(f.some(r => r.antipattern === 'bounce-easing')).toBe(true);
});
test('detects overused font in styled-components', () => {
const tsx = "const Wrapper = styled.main`\n font-family: 'Inter', sans-serif;\n`;";
const f = detectText(tsx, 'Layout.tsx');
expect(f.some(r => r.antipattern === 'overused-font')).toBe(true);
});
test('detects gradient-text in styled-components', () => {
const tsx = "const Title = styled.h1`\n background: linear-gradient(to right, purple, cyan);\n -webkit-background-clip: text;\n background-clip: text;\n`;";
const f = detectText(tsx, 'Hero.tsx');
expect(f.some(r => r.antipattern === 'gradient-text')).toBe(true);
});
test('detects pure-black-white in styled-components', () => {
const tsx = "const Dark = styled.section`\n background-color: #000000;\n`;";
const f = detectText(tsx, 'Dark.tsx');
expect(f.some(r => r.antipattern === 'pure-black-white')).toBe(true);
});
test('does not false-positive on clean CSS-in-JS', () => {
const tsx = "const Card = styled.div`\n border-radius: 12px;\n padding: 24px;\n`;";
const f = detectText(tsx, 'Card.tsx');
expect(f.filter(r => r.antipattern === 'side-tab')).toHaveLength(0);
});
});
// ---------------------------------------------------------------------------
// Tier 1: Fixture file integration tests (CLI)
// ---------------------------------------------------------------------------
describe('CLI -- framework fixtures', () => {
function run(...args) {
const result = spawnSync('node', [SCRIPT, ...args], { encoding: 'utf-8', timeout: 15000 });
return { stdout: result.stdout || '', stderr: result.stderr || '', code: result.status };
}
test('jsx-should-flag catches anti-patterns', () => {
const { code, stderr } = run(path.join(FIXTURES, 'jsx-should-flag.jsx'));
expect(code).toBe(2);
expect(stderr).toContain('side-tab');
});
test('jsx-should-pass is clean', () => {
const { code } = run(path.join(FIXTURES, 'jsx-should-pass.jsx'));
expect(code).toBe(0);
});
test('vue-should-flag catches anti-patterns', () => {
const { code, stderr } = run(path.join(FIXTURES, 'vue-should-flag.vue'));
expect(code).toBe(2);
expect(stderr).toContain('side-tab');
});
test('vue-should-pass is clean', () => {
const { code } = run(path.join(FIXTURES, 'vue-should-pass.vue'));
expect(code).toBe(0);
});
test('svelte-should-flag catches anti-patterns', () => {
const { code, stderr } = run(path.join(FIXTURES, 'svelte-should-flag.svelte'));
expect(code).toBe(2);
expect(stderr).toContain('side-tab');
});
test('svelte-should-pass is clean', () => {
const { code } = run(path.join(FIXTURES, 'svelte-should-pass.svelte'));
expect(code).toBe(0);
});
test('cssinjs-should-flag catches anti-patterns', () => {
const { code, stderr } = run(path.join(FIXTURES, 'cssinjs-should-flag.tsx'));
expect(code).toBe(2);
expect(stderr).toContain('side-tab');
});
test('cssinjs-should-pass is clean', () => {
const { code } = run(path.join(FIXTURES, 'cssinjs-should-pass.tsx'));
expect(code).toBe(0);
});
});
// ---------------------------------------------------------------------------
// Realistic Next.js project fixtures
// ---------------------------------------------------------------------------
describe('CLI -- Next.js + Tailwind project', () => {
const dir = path.join(FIXTURES, 'framework-next-tailwind');
let stderr;
function run(...args) {
const result = spawnSync('node', [SCRIPT, ...args], { encoding: 'utf-8', timeout: 15000 });
return { stdout: result.stdout || '', stderr: result.stderr || '', code: result.status };
}
test('finds all expected anti-pattern types', () => {
const result = run(dir);
stderr = result.stderr;
expect(result.code).toBe(2);
for (const ap of ['side-tab', 'gradient-text', 'ai-color-palette', 'overused-font', 'bounce-easing', 'pure-black-white']) {
expect(stderr).toContain(ap);
}
});
test('FeatureCard: side-tab + ai-color-palette + bounce-easing', () => {
const { stderr } = run(path.join(dir, 'components', 'FeatureCard.tsx'));
expect(stderr).toContain('side-tab');
expect(stderr).toContain('border-l-4');
expect(stderr).toContain('ai-color-palette');
expect(stderr).toContain('text-purple-600');
expect(stderr).toContain('bounce-easing');
expect(stderr).toContain('animate-bounce');
});
test('PricingCard: pure-black-white + gradient-text + ai-color-palette', () => {
const { stderr } = run(path.join(dir, 'components', 'PricingCard.tsx'));
expect(stderr).toContain('pure-black-white');
expect(stderr).toContain('bg-black');
expect(stderr).toContain('gradient-text');
expect(stderr).toContain('bg-clip-text');
expect(stderr).toContain('ai-color-palette');
});
test('globals.css: overused Inter font', () => {
const { stderr } = run(path.join(dir, 'app', 'globals.css'));
expect(stderr).toContain('overused-font');
expect(stderr).toContain('Inter');
});
test('page.tsx: gradient-text + ai-color-palette', () => {
const { stderr } = run(path.join(dir, 'app', 'page.tsx'));
expect(stderr).toContain('gradient-text');
expect(stderr).toContain('ai-color-palette');
});
test('directory scan shows import context for components', () => {
const { stderr } = run(dir);
expect(stderr).toContain('imported by page.tsx');
});
test('--json produces clean JSON without framework message', () => {
const { stderr, code } = run('--json', dir);
expect(code).toBe(2);
const parsed = JSON.parse(stderr.trim());
expect(parsed).toBeArray();
expect(parsed.length).toBeGreaterThanOrEqual(6);
});
});
describe('CLI -- Next.js + CSS Modules project', () => {
function run(...args) {
const result = spawnSync('node', [SCRIPT, ...args], { encoding: 'utf-8', timeout: 15000 });
return { stdout: result.stdout || '', stderr: result.stderr || '', code: result.status };
}
const dir = path.join(FIXTURES, 'framework-next-modules');
test('finds all expected anti-pattern types', () => {
const { code, stderr } = run(dir);
expect(code).toBe(2);
for (const ap of ['side-tab', 'overused-font', 'pure-black-white', 'layout-transition', 'gradient-text']) {
expect(stderr).toContain(ap);
}
});
test('StatsCard.module.css: side-tab + overused-font + layout-transition', () => {
const { stderr } = run(path.join(dir, 'components', 'StatsCard.module.css'));
expect(stderr).toContain('side-tab');
expect(stderr).toContain('border-left: 4px solid #6366f1');
expect(stderr).toContain('overused-font');
expect(stderr).toContain('Inter');
expect(stderr).toContain('layout-transition');
expect(stderr).toContain('transition: width');
});
test('Sidebar.module.css: side-tab border accent', () => {
const { stderr } = run(path.join(dir, 'components', 'Sidebar.module.css'));
expect(stderr).toContain('side-tab');
expect(stderr).toContain('border-right: 3px solid');
});
test('globals.css: overused Roboto + pure-black-white', () => {
const { stderr } = run(path.join(dir, 'app', 'globals.css'));
expect(stderr).toContain('overused-font');
expect(stderr).toContain('Roboto');
expect(stderr).toContain('pure-black-white');
expect(stderr).toContain('#000000');
});
test('page.module.css: gradient-text across lines', () => {
const { stderr } = run(path.join(dir, 'app', 'page.module.css'));
expect(stderr).toContain('gradient-text');
expect(stderr).toContain('background-clip: text');
});
test('directory scan shows import context for CSS modules', () => {
const { stderr } = run(dir);
expect(stderr).toContain('imported by StatsCard.tsx');
expect(stderr).toContain('imported by Sidebar.tsx');
expect(stderr).toContain('imported by layout.tsx');
});
});
describe('CLI -- Next.js + CSS-in-JS (styled-components) project', () => {
function run(...args) {
const result = spawnSync('node', [SCRIPT, ...args], { encoding: 'utf-8', timeout: 15000 });
return { stdout: result.stdout || '', stderr: result.stderr || '', code: result.status };
}
const dir = path.join(FIXTURES, 'framework-next-cssinjs');
test('finds all expected anti-pattern types', () => {
const { code, stderr } = run(dir);
expect(code).toBe(2);
for (const ap of ['side-tab', 'gradient-text', 'overused-font', 'bounce-easing', 'pure-black-white', 'layout-transition']) {
expect(stderr).toContain(ap);
}
});
test('FeatureGrid.tsx: side-tab + bounce-easing + layout-transition', () => {
const { stderr } = run(path.join(dir, 'components', 'FeatureGrid.tsx'));
expect(stderr).toContain('side-tab');
expect(stderr).toContain('border-left: 4px solid');
expect(stderr).toContain('bounce-easing');
expect(stderr).toContain('animation: bounce');
expect(stderr).toContain('layout-transition');
expect(stderr).toContain('transition: width');
});
test('Hero.tsx: gradient-text + overused Montserrat font', () => {
const { stderr } = run(path.join(dir, 'components', 'Hero.tsx'));
expect(stderr).toContain('gradient-text');
expect(stderr).toContain('background-clip: text');
expect(stderr).toContain('overused-font');
expect(stderr).toContain('Montserrat');
});
test('GlobalStyle.tsx: overused Inter + pure-black-white', () => {
const { stderr } = run(path.join(dir, 'components', 'GlobalStyle.tsx'));
expect(stderr).toContain('overused-font');
expect(stderr).toContain('Inter');
expect(stderr).toContain('pure-black-white');
expect(stderr).toContain('#000000');
});
test('Testimonials.tsx: side-tab + gradient-text in styled blockquote', () => {
const { stderr } = run(path.join(dir, 'components', 'Testimonials.tsx'));
expect(stderr).toContain('side-tab');
expect(stderr).toContain('border-left: 4px solid');
expect(stderr).toContain('gradient-text');
});
test('directory scan shows import context for components', () => {
const { stderr } = run(dir);
expect(stderr).toContain('imported by index.tsx');
expect(stderr).toContain('imported by _app.tsx');
});
test('--json produces clean JSON without framework message', () => {
const { stderr, code } = run('--json', dir);
expect(code).toBe(2);
const parsed = JSON.parse(stderr.trim());
expect(parsed).toBeArray();
expect(parsed.length).toBeGreaterThanOrEqual(6);
// Verify importedBy is present in JSON
const featureGridFindings = parsed.filter(f => f.file?.includes('FeatureGrid'));
expect(featureGridFindings.length).toBeGreaterThan(0);
expect(featureGridFindings[0].importedBy).toContain('index.tsx');
});
});
// ---------------------------------------------------------------------------
// Tier 2: Import graph
// ---------------------------------------------------------------------------
describe('buildImportGraph', () => {
const MF = path.join(FIXTURES, 'multifile');
test('resolves ES import from tsx to tsx', () => {
const graph = buildImportGraph([
path.join(MF, 'App.tsx'),
path.join(MF, 'Card.tsx'),
path.join(MF, 'styles.css'),
]);
const appImports = graph.get(path.join(MF, 'App.tsx'));
expect(appImports).toBeDefined();
expect(appImports.has(path.join(MF, 'Card.tsx'))).toBe(true);
expect(appImports.has(path.join(MF, 'styles.css'))).toBe(true);
});
test('resolves extensionless imports', () => {
const graph = buildImportGraph([
path.join(MF, 'App.tsx'),
path.join(MF, 'Card.tsx'),
]);
const appImports = graph.get(path.join(MF, 'App.tsx'));
expect(appImports.has(path.join(MF, 'Card.tsx'))).toBe(true);
});
test('resolves CSS @import', () => {
const graph = buildImportGraph([
path.join(MF, 'theme.scss'),
path.join(MF, 'variables.scss'),
]);
const themeImports = graph.get(path.join(MF, 'theme.scss'));
expect(themeImports).toBeDefined();
expect(themeImports.has(path.join(MF, 'variables.scss'))).toBe(true);
});
test('ignores bare/node_modules imports', () => {
const graph = buildImportGraph([
path.join(MF, 'App.tsx'),
]);
const appImports = graph.get(path.join(MF, 'App.tsx'));
// Should not contain 'react' or 'styled-components'
for (const imp of appImports) {
expect(imp).toContain(MF);
}
});
});
describe('resolveImport', () => {
const MF = path.join(FIXTURES, 'multifile');
test('resolves relative path with extension', () => {
const fileSet = new Set([path.join(MF, 'Card.tsx')]);
const result = resolveImport('./Card.tsx', MF, fileSet);
expect(result).toBe(path.join(MF, 'Card.tsx'));
});
test('resolves extensionless import by trying extensions', () => {
const fileSet = new Set([path.join(MF, 'Card.tsx')]);
const result = resolveImport('./Card', MF, fileSet);
expect(result).toBe(path.join(MF, 'Card.tsx'));
});
test('returns null for bare specifiers', () => {
const fileSet = new Set([path.join(MF, 'Card.tsx')]);
expect(resolveImport('react', MF, fileSet)).toBeNull();
expect(resolveImport('styled-components', MF, fileSet)).toBeNull();
});
test('returns null for unresolvable imports', () => {
const fileSet = new Set([path.join(MF, 'Card.tsx')]);
expect(resolveImport('./Unknown', MF, fileSet)).toBeNull();
});
});
// ---------------------------------------------------------------------------
// Tier 2: Multi-file directory scan
// ---------------------------------------------------------------------------
describe('CLI -- multi-file scan', () => {
function run(...args) {
const result = spawnSync('node', [SCRIPT, ...args], { encoding: 'utf-8', timeout: 15000 });
return { stdout: result.stdout || '', stderr: result.stderr || '', code: result.status };
}
test('scanning multifile/ directory finds findings across files', () => {
const { code, stderr } = run(path.join(FIXTURES, 'multifile'));
expect(code).toBe(2);
expect(stderr).toContain('side-tab');
});
test('--json multi-file scan includes import context', () => {
const { stderr, code } = run('--json', path.join(FIXTURES, 'multifile'));
expect(code).toBe(2);
const parsed = JSON.parse(stderr.trim());
expect(parsed.length).toBeGreaterThan(0);
// Findings from Card.tsx should mention being imported by App.tsx
const cardFindings = parsed.filter(f => f.file?.includes('Card.tsx'));
expect(cardFindings.length).toBeGreaterThan(0);
expect(cardFindings.some(f => f.importedBy?.includes('App.tsx'))).toBe(true);
});
});
// ---------------------------------------------------------------------------
// Tier 3: Framework config detection
// ---------------------------------------------------------------------------
describe('detectFrameworkConfig', () => {
test('detects next.config.mjs and returns Next.js with default port', () => {
const result = detectFrameworkConfig(path.join(FIXTURES, 'framework-next-tailwind'));
expect(result).not.toBeNull();
expect(result.name).toBe('Next.js');
expect(result.port).toBe(3000);
});
test('detects next.config.js (pages router)', () => {
const result = detectFrameworkConfig(path.join(FIXTURES, 'framework-next-cssinjs'));
expect(result).not.toBeNull();
expect(result.name).toBe('Next.js');
});
test('parses custom port from vite.config.ts', () => {
const result = detectFrameworkConfig(path.join(FIXTURES, 'framework-vite'));
expect(result).not.toBeNull();
expect(result.name).toBe('Vite');
expect(result.port).toBe(8080);
});
test('returns null for directory without framework config', () => {
const result = detectFrameworkConfig(path.join(FIXTURES, 'multifile'));
expect(result).toBeNull();
});
test('returns null for nonexistent directory', () => {
const result = detectFrameworkConfig('/nonexistent/path/12345');
expect(result).toBeNull();
});
});
describe('isPortListening', () => {
test('returns { listening: false } for unlikely port', async () => {
const result = await isPortListening(59999);
expect(result.listening).toBe(false);
});
});
describe('FRAMEWORK_CONFIGS', () => {
test('covers major frameworks', () => {
const names = FRAMEWORK_CONFIGS.map(c => c.name);
expect(names).toContain('Next.js');
expect(names).toContain('Vite');
expect(names).toContain('SvelteKit');
expect(names).toContain('Nuxt');
expect(names).toContain('Astro');
});
test('each config has required fields', () => {
for (const cfg of FRAMEWORK_CONFIGS) {
expect(cfg.name).toBeTypeOf('string');
expect(cfg.defaultPort).toBeTypeOf('number');
expect(cfg.files).toBeArray();
expect(cfg.files.length).toBeGreaterThan(0);
}
});
});
describe('CLI -- dev server suggestion', () => {
function run(...args) {
const result = spawnSync('node', [SCRIPT, ...args], { encoding: 'utf-8', timeout: 15000 });
return { stdout: result.stdout || '', stderr: result.stderr || '', code: result.status };
}
test('suggests URL scan when Next.js config found', () => {
const { stderr } = run(path.join(FIXTURES, 'framework-next-tailwind'));
expect(stderr).toContain('Next.js');
expect(stderr).toContain('3000');
});
test('suggests URL scan when Vite config found', () => {
const { stderr } = run(path.join(FIXTURES, 'framework-vite'));
expect(stderr).toContain('Vite');
expect(stderr).toContain('8080');
});
});
+44
View File
@@ -0,0 +1,44 @@
// CSS-in-JS patterns with anti-patterns (styled-components + emotion)
import styled from 'styled-components';
import { css } from '@emotion/react';
// styled-components: side-tab + border-accent-on-rounded
export const Card = styled.div`
border-left: 4px solid #3b82f6;
border-radius: 12px;
padding: 24px;
font-family: 'Inter', sans-serif;
`;
// styled-components: pure black background + gradient text
export const Hero = styled.section`
background-color: #000000;
padding: 80px 20px;
text-align: center;
h1 {
background: linear-gradient(to right, #a855f7, #06b6d4);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
`;
// emotion css: bounce animation + layout transition
export const animatedPanel = css`
animation: bounce 1s infinite;
transition: width 0.3s ease;
`;
// styled with parenthesized component
export const AccentBox = styled(Box)`
border-right: 5px solid #8b5cf6;
border-radius: 8px;
`;
// Object style pattern
export const inlineStyles = {
borderLeft: '4px solid #6366f1',
borderRadius: '12px',
};
+25
View File
@@ -0,0 +1,25 @@
// Clean CSS-in-JS patterns -- no anti-patterns
import styled from 'styled-components';
import { css } from '@emotion/react';
export const Card = styled.div`
border-radius: 12px;
padding: 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
font-family: 'Geist', system-ui, sans-serif;
`;
export const Hero = styled.section`
background-color: #0f172a;
padding: 80px 20px;
h1 {
color: #f8fafc;
font-size: 3rem;
}
`;
export const smoothPanel = css`
transition: opacity 0.3s ease, transform 0.2s ease;
`;
@@ -0,0 +1,58 @@
import styled from "styled-components";
const Grid = styled.div`
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 32px;
max-width: 1200px;
margin: 0 auto;
padding: 80px 24px;
`;
const Card = styled.div`
border-left: 4px solid #8b5cf6;
border-radius: 16px;
background: #1a1a2e;
padding: 32px;
box-shadow: 0 0 25px rgba(139, 92, 246, 0.2);
transition: width 0.3s ease;
`;
const CardIcon = styled.div`
font-size: 40px;
margin-bottom: 16px;
animation: bounce 2s infinite;
`;
const CardTitle = styled.h3`
font-size: 20px;
font-weight: 700;
color: #a855f7;
margin-bottom: 8px;
`;
const CardDescription = styled.p`
font-size: 15px;
color: #6b7280;
line-height: 1.6;
`;
const features = [
{ icon: "⚡", title: "Blazing Fast", description: "Optimized for speed with edge-first architecture." },
{ icon: "🔒", title: "Secure by Default", description: "Enterprise-grade security with zero configuration." },
{ icon: "📦", title: "Modular Design", description: "Pick and choose only what you need. No bloat." },
];
export function FeatureGrid() {
return (
<Grid>
{features.map((feature) => (
<Card key={feature.title}>
<CardIcon>{feature.icon}</CardIcon>
<CardTitle>{feature.title}</CardTitle>
<CardDescription>{feature.description}</CardDescription>
</Card>
))}
</Grid>
);
}
@@ -0,0 +1,18 @@
import { createGlobalStyle } from "styled-components";
export const GlobalStyle = createGlobalStyle`
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', sans-serif;
background-color: #000000;
color: #ffffff;
line-height: 1.6;
}
`;
@@ -0,0 +1,61 @@
import styled from "styled-components";
const Section = styled.section`
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 24px;
text-align: center;
`;
const Title = styled.h1`
font-size: 64px;
font-weight: 800;
font-family: 'Montserrat', sans-serif;
background: linear-gradient(135deg, #a855f7, #06b6d4);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
margin-bottom: 24px;
line-height: 1.1;
`;
const Subtitle = styled.p`
font-size: 20px;
color: #6b7280;
max-width: 600px;
margin-bottom: 48px;
`;
const CTAButton = styled.button`
padding: 16px 48px;
font-size: 18px;
font-weight: 600;
color: white;
background: linear-gradient(135deg, #8b5cf6, #6366f1);
border: none;
border-radius: 12px;
cursor: pointer;
box-shadow: 0 0 40px rgba(139, 92, 246, 0.4);
transition: transform 0.2s ease, box-shadow 0.2s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 0 60px rgba(139, 92, 246, 0.6);
}
`;
export function Hero() {
return (
<Section>
<Title>Build the Future</Title>
<Subtitle>
The most powerful platform for modern web development.
Ship faster, scale easier.
</Subtitle>
<CTAButton>Get Started Free</CTAButton>
</Section>
);
}
@@ -0,0 +1,53 @@
import styled from "styled-components";
const Section = styled.section`
padding: 80px 24px;
max-width: 800px;
margin: 0 auto;
`;
const SectionTitle = styled.h2`
font-size: 36px;
font-weight: 700;
text-align: center;
margin-bottom: 48px;
background: linear-gradient(to right, #a855f7, #ec4899);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
`;
const Quote = styled.blockquote`
border-left: 4px solid #6366f1;
border-radius: 12px;
background: #1a1a2e;
padding: 24px 32px;
margin-bottom: 24px;
font-size: 16px;
color: #d1d5db;
font-style: italic;
`;
const Author = styled.p`
font-size: 14px;
color: #6b7280;
margin-top: 12px;
font-style: normal;
`;
export function Testimonials() {
return (
<Section>
<SectionTitle>What People Say</SectionTitle>
<Quote>
"This platform completely transformed our workflow. Deployment went from
hours to minutes."
<Author>-- Jane Smith, CTO at TechCorp</Author>
</Quote>
<Quote>
"The developer experience is unmatched. I can't imagine going back."
<Author>-- Alex Chen, Senior Engineer</Author>
</Quote>
</Section>
);
}
@@ -0,0 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
compiler: {
styledComponents: true,
},
};
module.exports = nextConfig;
@@ -0,0 +1,27 @@
import type { AppProps } from "next/app";
import { ThemeProvider } from "styled-components";
import { GlobalStyle } from "../components/GlobalStyle";
const theme = {
colors: {
primary: "#8b5cf6",
secondary: "#06b6d4",
background: "#000000",
surface: "#1a1a2e",
text: "#ffffff",
muted: "#6b7280",
},
fonts: {
body: "'Inter', sans-serif",
heading: "'Montserrat', sans-serif",
},
};
export default function App({ Component, pageProps }: AppProps) {
return (
<ThemeProvider theme={theme}>
<GlobalStyle />
<Component {...pageProps} />
</ThemeProvider>
);
}
@@ -0,0 +1,13 @@
import { Hero } from "../components/Hero";
import { FeatureGrid } from "../components/FeatureGrid";
import { Testimonials } from "../components/Testimonials";
export default function Home() {
return (
<>
<Hero />
<FeatureGrid />
<Testimonials />
</>
);
}
@@ -0,0 +1,18 @@
*,
*::before,
*::after {
box-sizing: border-box;
padding: 0;
margin: 0;
}
body {
font-family: 'Roboto', sans-serif;
background-color: #000000;
color: #fff;
}
a {
color: inherit;
text-decoration: none;
}
@@ -0,0 +1,19 @@
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "Dashboard",
description: "Analytics dashboard",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
@@ -0,0 +1,25 @@
.container {
display: flex;
min-height: 100vh;
}
.main {
flex: 1;
padding: 32px;
}
.title {
font-size: 28px;
font-weight: 700;
margin-bottom: 32px;
background: linear-gradient(to right, #a855f7, #06b6d4);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
@@ -0,0 +1,19 @@
import { StatsCard } from "../components/StatsCard";
import { Sidebar } from "../components/Sidebar";
import styles from "./page.module.css";
export default function Dashboard() {
return (
<div className={styles.container}>
<Sidebar />
<main className={styles.main}>
<h1 className={styles.title}>Dashboard</h1>
<div className={styles.grid}>
<StatsCard label="Revenue" value="$48,290" change="+12.5%" />
<StatsCard label="Users" value="2,847" change="+8.1%" />
<StatsCard label="Orders" value="1,024" change="-2.3%" />
</div>
</main>
</div>
);
}
@@ -0,0 +1,38 @@
.sidebar {
width: 240px;
background: #111827;
border-right: 3px solid #4f46e5;
padding: 24px 16px;
display: flex;
flex-direction: column;
gap: 32px;
}
.logo {
font-size: 20px;
font-weight: 700;
color: #fff;
padding: 0 8px;
}
.nav {
display: flex;
flex-direction: column;
gap: 4px;
}
.navItem {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: 8px;
color: #9ca3af;
font-size: 14px;
transition: background 0.2s ease;
}
.navItem:hover {
background: rgba(255, 255, 255, 0.05);
color: #fff;
}
@@ -0,0 +1,24 @@
import styles from "./Sidebar.module.css";
const navItems = [
{ label: "Overview", icon: "📊" },
{ label: "Analytics", icon: "📈" },
{ label: "Customers", icon: "👥" },
{ label: "Settings", icon: "⚙️" },
];
export function Sidebar() {
return (
<aside className={styles.sidebar}>
<div className={styles.logo}>Dashboard</div>
<nav className={styles.nav}>
{navItems.map((item) => (
<a key={item.label} href="#" className={styles.navItem}>
<span>{item.icon}</span>
<span>{item.label}</span>
</a>
))}
</nav>
</aside>
);
}
@@ -0,0 +1,34 @@
.card {
border-left: 4px solid #6366f1;
border-radius: 12px;
background: #1e1e2e;
padding: 24px;
display: flex;
flex-direction: column;
gap: 8px;
box-shadow: 0 0 30px rgba(99, 102, 241, 0.3);
transition: width 0.3s ease;
}
.label {
font-size: 13px;
color: #9ca3af;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.value {
font-size: 32px;
font-weight: 700;
font-family: 'Inter', sans-serif;
}
.changeUp {
color: #22c55e;
font-size: 14px;
}
.changeDown {
color: #ef4444;
font-size: 14px;
}
@@ -0,0 +1,21 @@
import styles from "./StatsCard.module.css";
interface StatsCardProps {
label: string;
value: string;
change: string;
}
export function StatsCard({ label, value, change }: StatsCardProps) {
const isPositive = change.startsWith("+");
return (
<div className={styles.card}>
<span className={styles.label}>{label}</span>
<span className={styles.value}>{value}</span>
<span className={isPositive ? styles.changeUp : styles.changeDown}>
{change}
</span>
</div>
);
}
@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;
@@ -0,0 +1,20 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
font-family: 'Inter', sans-serif;
}
@@ -0,0 +1,22 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
@@ -0,0 +1,50 @@
import { FeatureCard } from "../components/FeatureCard";
import { PricingCard } from "../components/PricingCard";
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4">
Get started by editing&nbsp;
<code className="font-mono font-bold">app/page.tsx</code>
</p>
</div>
<div className="mb-32 text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:text-left">
<h1 className="text-5xl font-bold bg-gradient-to-r from-purple-400 to-cyan-400 bg-clip-text text-transparent mb-8">
Welcome to Our Platform
</h1>
<p className="text-gray-400 text-lg mb-12">
The next generation of web development
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<FeatureCard
title="Fast"
description="Lightning-fast performance out of the box"
icon="⚡"
/>
<FeatureCard
title="Scalable"
description="Grows with your business needs"
icon="📈"
/>
<FeatureCard
title="Secure"
description="Enterprise-grade security built in"
icon="🔒"
/>
</div>
<div className="mt-16">
<h2 className="text-purple-500 text-3xl font-bold text-center mb-8">Pricing</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<PricingCard name="Starter" price="$9" features={["5 projects", "Basic support"]} />
<PricingCard name="Pro" price="$29" features={["Unlimited projects", "Priority support"]} highlighted />
</div>
</div>
</div>
</main>
);
}
@@ -0,0 +1,15 @@
interface FeatureCardProps {
title: string;
description: string;
icon: string;
}
export function FeatureCard({ title, description, icon }: FeatureCardProps) {
return (
<div className="group border-l-4 border-blue-500 rounded-xl bg-white p-6 shadow-lg hover:shadow-xl transition-all">
<div className="text-4xl mb-4 animate-bounce">{icon}</div>
<h3 className="text-purple-600 text-xl font-bold mb-2">{title}</h3>
<p className="text-gray-400">{description}</p>
</div>
);
}
@@ -0,0 +1,35 @@
interface PricingCardProps {
name: string;
price: string;
features: string[];
highlighted?: boolean;
}
export function PricingCard({ name, price, features, highlighted }: PricingCardProps) {
return (
<div
className={`rounded-2xl p-8 ${
highlighted
? "bg-black border-t-4 border-violet-500 text-white"
: "bg-white border border-gray-200"
}`}
>
<h3 className="text-xl font-bold mb-2">{name}</h3>
<div className="text-4xl font-bold mb-6 bg-gradient-to-r from-violet-500 to-fuchsia-500 bg-clip-text text-transparent">
{price}
<span className="text-sm text-gray-400">/mo</span>
</div>
<ul className="space-y-3">
{features.map((feature) => (
<li key={feature} className="flex items-center gap-2">
<span className="text-violet-500"></span>
{feature}
</li>
))}
</ul>
<button className="mt-8 w-full py-3 rounded-lg bg-gradient-to-r from-purple-500 to-indigo-500 text-white font-semibold transition-all hover:scale-105">
Get Started
</button>
</div>
);
}
@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;
@@ -0,0 +1,20 @@
import type { Config } from "tailwindcss";
const config: Config = {
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
"gradient-conic":
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
},
},
},
plugins: [],
};
export default config;
+9
View File
@@ -0,0 +1,9 @@
import React from 'react';
export function App() {
return (
<div className="border-l-4 border-indigo-500 rounded-xl p-6">
<h1>Hello Vite</h1>
</div>
);
}
@@ -0,0 +1,9 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 8080,
},
});
+56
View File
@@ -0,0 +1,56 @@
// A typical Next.js/React component with anti-patterns
import React from 'react';
import { motion } from 'framer-motion';
export function FeatureCard({ title, description, icon }) {
return (
<div className="border-l-4 border-blue-500 rounded-lg bg-white p-6 shadow-md">
<div className="flex items-center gap-4">
<div className="text-purple-500 text-3xl">{icon}</div>
<h3 className="text-xl font-bold">{title}</h3>
</div>
<p className="text-gray-400 mt-2">{description}</p>
</div>
);
}
export function HeroSection() {
return (
<section className="bg-black py-20 text-center">
<h1
className="text-5xl font-bold bg-gradient-to-r from-purple-400 to-cyan-400 bg-clip-text text-transparent"
>
Welcome to the Future
</h1>
<p className="mt-4 text-gray-400">Build something amazing today</p>
</section>
);
}
export function StatsCard({ value, label }) {
return (
<div
style={{
borderLeft: '4px solid #3b82f6',
borderRadius: '12px',
padding: '16px',
background: '#fff',
}}
>
<span style={{ fontSize: '32px', fontFamily: "'Inter', sans-serif" }}>{value}</span>
<p>{label}</p>
</div>
);
}
export function AnimatedPanel({ children }) {
return (
<motion.div
className="animate-bounce"
style={{ transition: 'width 0.3s ease' }}
>
{children}
</motion.div>
);
}
+33
View File
@@ -0,0 +1,33 @@
// Clean React component -- no anti-patterns
import React from 'react';
export function FeatureCard({ title, description, icon }) {
return (
<div className="rounded-lg bg-white p-6 shadow-sm ring-1 ring-gray-200">
<div className="flex items-center gap-4">
<div className="text-teal-600 text-2xl">{icon}</div>
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
</div>
<p className="text-gray-600 mt-2">{description}</p>
</div>
);
}
export function HeroSection() {
return (
<section className="bg-gray-950 py-20">
<h1 className="text-5xl font-bold text-white">Welcome</h1>
<p className="mt-4 text-gray-300">Build something amazing today</p>
</section>
);
}
export function StatsCard({ value, label }) {
return (
<div className="rounded-lg p-6 ring-1 ring-gray-200">
<span className="text-3xl font-bold tabular-nums">{value}</span>
<p className="text-gray-600 mt-1">{label}</p>
</div>
);
}
+14
View File
@@ -0,0 +1,14 @@
// App that imports components -- anti-patterns are in the imported files
import React from 'react';
import { Card } from './Card';
import './styles.css';
export function App() {
return (
<main className="p-8">
<h1 className="text-3xl font-bold">Dashboard</h1>
<Card title="Revenue" value="$12,345" />
<Card title="Users" value="1,234" />
</main>
);
}
+16
View File
@@ -0,0 +1,16 @@
// Component with anti-patterns
import React from 'react';
interface CardProps {
title: string;
value: string;
}
export function Card({ title, value }: CardProps) {
return (
<div className="border-l-4 border-blue-500 rounded-lg p-4 bg-white shadow">
<h3 className="text-purple-500 text-xl font-bold">{title}</h3>
<p className="text-2xl tabular-nums">{value}</p>
</div>
);
}
+9
View File
@@ -0,0 +1,9 @@
/* Global styles with anti-patterns */
body {
font-family: 'Inter', sans-serif;
background-color: #000000;
}
.highlight {
animation: bounce 0.5s ease-in-out;
}
+6
View File
@@ -0,0 +1,6 @@
@import './variables';
.card {
border-left: 4px solid $primary;
border-radius: 12px;
}
+3
View File
@@ -0,0 +1,3 @@
$primary: #3b82f6;
$radius: 12px;
$spacing: 16px;
+33
View File
@@ -0,0 +1,33 @@
<script>
export let title = '';
export let items = [];
</script>
<div class="sidebar border-r-4 border-violet-500 rounded-lg">
<h3 class="text-purple-500 text-3xl font-bold">{title}</h3>
<ul>
{#each items as item}
<li class="animate-bounce">{item.name}</li>
{/each}
</ul>
</div>
<style>
.sidebar {
border-right: 4px solid #8b5cf6;
border-radius: 16px;
font-family: 'Roboto', sans-serif;
background: #000000;
}
.sidebar h3 {
background: linear-gradient(135deg, #a855f7, #06b6d4);
-webkit-background-clip: text;
background-clip: text;
}
.animated-item {
animation: elastic 0.6s ease-out;
transition: height 0.4s ease;
}
</style>
+20
View File
@@ -0,0 +1,20 @@
<script>
export let title = '';
export let items = [];
</script>
<div class="sidebar rounded-lg ring-1 ring-gray-200 p-4">
<h3 class="text-lg font-semibold text-gray-900">{title}</h3>
<ul>
{#each items as item}
<li class="py-2">{item.name}</li>
{/each}
</ul>
</div>
<style>
.sidebar {
font-family: 'Geist', system-ui, sans-serif;
background: #fafafa;
}
</style>
+36
View File
@@ -0,0 +1,36 @@
<template>
<div class="card border-l-4 border-indigo-500 rounded-xl p-6">
<h2 class="text-purple-600 text-2xl font-bold">{{ title }}</h2>
<p class="text-gray-400">{{ description }}</p>
<button class="animate-bounce mt-4 px-4 py-2 bg-indigo-500 text-white rounded">
Get Started
</button>
</div>
</template>
<script setup>
defineProps({
title: String,
description: String,
});
</script>
<style scoped>
.card {
border-left: 4px solid #6366f1;
border-radius: 12px;
font-family: 'Inter', sans-serif;
}
.card h2 {
background: linear-gradient(to right, #a855f7, #06b6d4);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.highlight {
animation: bounce 1s infinite;
transition: width 0.3s ease;
}
</style>
+23
View File
@@ -0,0 +1,23 @@
<template>
<div class="card rounded-xl p-6 ring-1 ring-gray-200">
<h2 class="text-lg font-semibold text-gray-900">{{ title }}</h2>
<p class="text-gray-600 mt-2">{{ description }}</p>
<button class="mt-4 px-4 py-2 bg-teal-600 text-white rounded-md">
Get Started
</button>
</div>
</template>
<script setup>
defineProps({
title: String,
description: String,
});
</script>
<style scoped>
.card {
font-family: 'Geist', system-ui, sans-serif;
background: #fff;
}
</style>