Skip to content

Instantly share code, notes, and snippets.

@yuheiy
Last active July 10, 2025 04:25
Show Gist options
  • Select an option

  • Save yuheiy/f78b92ce60c1aa931ac2edc4e3260aa8 to your computer and use it in GitHub Desktop.

Select an option

Save yuheiy/f78b92ce60c1aa931ac2edc4e3260aa8 to your computer and use it in GitHub Desktop.
Tailwind CSS Border-Width Detector

Tailwind CSS Border-Width Detector

A Node.js CLI tool to detect and list Tailwind CSS border-width related classes in your project files with their exact locations.

Features

  • πŸ” Detects all border-width related Tailwind CSS classes
  • πŸ“ Shows exact file paths and line numbers
  • 🎯 Supports multiple file formats (HTML, JSX, Vue, PHP, etc.)
  • ⚑ Fast scanning with built-in file filtering
  • πŸ“Š Provides summary statistics
  • πŸ› οΈ Configurable file extensions and ignore patterns

Supported Border-Width Classes

The tool detects the following Tailwind CSS border-width classes, including those with variants:

Generic Border-Width

  • border - Sets border-width to 1px
  • border-0 - Sets border-width to 0
  • border-2 - Sets border-width to 2px
  • border-4 - Sets border-width to 4px
  • border-8 - Sets border-width to 8px

Directional Border-Width

  • border-x, border-x-0, border-x-2, border-x-4, border-x-8 - Horizontal borders
  • border-y, border-y-0, border-y-2, border-y-4, border-y-8 - Vertical borders
  • border-t, border-t-0, border-t-2, border-t-4, border-t-8 - Top border
  • border-r, border-r-0, border-r-2, border-r-4, border-r-8 - Right border
  • border-b, border-b-0, border-b-2, border-b-4, border-b-8 - Bottom border
  • border-l, border-l-0, border-l-2, border-l-4, border-l-8 - Left border

Arbitrary Values

  • border-[3px], border-t-[1rem], border-x-[2.5px] - Custom border-width values

With Variants

The tool also detects all border-width classes when used with Tailwind variants:

Responsive Variants

  • sm:border-2, md:border-4, lg:border-8, xl:border-0, 2xl:border-t-4

Hover/Focus/Active States

  • hover:border-2, focus:border-4, active:border-0
  • hover:border-t-8, focus:border-x-[3px]

Dark Mode

  • dark:border-0, dark:border-[2px], dark:hover:border-4

Complex Variants (Multiple Combined)

  • lg:hover:border-2
  • xl:focus:border-t-8
  • 2xl:hover:focus:border-[3px]
  • dark:md:hover:border-b-4

The detector recognizes any combination of variants with border-width classes.

Installation

No installation required. Just download the script and run it with Node.js.

# Navigate to the tool directory
cd tools/border-width-detector

# Make executable
chmod +x border-width-detector.js

# Run directly
node border-width-detector.js

Usage

Basic Usage

# Scan current directory (from project root)
node tools/border-width-detector/border-width-detector.js

# Or from within the tool directory
cd tools/border-width-detector
node border-width-detector.js ../../

# Scan specific directory
node border-width-detector.js ../../src

# Show help
node border-width-detector.js --help

Advanced Options

# Scan only specific file types
node border-width-detector.js ../../src --ext js,jsx,ts,tsx

# Custom ignore patterns
node border-width-detector.js ../../ --ignore node_modules/**,dist/**,coverage/**

# Combined options
node border-width-detector.js ../../app --ext html,js,vue --ignore tests/**,*.test.js

Command Line Options

Option Description Default
--help, -h Show help message -
--ext <extensions> File extensions to scan (comma-separated) html,htm,js,jsx,ts,tsx,vue,svelte,php,erb,haml,slim,pug,twig,razor,blade.php
--ignore <patterns> Ignore patterns (comma-separated) node_modules/**,dist/**,build/**,.git/**

Example Output

πŸ” Scanning for border-width classes in: /path/to/project
πŸ“„ File extensions: html, js, jsx, ts, tsx
🚫 Ignore patterns: node_modules/**, dist/**, build/**, .git/**

Found 8 border-width class usage(s):

πŸ“ src/components/Button.jsx
  12:border-2 - <button className="px-4 py-2 border-2 border-blue-500">
  24:hover:border-0 - <button className="hover:border-0 bg-transparent">

πŸ“ src/pages/Home.tsx  
  45:md:border-t-4 - <div className="md:border-t-4 border-gray-200">
  67:lg:hover:border-x-[3px] - <section className="lg:hover:border-x-[3px] border-red-500">

πŸ“ public/index.html
  89:border - <div class="border rounded-lg p-4">
  102:dark:border-2 - <div class="dark:border-2 bg-gray-800">
  115:xl:focus:border-b-8 - <input class="xl:focus:border-b-8">

πŸ“ src/utils/styles.js
  23:2xl:hover:border-[5px] - const dynamicClass = "2xl:hover:border-[5px]";

πŸ“Š Summary by class:
  2xl:hover:border-[5px]: 1 usage(s)
  border: 1 usage(s)
  border-2: 1 usage(s)
  dark:border-2: 1 usage(s)
  hover:border-0: 1 usage(s)
  lg:hover:border-x-[3px]: 1 usage(s)
  md:border-t-4: 1 usage(s)
  xl:focus:border-b-8: 1 usage(s)

How It Works

The tool uses the extraction patterns inspired by Tailwind CSS's oxide engine:

  1. Class Extraction: Scans files for class attributes, className props, template literals, and arrays
  2. Pattern Matching: Uses precise regular expressions to identify only border-width classes
  3. Line Detection: Finds exact line numbers where classes are used
  4. Result Formatting: Groups results by file and provides summary statistics

Supported File Types

  • HTML: .html, .htm
  • JavaScript: .js, .jsx
  • TypeScript: .ts, .tsx
  • Vue: .vue
  • Svelte: .svelte
  • PHP: .php, .blade.php
  • Ruby: .erb, .haml, .slim
  • Template Engines: .pug, .twig
  • ASP.NET: .razor

Testing

Run the included test suite:

node test-border-detector.js

The tests verify:

  • Pattern matching accuracy
  • Class extraction from various syntaxes
  • File scanning functionality
  • Line number detection

Programmatic Usage

You can also use the detector programmatically:

const { BorderWidthDetector } = require('./border-width-detector');

const detector = new BorderWidthDetector({
  extensions: ['js', 'jsx', 'ts', 'tsx'],
  ignorePatterns: ['node_modules/**', 'dist/**']
});

// Scan a directory
const results = detector.scanDirectory('./src');

// Scan a single file
const fileResults = detector.scanFile('./src/component.jsx');

// Format results
console.log(detector.formatResults(results));

License

MIT

#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
// border-width related class patterns
const BORDER_WIDTH_PATTERNS = [
// Generic border-width: border, border-0, border-2, border-4, border-8
// With variants: hover:border, md:border-2, lg:hover:border-4, etc.
/^([^:]+:)*border(-[0248])?$/,
// Directional border-width: border-x, border-x-0, border-y-2, etc.
// With variants: hover:border-x, md:border-t-4, lg:hover:border-l-8, etc.
/^([^:]+:)*border-[xytrbl](-[0248])?$/,
// Arbitrary values: border-[3px], border-t-[1px], etc.
// With variants: hover:border-[3px], md:border-t-[1rem], etc.
/^([^:]+:)*border(-[xytrbl])?-\[[^\]]+\]$/
];
class BorderWidthDetector {
constructor(options = {}) {
this.extensions = options.extensions || [
'html', 'htm', 'js', 'jsx', 'ts', 'tsx', 'vue', 'svelte', 'php',
'erb', 'haml', 'slim', 'pug', 'twig', 'razor', 'blade.php'
];
this.ignorePatterns = options.ignorePatterns || [
'node_modules/**',
'dist/**',
'build/**',
'.git/**'
];
}
extractClasses(content) {
const classes = new Set();
// Extract from class attributes
const classAttrRegex = /class\s*=\s*["'`]([^"'`]*?)["'`]/g;
let match;
while ((match = classAttrRegex.exec(content)) !== null) {
const classStr = match[1];
const classNames = classStr.split(/\s+/).filter(Boolean);
classNames.forEach(cls => classes.add(cls));
}
// Extract from className attributes (React/JSX)
const classNameRegex = /className\s*=\s*["'`]([^"'`]*?)["'`]/g;
while ((match = classNameRegex.exec(content)) !== null) {
const classStr = match[1];
const classNames = classStr.split(/\s+/).filter(Boolean);
classNames.forEach(cls => classes.add(cls));
}
// Extract from template literals and object syntax
const templateLiteralRegex = /`[^`]*`/g;
while ((match = templateLiteralRegex.exec(content)) !== null) {
const templateContent = match[0];
const classNames = templateContent.match(/[\w-]+/g) || [];
classNames.forEach(cls => classes.add(cls));
}
// Extract from array syntax ['class1', 'class2']
const arrayRegex = /\[[\s\S]*?\]/g;
while ((match = arrayRegex.exec(content)) !== null) {
const arrayContent = match[0];
const stringRegex = /["'`]([^"'`]+)["'`]/g;
let stringMatch;
while ((stringMatch = stringRegex.exec(arrayContent)) !== null) {
const classNames = stringMatch[1].split(/\s+/).filter(Boolean);
classNames.forEach(cls => classes.add(cls));
}
}
return Array.from(classes);
}
isBorderWidthClass(className) {
return BORDER_WIDTH_PATTERNS.some(pattern => pattern.test(className));
}
scanFile(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split('\n');
const allClasses = this.extractClasses(content);
const borderWidthClasses = allClasses.filter(cls => this.isBorderWidthClass(cls));
if (borderWidthClasses.length === 0) {
return [];
}
const results = [];
// Find line numbers and column positions for each border-width class
borderWidthClasses.forEach(className => {
lines.forEach((line, index) => {
let searchIndex = 0;
let position;
// Find all occurrences of the className in the line
while ((position = line.indexOf(className, searchIndex)) !== -1) {
// Verify this is a complete class name, not part of another word
const beforeChar = position > 0 ? line[position - 1] : ' ';
const afterChar = position + className.length < line.length ? line[position + className.length] : ' ';
// Check if surrounded by whitespace, quotes, or word boundaries
if (/[\s"'`\[\],]/.test(beforeChar) && /[\s"'`\[\],]/.test(afterChar)) {
results.push({
file: filePath,
line: index + 1,
column: position + 1, // 1-based column indexing
className: className,
lineContent: line.trim()
});
}
searchIndex = position + 1;
}
});
});
return results;
} catch (error) {
console.error(`Error reading file ${filePath}:`, error.message);
return [];
}
}
scanDirectory(dir) {
const allResults = [];
const scanRecursive = (currentDir) => {
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
const relativePath = path.relative(dir, fullPath);
// Check if path should be ignored
const shouldIgnore = this.ignorePatterns.some(pattern => {
const cleanPattern = pattern.replace(/\*\*/g, '').replace(/\*/g, '');
return relativePath.includes(cleanPattern) || entry.name.includes(cleanPattern);
});
if (shouldIgnore) continue;
if (entry.isDirectory()) {
scanRecursive(fullPath);
} else if (entry.isFile()) {
const ext = path.extname(entry.name).slice(1);
if (this.extensions.includes(ext) ||
(entry.name.includes('.blade.php') && this.extensions.includes('blade.php'))) {
try {
const results = this.scanFile(fullPath);
if (Array.isArray(results)) {
allResults.push(...results);
}
} catch (error) {
console.error(`Error scanning file ${fullPath}:`, error.message);
}
}
}
}
};
scanRecursive(dir);
return allResults;
}
formatResults(results) {
if (results.length === 0) {
return 'No border-width classes found.';
}
const output = [];
output.push(`Found ${results.length} border-width class usage(s):\n`);
// Group by file
const fileGroups = {};
results.forEach(result => {
if (!fileGroups[result.file]) {
fileGroups[result.file] = [];
}
fileGroups[result.file].push(result);
});
Object.keys(fileGroups).sort().forEach(file => {
output.push(`πŸ“ ${file}`);
fileGroups[file].forEach(result => {
output.push(` ${file}:${result.line}:${result.column} - ${result.className} - ${result.lineContent}`);
});
output.push('');
});
// Summary by class name
const classSummary = {};
results.forEach(result => {
if (!classSummary[result.className]) {
classSummary[result.className] = 0;
}
classSummary[result.className]++;
});
output.push('πŸ“Š Summary by class:');
Object.keys(classSummary).sort().forEach(className => {
output.push(` ${className}: ${classSummary[className]} usage(s)`);
});
return output.join('\n');
}
}
// CLI interface
function main() {
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log(`
Border-Width Class Detector
Usage: node border-width-detector.js [directory] [options]
Options:
--help, -h Show this help message
--ext <extensions> File extensions to scan (comma-separated)
Default: html,htm,js,jsx,ts,tsx,vue,svelte,php,erb,haml,slim,pug,twig,razor,blade.php
--ignore <patterns> Ignore patterns (comma-separated)
Default: node_modules/**,dist/**,build/**,.git/**
Examples:
node border-width-detector.js
node border-width-detector.js ./src
node border-width-detector.js ./src --ext js,jsx,ts,tsx
node border-width-detector.js . --ignore node_modules/**,dist/**
`);
return;
}
const directory = args[0] || process.cwd();
// Parse options
const options = {};
for (let i = 1; i < args.length; i++) {
if (args[i] === '--ext' && args[i + 1]) {
options.extensions = args[i + 1].split(',');
i++;
} else if (args[i] === '--ignore' && args[i + 1]) {
options.ignorePatterns = args[i + 1].split(',');
i++;
}
}
if (!fs.existsSync(directory)) {
console.error(`Error: Directory "${directory}" does not exist.`);
process.exit(1);
}
const detector = new BorderWidthDetector(options);
console.log(`πŸ” Scanning for border-width classes in: ${path.resolve(directory)}`);
console.log(`πŸ“„ File extensions: ${detector.extensions.join(', ')}`);
console.log(`🚫 Ignore patterns: ${detector.ignorePatterns.join(', ')}\n`);
try {
const results = detector.scanDirectory(directory);
console.log(detector.formatResults(results));
} catch (error) {
console.error('Error during scanning:', error.message);
process.exit(1);
}
}
if (require.main === module) {
main();
}
module.exports = { BorderWidthDetector, BORDER_WIDTH_PATTERNS };
{
"name": "tailwind-border-width-detector",
"version": "1.0.0",
"description": "CLI tool to detect Tailwind CSS border-width classes in project files",
"main": "index.js",
"bin": {
"border-width-detector": "./index.js"
},
"scripts": {
"test": "node test.js"
},
"dependencies": {},
"keywords": [
"tailwindcss",
"border-width",
"detector",
"cli",
"static-analysis"
],
"author": "",
"license": "MIT"
}
const fs = require('fs');
const path = require('path');
const { BorderWidthDetector, BORDER_WIDTH_PATTERNS } = require('./index');
// Test cases
const testCases = [
{
name: 'HTML with class attribute',
content: `<div class="border-2 p-4 bg-blue-500">Content</div>`,
expected: ['border-2']
},
{
name: 'React JSX with className',
content: `<div className="border-t-4 border-b-0 flex">Content</div>`,
expected: ['border-t-4', 'border-b-0']
},
{
name: 'Multiple border classes',
content: `<div class="border border-x-2 border-y-4 border-l-8">Content</div>`,
expected: ['border', 'border-x-2', 'border-y-4', 'border-l-8']
},
{
name: 'Arbitrary values',
content: `<div class="border-[3px] border-t-[1px]">Content</div>`,
expected: ['border-[3px]', 'border-t-[1px]']
},
{
name: 'No border classes',
content: `<div class="p-4 bg-red-500 text-white">Content</div>`,
expected: []
},
{
name: 'Template literal',
content: `const classes = \`border-2 \${condition ? 'border-r-4' : 'border-l-0'}\`;`,
expected: ['border-2', 'border-r-4', 'border-l-0']
},
{
name: 'Array syntax',
content: `const classes = ['border', 'border-t-2', 'p-4'];`,
expected: ['border', 'border-t-2']
},
{
name: 'Hover variants',
content: `<div class="hover:border-2 hover:border-t-4">Content</div>`,
expected: ['hover:border-2', 'hover:border-t-4']
},
{
name: 'Responsive variants',
content: `<div class="md:border lg:border-4 xl:border-b-8">Content</div>`,
expected: ['md:border', 'lg:border-4', 'xl:border-b-8']
},
{
name: 'Complex variants',
content: `<div class="lg:hover:border-2 2xl:focus:border-t-[3px]">Content</div>`,
expected: ['lg:hover:border-2', '2xl:focus:border-t-[3px]']
},
{
name: 'Mixed variants and non-variants',
content: `<div class="border hover:border-4 p-4 md:border-x-2">Content</div>`,
expected: ['border', 'hover:border-4', 'md:border-x-2']
},
{
name: 'Dark mode variants',
content: `<div class="dark:border-0 dark:hover:border-2">Content</div>`,
expected: ['dark:border-0', 'dark:hover:border-2']
}
];
function testPatterns() {
console.log('πŸ§ͺ Testing border-width patterns...\n');
const testClasses = [
// Should match - basic classes
{ class: 'border', shouldMatch: true },
{ class: 'border-0', shouldMatch: true },
{ class: 'border-2', shouldMatch: true },
{ class: 'border-4', shouldMatch: true },
{ class: 'border-8', shouldMatch: true },
{ class: 'border-x', shouldMatch: true },
{ class: 'border-x-0', shouldMatch: true },
{ class: 'border-x-2', shouldMatch: true },
{ class: 'border-y-4', shouldMatch: true },
{ class: 'border-t-8', shouldMatch: true },
{ class: 'border-r-2', shouldMatch: true },
{ class: 'border-b-4', shouldMatch: true },
{ class: 'border-l-0', shouldMatch: true },
{ class: 'border-[3px]', shouldMatch: true },
{ class: 'border-t-[1px]', shouldMatch: true },
{ class: 'border-x-[2rem]', shouldMatch: true },
// Should match - with variants
{ class: 'hover:border', shouldMatch: true },
{ class: 'hover:border-2', shouldMatch: true },
{ class: 'md:border-4', shouldMatch: true },
{ class: 'lg:border-8', shouldMatch: true },
{ class: 'hover:border-x', shouldMatch: true },
{ class: 'md:border-t-4', shouldMatch: true },
{ class: 'lg:hover:border-2', shouldMatch: true },
{ class: 'xl:focus:border-b-8', shouldMatch: true },
{ class: 'dark:border-0', shouldMatch: true },
{ class: 'sm:hover:border-l-[3px]', shouldMatch: true },
{ class: 'md:focus:border-[1rem]', shouldMatch: true },
{ class: '2xl:hover:focus:border-t-2', shouldMatch: true },
// Should not match
{ class: 'border-red-500', shouldMatch: false },
{ class: 'border-solid', shouldMatch: false },
{ class: 'border-dashed', shouldMatch: false },
{ class: 'border-dotted', shouldMatch: false },
{ class: 'rounded-border', shouldMatch: false },
{ class: 'p-4', shouldMatch: false },
{ class: 'bg-border-2', shouldMatch: false },
{ class: 'hover:border-red-500', shouldMatch: false },
{ class: 'md:border-solid', shouldMatch: false },
{ class: 'lg:border-opacity-50', shouldMatch: false }
];
let passed = 0;
let failed = 0;
testClasses.forEach(({ class: className, shouldMatch }) => {
const matches = BORDER_WIDTH_PATTERNS.some(pattern => pattern.test(className));
if (matches === shouldMatch) {
console.log(`βœ… ${className} - ${shouldMatch ? 'matches' : 'does not match'}`);
passed++;
} else {
console.log(`❌ ${className} - expected ${shouldMatch ? 'match' : 'no match'}, got ${matches ? 'match' : 'no match'}`);
failed++;
}
});
console.log(`\nPattern tests: ${passed} passed, ${failed} failed\n`);
return failed === 0;
}
function testExtraction() {
console.log('πŸ§ͺ Testing class extraction...\n');
const detector = new BorderWidthDetector();
let passed = 0;
let failed = 0;
testCases.forEach(({ name, content, expected }) => {
const extractedClasses = detector.extractClasses(content);
const borderWidthClasses = extractedClasses.filter(cls => detector.isBorderWidthClass(cls));
// Sort arrays for comparison
const sortedExpected = [...expected].sort();
const sortedActual = [...borderWidthClasses].sort();
const isEqual = JSON.stringify(sortedExpected) === JSON.stringify(sortedActual);
if (isEqual) {
console.log(`βœ… ${name}: ${borderWidthClasses.join(', ') || 'none'}`);
passed++;
} else {
console.log(`❌ ${name}:`);
console.log(` Expected: ${expected.join(', ') || 'none'}`);
console.log(` Actual: ${borderWidthClasses.join(', ') || 'none'}`);
failed++;
}
});
console.log(`\nExtraction tests: ${passed} passed, ${failed} failed\n`);
return failed === 0;
}
async function testFileScanning() {
console.log('πŸ§ͺ Testing file scanning...\n');
const testDir = path.join(__dirname, 'test-files');
const testFile = path.join(testDir, 'test.html');
// Create test directory and file
if (!fs.existsSync(testDir)) {
fs.mkdirSync(testDir);
}
const testContent = `
<html>
<head><title>Test</title></head>
<body>
<div class="border-2 p-4">
<h1 class="border-b-4 text-xl">Title</h1>
<p class="border-l-0">Paragraph</p>
</div>
<div className="border-x-8 flex">
React component
</div>
</body>
</html>
`;
fs.writeFileSync(testFile, testContent);
try {
const detector = new BorderWidthDetector();
const results = await detector.scanFile(testFile);
const expectedClasses = ['border-2', 'border-b-4', 'border-l-0', 'border-x-8'];
const actualClasses = [...new Set(results.map(r => r.className))];
const isEqual = JSON.stringify(expectedClasses.sort()) === JSON.stringify(actualClasses.sort());
if (isEqual) {
console.log(`βœ… File scanning: found ${actualClasses.join(', ')}`);
// Test line numbers
const hasValidLineNumbers = results.every(r => r.line > 0);
if (hasValidLineNumbers) {
console.log(`βœ… Line numbers: all results have valid line numbers`);
} else {
console.log(`❌ Line numbers: some results have invalid line numbers`);
}
return true;
} else {
console.log(`❌ File scanning:`);
console.log(` Expected: ${expectedClasses.join(', ')}`);
console.log(` Actual: ${actualClasses.join(', ')}`);
return false;
}
} finally {
// Cleanup
if (fs.existsSync(testFile)) {
fs.unlinkSync(testFile);
}
if (fs.existsSync(testDir)) {
fs.rmdirSync(testDir);
}
}
}
async function runTests() {
console.log('πŸš€ Running Border Width Detector Tests\n');
const results = [
testPatterns(),
testExtraction(),
await testFileScanning()
];
const allPassed = results.every(result => result);
console.log('\n' + '='.repeat(50));
if (allPassed) {
console.log('πŸŽ‰ All tests passed!');
process.exit(0);
} else {
console.log('πŸ’₯ Some tests failed!');
process.exit(1);
}
}
if (require.main === module) {
runTests().catch(console.error);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment