mirror of
https://github.com/nocodb/nocodb.git
synced 2026-05-02 00:07:04 +00:00
824 lines
27 KiB
JavaScript
824 lines
27 KiB
JavaScript
const fs = require('fs')
|
|
const glob = require('glob')
|
|
|
|
/**
|
|
* @example
|
|
* ```zsh
|
|
* cd packages/nc-gui
|
|
* node migrate-colors.js convert ./components/dlg/
|
|
* ```
|
|
*/
|
|
|
|
// Define conflict resolution strategies
|
|
const CONFLICT_STRATEGIES = {
|
|
ASK_USER: 'ask_user',
|
|
MOST_COMMON: 'most_common',
|
|
CONTEXT_AWARE: 'context_aware',
|
|
PRIORITY_BASED: 'priority_based',
|
|
}
|
|
|
|
// Multiple mapping definitions with conflict resolution
|
|
const colorMappingConflicts = {
|
|
'text-gray-700': [
|
|
{
|
|
token: 'text-nc-content-gray-subtle',
|
|
priority: 70,
|
|
context: ['body', 'paragraph', 'description', 'secondary', 'content'],
|
|
usage: 'Primary choice for subtle text content',
|
|
},
|
|
{
|
|
token: 'text-nc-content-inverted-secondary',
|
|
priority: 75,
|
|
context: ['inverted', 'dark', 'overlay', 'modal', 'bg-black', 'bg-gray-900'],
|
|
usage: 'For text on dark/inverted backgrounds',
|
|
},
|
|
],
|
|
'text-gray-500': [
|
|
{
|
|
token: 'text-nc-content-gray-muted',
|
|
priority: 50,
|
|
context: ['muted', 'disabled', 'placeholder', 'hint', 'caption', 'GeneralIcon'],
|
|
usage: 'Primary choice for muted text',
|
|
},
|
|
{
|
|
token: 'text-nc-content-inverted-primary-disabled',
|
|
priority: 40,
|
|
context: ['inverted', 'disabled', 'dark'],
|
|
usage: 'For disabled text on inverted backgrounds',
|
|
},
|
|
{
|
|
token: 'text-nc-content-inverted-secondary-disabled',
|
|
priority: 40,
|
|
context: ['inverted', 'disabled', 'secondary', 'dark'],
|
|
usage: 'For disabled secondary text on inverted backgrounds',
|
|
},
|
|
],
|
|
'text-white': [
|
|
{
|
|
token: 'text-nc-content-inverted-primary',
|
|
priority: 85,
|
|
context: ['inverted', 'dark', 'overlay', 'primary', 'bg-black', 'bg-gray'],
|
|
usage: 'Primary choice for white text',
|
|
},
|
|
{
|
|
token: 'text-nc-content-inverted-primary-hover',
|
|
priority: 80,
|
|
context: ['inverted', 'hover', 'dark'],
|
|
usage: 'For hover states on inverted backgrounds',
|
|
},
|
|
],
|
|
}
|
|
|
|
// Fill color conflicts
|
|
const fillColorConflicts = {
|
|
'fill-gray-300': [
|
|
{
|
|
token: 'fill-nc-fill-primary-disabled',
|
|
priority: 60,
|
|
context: ['disabled', 'inactive', 'primary', 'button'],
|
|
usage: 'For disabled primary buttons/icons',
|
|
},
|
|
],
|
|
'fill-white': [
|
|
{
|
|
token: 'fill-nc-fill-secondary',
|
|
priority: 70,
|
|
context: ['secondary', 'outline', 'ghost', 'button'],
|
|
usage: 'For secondary buttons/icons',
|
|
},
|
|
{
|
|
token: 'fill-nc-fill-secondary-disabled',
|
|
priority: 40,
|
|
context: ['secondary', 'disabled', 'button'],
|
|
usage: 'For disabled secondary buttons/icons',
|
|
},
|
|
],
|
|
'fill-gray-50': [
|
|
{
|
|
token: 'fill-nc-fill-secondary-hover',
|
|
priority: 65,
|
|
context: ['secondary', 'hover', 'button'],
|
|
usage: 'For hover state of secondary buttons/icons',
|
|
},
|
|
{
|
|
token: 'fill-nc-fill-warning-disabled',
|
|
priority: 30,
|
|
context: ['warning', 'disabled'],
|
|
usage: 'For disabled warning buttons/icons',
|
|
},
|
|
{
|
|
token: 'fill-nc-fill-success-disabled',
|
|
priority: 30,
|
|
context: ['success', 'disabled'],
|
|
usage: 'For disabled success buttons/icons',
|
|
},
|
|
],
|
|
'fill-red-500': [
|
|
{
|
|
token: 'fill-nc-fill-warning',
|
|
priority: 80,
|
|
context: ['warning', 'danger', 'error', 'destructive', 'delete'],
|
|
usage: 'Primary choice for warning/danger states',
|
|
},
|
|
{
|
|
token: 'fill-nc-fill-red-medium',
|
|
priority: 50,
|
|
context: ['red', 'medium', 'color'],
|
|
usage: 'For general red colored elements',
|
|
},
|
|
],
|
|
'fill-green-500': [
|
|
{
|
|
token: 'fill-nc-fill-success',
|
|
priority: 80,
|
|
context: ['success', 'confirm', 'positive', 'save', 'submit'],
|
|
usage: 'Primary choice for success states',
|
|
},
|
|
{
|
|
token: 'fill-nc-fill-green-medium',
|
|
priority: 50,
|
|
context: ['green', 'medium', 'color'],
|
|
usage: 'For general green colored elements',
|
|
},
|
|
],
|
|
}
|
|
|
|
const colorMappings = {
|
|
// Text colors - definitive mappings
|
|
'text-black': 'text-nc-content-gray-extreme',
|
|
'text-gray-600': 'text-nc-content-gray-subtle2',
|
|
'text-gray-800': 'text-nc-content-gray',
|
|
'text-gray-900': 'text-nc-content-gray-emphasis',
|
|
|
|
// Brand colors
|
|
'text-brand-500': 'text-nc-content-brand',
|
|
'text-brand-600': 'text-nc-content-brand-disabled',
|
|
'text-gray-300': 'text-nc-content-brand-hover',
|
|
|
|
// Color-specific text mappings (no conflicts)
|
|
'text-red-700': 'text-nc-content-red-dark',
|
|
'text-red-500': 'text-nc-content-red-medium',
|
|
'text-red-300': 'text-nc-content-red-light',
|
|
'text-green-700': 'text-nc-content-green-dark',
|
|
'text-green-500': 'text-nc-content-green-medium',
|
|
'text-green-300': 'text-nc-content-green-light',
|
|
'text-yellow-700': 'text-nc-content-yellow-dark',
|
|
'text-yellow-500': 'text-nc-content-yellow-medium',
|
|
'text-yellow-300': 'text-nc-content-yellow-light',
|
|
'text-blue-700': 'text-nc-content-blue-dark',
|
|
'text-blue-500': 'text-nc-content-blue-medium',
|
|
'text-blue-300': 'text-nc-content-blue-light',
|
|
'text-purple-700': 'text-nc-content-purple-dark',
|
|
'text-purple-500': 'text-nc-content-purple-medium',
|
|
'text-purple-300': 'text-nc-content-purple-light',
|
|
'text-pink-700': 'text-nc-content-pink-dark',
|
|
'text-pink-500': 'text-nc-content-pink-medium',
|
|
'text-pink-300': 'text-nc-content-pink-light',
|
|
'text-orange-700': 'text-nc-content-orange-dark',
|
|
'text-orange-500': 'text-nc-content-orange-medium',
|
|
'text-orange-300': 'text-nc-content-orange-light',
|
|
'text-maroon-700': 'text-nc-content-maroon-dark',
|
|
'text-maroon-500': 'text-nc-content-maroon-medium',
|
|
'text-maroon-300': 'text-nc-content-maroon-light',
|
|
|
|
// Background colors (no conflicts in your theme)
|
|
'bg-white': 'bg-nc-bg-default',
|
|
'bg-brand-50': 'bg-nc-bg-brand',
|
|
'bg-gray-50': 'bg-nc-bg-gray-extralight',
|
|
'bg-gray-100': 'bg-nc-bg-gray-light',
|
|
'bg-gray-200': 'bg-nc-bg-gray-medium',
|
|
'bg-gray-300': 'bg-nc-bg-gray-dark',
|
|
'bg-gray-400': 'bg-nc-bg-gray-extradark',
|
|
'bg-red-50': 'bg-nc-bg-red-light',
|
|
'bg-red-100': 'bg-nc-bg-red-dark',
|
|
'bg-green-50': 'bg-nc-bg-green-light',
|
|
'bg-green-100': 'bg-nc-bg-green-dark',
|
|
'bg-yellow-50': 'bg-nc-bg-yellow-light',
|
|
'bg-yellow-100': 'bg-nc-bg-yellow-dark',
|
|
'bg-blue-50': 'bg-nc-bg-blue-light',
|
|
'bg-blue-100': 'bg-nc-bg-blue-dark',
|
|
'bg-purple-50': 'bg-nc-bg-purple-light',
|
|
'bg-purple-100': 'bg-nc-bg-purple-dark',
|
|
'bg-pink-50': 'bg-nc-bg-pink-light',
|
|
'bg-pink-100': 'bg-nc-bg-pink-dark',
|
|
'bg-orange-50': 'bg-nc-bg-orange-light',
|
|
'bg-orange-100': 'bg-nc-bg-orange-dark',
|
|
'bg-maroon-50': 'bg-nc-bg-maroon-light',
|
|
'bg-maroon-100': 'bg-nc-bg-maroon-dark',
|
|
|
|
// Border colors (no conflicts)
|
|
'border-brand-500': 'border-nc-border-brand',
|
|
'border-gray-50': 'border-nc-border-gray-extralight',
|
|
'border-gray-100': 'border-nc-border-gray-light',
|
|
'border-gray-200': 'border-nc-border-gray-medium',
|
|
'border-gray-300': 'border-nc-border-gray-dark',
|
|
'border-gray-400': 'border-nc-border-gray-extradark',
|
|
'border-red-500': 'border-nc-border-red',
|
|
'border-green-500': 'border-nc-border-green',
|
|
'border-yellow-500': 'border-nc-border-yellow',
|
|
'border-blue-500': 'border-nc-border-blue',
|
|
'border-purple-500': 'border-nc-border-purple',
|
|
'border-pink-500': 'border-nc-border-pink',
|
|
'border-orange-500': 'border-nc-border-orange',
|
|
'border-maroon-500': 'border-nc-border-maroon',
|
|
|
|
// Fill colors (non-conflicting)
|
|
'fill-brand-500': 'fill-nc-fill-primary',
|
|
'fill-brand-600': 'fill-nc-fill-primary-hover',
|
|
'fill-brand-200': 'fill-nc-fill-primary-disabled2',
|
|
'fill-red-600': 'fill-nc-fill-warning-hover',
|
|
'fill-green-600': 'fill-nc-fill-success-hover',
|
|
'fill-red-700': 'fill-nc-fill-red-dark',
|
|
'fill-red-300': 'fill-nc-fill-red-light',
|
|
'fill-green-700': 'fill-nc-fill-green-dark',
|
|
'fill-green-300': 'fill-nc-fill-green-light',
|
|
'fill-yellow-700': 'fill-nc-fill-yellow-dark',
|
|
'fill-yellow-500': 'fill-nc-fill-yellow-medium',
|
|
'fill-yellow-300': 'fill-nc-fill-yellow-light',
|
|
'fill-blue-700': 'fill-nc-fill-blue-dark',
|
|
'fill-blue-500': 'fill-nc-fill-blue-medium',
|
|
'fill-blue-300': 'fill-nc-fill-blue-light',
|
|
'fill-purple-700': 'fill-nc-fill-purple-dark',
|
|
'fill-purple-500': 'fill-nc-fill-purple-medium',
|
|
'fill-purple-300': 'fill-nc-fill-purple-light',
|
|
'fill-pink-700': 'fill-nc-fill-pink-dark',
|
|
'fill-pink-500': 'fill-nc-fill-pink-medium',
|
|
'fill-pink-300': 'fill-nc-fill-pink-light',
|
|
'fill-orange-700': 'fill-nc-fill-orange-dark',
|
|
'fill-orange-500': 'fill-nc-fill-orange-medium',
|
|
'fill-orange-300': 'fill-nc-fill-orange-light',
|
|
'fill-maroon-700': 'fill-nc-fill-maroon-dark',
|
|
'fill-maroon-500': 'fill-nc-fill-maroon-medium',
|
|
'fill-maroon-300': 'fill-nc-fill-maroon-light',
|
|
}
|
|
|
|
/**
|
|
* Resolve conflicts using context analysis
|
|
*/
|
|
function resolveConflictWithContext(tailwindClass, conflicts, surroundingCode, strategy = CONFLICT_STRATEGIES.CONTEXT_AWARE) {
|
|
const codeContext = surroundingCode.toLowerCase()
|
|
|
|
switch (strategy) {
|
|
case CONFLICT_STRATEGIES.CONTEXT_AWARE: {
|
|
// Score each option based on context keywords
|
|
const scored = conflicts.map((option) => {
|
|
let score = option.priority
|
|
|
|
// Boost score if context keywords are found
|
|
option.context.forEach((keyword) => {
|
|
if (codeContext.includes(keyword)) {
|
|
score += 20
|
|
}
|
|
})
|
|
|
|
return { ...option, finalScore: score }
|
|
})
|
|
|
|
// Return highest scored option
|
|
return scored.sort((a, b) => b.finalScore - a.finalScore)[0]
|
|
}
|
|
case CONFLICT_STRATEGIES.PRIORITY_BASED:
|
|
return conflicts.sort((a, b) => b.priority - a.priority)[0]
|
|
|
|
case CONFLICT_STRATEGIES.MOST_COMMON:
|
|
// For now, return highest priority (could be enhanced with usage analytics)
|
|
return conflicts.sort((a, b) => b.priority - a.priority)[0]
|
|
|
|
default:
|
|
return conflicts[0]
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all conflicting mappings combined
|
|
*/
|
|
function getAllConflicts() {
|
|
return { ...colorMappingConflicts, ...fillColorConflicts }
|
|
}
|
|
|
|
/**
|
|
* Enhanced conversion with conflict resolution
|
|
*/
|
|
function convertFileToSemanticWithConflicts(filePath, strategy = CONFLICT_STRATEGIES.CONTEXT_AWARE, interactive = false) {
|
|
try {
|
|
let content = fs.readFileSync(filePath, 'utf8')
|
|
let hasChanges = false
|
|
const changes = []
|
|
const conflictChoices = []
|
|
|
|
const allConflicts = getAllConflicts()
|
|
|
|
// First, handle non-conflicting mappings
|
|
const sortedMappings = Object.keys(colorMappings).sort((a, b) => b.length - a.length)
|
|
|
|
sortedMappings.forEach((tailwindClass) => {
|
|
const semanticClass = colorMappings[tailwindClass]
|
|
const classRegex = new RegExp(`\\b${tailwindClass.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g')
|
|
|
|
if (classRegex.test(content)) {
|
|
const beforeContent = content
|
|
content = content.replace(classRegex, semanticClass)
|
|
|
|
if (beforeContent !== content) {
|
|
hasChanges = true
|
|
changes.push(`${tailwindClass} → ${semanticClass}`)
|
|
}
|
|
}
|
|
})
|
|
|
|
// Then handle conflicting mappings
|
|
Object.keys(allConflicts).forEach((tailwindClass) => {
|
|
const conflicts = allConflicts[tailwindClass]
|
|
const classRegex = new RegExp(`\\b${tailwindClass.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g')
|
|
|
|
if (classRegex.test(content)) {
|
|
// Get surrounding context for each match
|
|
const matches = [...content.matchAll(classRegex)]
|
|
|
|
matches.forEach((match) => {
|
|
const matchIndex = match.index
|
|
const contextStart = Math.max(0, matchIndex - 200)
|
|
const contextEnd = Math.min(content.length, matchIndex + 200)
|
|
const surroundingCode = content.slice(contextStart, contextEnd)
|
|
|
|
const resolvedConflict = resolveConflictWithContext(tailwindClass, conflicts, surroundingCode, strategy)
|
|
|
|
conflictChoices.push({
|
|
original: tailwindClass,
|
|
chosen: resolvedConflict.token,
|
|
reason: resolvedConflict.usage,
|
|
context: surroundingCode.trim(),
|
|
alternatives: conflicts.filter((c) => c !== resolvedConflict).map((c) => c.token),
|
|
})
|
|
})
|
|
|
|
// Apply the resolved choice to all instances
|
|
if (matches.length > 0) {
|
|
const firstResolution = resolveConflictWithContext(tailwindClass, conflicts, content, strategy)
|
|
|
|
const beforeContent = content
|
|
content = content.replace(classRegex, firstResolution.token)
|
|
|
|
if (beforeContent !== content) {
|
|
hasChanges = true
|
|
changes.push(`${tailwindClass} → ${firstResolution.token} (resolved conflict)`)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
if (hasChanges) {
|
|
if (!interactive) {
|
|
fs.writeFileSync(filePath, content)
|
|
}
|
|
|
|
console.log(`✅ ${interactive ? 'Preview for' : 'Updated'} ${filePath}`)
|
|
changes.forEach((change) => console.log(` ${change}`))
|
|
|
|
if (conflictChoices.length > 0) {
|
|
console.log(`\n🔀 Conflict resolutions:`)
|
|
conflictChoices.forEach((choice) => {
|
|
console.log(` ${choice.original} → ${choice.chosen}`)
|
|
console.log(` Reason: ${choice.reason}`)
|
|
if (choice.alternatives.length > 0) {
|
|
console.log(` Alternatives: ${choice.alternatives.join(', ')}`)
|
|
}
|
|
})
|
|
}
|
|
|
|
return { file: filePath, changes: changes.length, conflicts: conflictChoices }
|
|
}
|
|
|
|
return null
|
|
} catch (error) {
|
|
console.error(`❌ Error processing ${filePath}:`, error.message)
|
|
return null
|
|
}
|
|
}
|
|
|
|
// Additional mappings for responsive and state variants
|
|
const generateVariantMappings = () => {
|
|
const variants = [
|
|
'sm:',
|
|
'md:',
|
|
'lg:',
|
|
'xl:',
|
|
'2xl:',
|
|
'dark:',
|
|
'group-hover:',
|
|
'hover:',
|
|
'focus:',
|
|
'active:',
|
|
'disabled:',
|
|
'focus-within:',
|
|
'focus-visible:',
|
|
'group-focus:',
|
|
'peer-focus:',
|
|
'first:',
|
|
'last:',
|
|
'odd:',
|
|
'even:',
|
|
'visited:',
|
|
'checked:',
|
|
'group-active:',
|
|
'group-disabled:',
|
|
'peer-hover:',
|
|
'peer-active:',
|
|
]
|
|
const additionalMappings = {}
|
|
|
|
// Only generate variants for non-conflicting mappings
|
|
variants.forEach((variant) => {
|
|
Object.keys(colorMappings).forEach((oldClass) => {
|
|
const newClass = colorMappings[oldClass]
|
|
additionalMappings[variant + oldClass] = variant + newClass
|
|
})
|
|
})
|
|
|
|
return additionalMappings
|
|
}
|
|
|
|
const allMappings = { ...colorMappings, ...generateVariantMappings() }
|
|
|
|
/**
|
|
* Scan and convert files in a directory to semantic tokens with conflict resolution
|
|
*/
|
|
function convertDirectoryToSemanticWithConflicts(
|
|
directory,
|
|
strategy = CONFLICT_STRATEGIES.CONTEXT_AWARE,
|
|
fileExtensions = ['js', 'jsx', 'ts', 'tsx', 'vue', 'svelte', 'html', 'css', 'scss'],
|
|
) {
|
|
const pattern = `${directory}/**/*.{${fileExtensions.join(',')}}`
|
|
const files = glob.sync(pattern, {
|
|
ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**', '**/coverage/**'],
|
|
})
|
|
|
|
console.log(`🔍 Found ${files.length} files to process...`)
|
|
console.log(`🧠 Using strategy: ${strategy}`)
|
|
|
|
const results = []
|
|
let totalChanges = 0
|
|
let totalConflicts = 0
|
|
|
|
files.forEach((file) => {
|
|
const result = convertFileToSemanticWithConflicts(file, strategy, false)
|
|
if (result) {
|
|
results.push(result)
|
|
totalChanges += result.changes
|
|
totalConflicts += result.conflicts.length
|
|
}
|
|
})
|
|
|
|
console.log(`\n📊 Migration Summary:`)
|
|
console.log(` Files processed: ${files.length}`)
|
|
console.log(` Files updated: ${results.length}`)
|
|
console.log(` Total changes: ${totalChanges}`)
|
|
console.log(` Conflicts resolved: ${totalConflicts}`)
|
|
|
|
return results
|
|
}
|
|
|
|
/**
|
|
* Interactive conflict resolution for a single file
|
|
*/
|
|
function interactiveConflictResolution(filePath) {
|
|
const readline = require('readline')
|
|
const rl = readline.createInterface({
|
|
input: process.stdin,
|
|
output: process.stdout,
|
|
})
|
|
|
|
return new Promise((resolve) => {
|
|
try {
|
|
const content = fs.readFileSync(filePath, 'utf8')
|
|
const allConflicts = getAllConflicts()
|
|
let modifiedContent = content
|
|
const decisions = []
|
|
|
|
// First handle non-conflicting mappings
|
|
Object.keys(colorMappings).forEach((tailwindClass) => {
|
|
const semanticClass = colorMappings[tailwindClass]
|
|
const classRegex = new RegExp(`\\b${tailwindClass.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g')
|
|
modifiedContent = modifiedContent.replace(classRegex, semanticClass)
|
|
})
|
|
|
|
const conflictKeys = Object.keys(allConflicts)
|
|
let currentConflictIndex = 0
|
|
|
|
function processNextConflict() {
|
|
if (currentConflictIndex >= conflictKeys.length) {
|
|
rl.close()
|
|
fs.writeFileSync(filePath, modifiedContent)
|
|
console.log('\n✅ Interactive conversion completed!')
|
|
resolve(decisions)
|
|
return
|
|
}
|
|
|
|
const tailwindClass = conflictKeys[currentConflictIndex]
|
|
const conflicts = allConflicts[tailwindClass]
|
|
const classRegex = new RegExp(`\\b${tailwindClass.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g')
|
|
|
|
if (!classRegex.test(modifiedContent)) {
|
|
currentConflictIndex++
|
|
processNextConflict()
|
|
return
|
|
}
|
|
|
|
console.log(`\n🔀 Conflict found: ${tailwindClass}`)
|
|
console.log('Choose the semantic token:')
|
|
|
|
conflicts.forEach((option, index) => {
|
|
console.log(` ${index + 1}. ${option.token}`)
|
|
console.log(` Usage: ${option.usage}`)
|
|
console.log(` Context: ${option.context.join(', ')}`)
|
|
})
|
|
|
|
console.log(` ${conflicts.length + 1}. Skip this class`)
|
|
|
|
rl.question('\nEnter your choice (number): ', (answer) => {
|
|
const choice = parseInt(answer) - 1
|
|
|
|
if (choice >= 0 && choice < conflicts.length) {
|
|
const selectedOption = conflicts[choice]
|
|
modifiedContent = modifiedContent.replace(classRegex, selectedOption.token)
|
|
decisions.push({
|
|
original: tailwindClass,
|
|
chosen: selectedOption.token,
|
|
reason: 'User selected',
|
|
})
|
|
console.log(`✅ Replaced ${tailwindClass} with ${selectedOption.token}`)
|
|
} else if (choice === conflicts.length) {
|
|
console.log(`⏭️ Skipped ${tailwindClass}`)
|
|
} else {
|
|
console.log('❌ Invalid choice, skipping...')
|
|
}
|
|
|
|
currentConflictIndex++
|
|
processNextConflict()
|
|
})
|
|
}
|
|
|
|
processNextConflict()
|
|
} catch (error) {
|
|
rl.close()
|
|
console.error(`❌ Error in interactive resolution:`, error.message)
|
|
resolve([])
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Generate detailed conflict report
|
|
*/
|
|
function generateConflictReport(directory) {
|
|
const pattern = `${directory}/**/*.{js,jsx,ts,tsx,vue,svelte,html}`
|
|
const files = glob.sync(pattern, {
|
|
ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**'],
|
|
})
|
|
|
|
const allConflicts = getAllConflicts()
|
|
const foundConflicts = {}
|
|
const fileConflicts = []
|
|
|
|
files.forEach((file) => {
|
|
try {
|
|
const content = fs.readFileSync(file, 'utf8')
|
|
const fileConflictInfo = { file, conflicts: [] }
|
|
|
|
Object.keys(allConflicts).forEach((tailwindClass) => {
|
|
const classRegex = new RegExp(`\\b${tailwindClass.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g')
|
|
|
|
if (classRegex.test(content)) {
|
|
const matches = content.match(classRegex)
|
|
foundConflicts[tailwindClass] = (foundConflicts[tailwindClass] || 0) + matches.length
|
|
|
|
fileConflictInfo.conflicts.push({
|
|
class: tailwindClass,
|
|
count: matches.length,
|
|
options: allConflicts[tailwindClass],
|
|
})
|
|
}
|
|
})
|
|
|
|
if (fileConflictInfo.conflicts.length > 0) {
|
|
fileConflicts.push(fileConflictInfo)
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error reading ${file}:`, error.message)
|
|
}
|
|
})
|
|
|
|
console.log(`\n📊 Conflict Analysis Report`)
|
|
console.log(`==========================`)
|
|
|
|
if (Object.keys(foundConflicts).length === 0) {
|
|
console.log(`✅ No conflicts found!`)
|
|
return
|
|
}
|
|
|
|
console.log(`\n🔥 Conflicts Summary:`)
|
|
Object.entries(foundConflicts)
|
|
.sort(([, a], [, b]) => b - a)
|
|
.forEach(([tailwindClass, count]) => {
|
|
const options = allConflicts[tailwindClass]
|
|
console.log(`\n${tailwindClass} (${count} occurrences)`)
|
|
options.forEach((option, index) => {
|
|
console.log(` ${index + 1}. ${option.token} (priority: ${option.priority})`)
|
|
console.log(` ${option.usage}`)
|
|
})
|
|
})
|
|
|
|
console.log(`\n📁 Files with conflicts:`)
|
|
fileConflicts.forEach((fileInfo) => {
|
|
console.log(`\n${fileInfo.file}`)
|
|
fileInfo.conflicts.forEach((conflict) => {
|
|
console.log(` ${conflict.class} (${conflict.count}x)`)
|
|
})
|
|
})
|
|
|
|
console.log(`\n💡 Recommendations:`)
|
|
console.log(`- Use 'convert-smart' for automatic context-aware resolution`)
|
|
console.log(`- Use 'interactive' for manual decision making`)
|
|
}
|
|
|
|
/**
|
|
* Generate a report of unconverted Tailwind classes
|
|
*/
|
|
function generateTailwindReport(directory) {
|
|
const pattern = `${directory}/**/*.{js,jsx,ts,tsx,vue,svelte,html}`
|
|
const files = glob.sync(pattern, {
|
|
ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**'],
|
|
})
|
|
|
|
const unconvertedClasses = new Set()
|
|
|
|
// Enhanced regex to catch more Tailwind color classes
|
|
const colorClassRegex = new RegExp(
|
|
[
|
|
// Standard color classes
|
|
'\\b(text-|bg-|border-|fill-|ring-|divide-|placeholder-|caret-)',
|
|
// State variants
|
|
'(hover:|focus:|active:|disabled:|group-hover:|group-focus:|peer-hover:|focus-within:|focus-visible:)?',
|
|
// Responsive variants
|
|
'(sm:|md:|lg:|xl:|2xl:|dark:)?',
|
|
// Color families with numbers
|
|
'(gray|red|green|blue|yellow|purple|pink|orange|indigo|cyan|teal|lime|emerald|sky|violet|fuchsia|rose|amber|slate|zinc|neutral|stone|black|white|brand|maroon)-',
|
|
// Color scale
|
|
'(50|100|200|300|400|500|600|700|800|900|950)\\b',
|
|
].join(''),
|
|
'g',
|
|
)
|
|
|
|
files.forEach((file) => {
|
|
try {
|
|
const content = fs.readFileSync(file, 'utf8')
|
|
const matches = content.match(colorClassRegex)
|
|
|
|
if (matches) {
|
|
matches.forEach((match) => {
|
|
// Only add if we don't have a mapping for it
|
|
if (!allMappings[match] && !getAllConflicts()[match]) {
|
|
unconvertedClasses.add(match)
|
|
}
|
|
})
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error reading ${file}:`, error.message)
|
|
}
|
|
})
|
|
|
|
if (unconvertedClasses.size > 0) {
|
|
console.log(`\n⚠️ Tailwind classes that could be converted to semantic tokens:`)
|
|
Array.from(unconvertedClasses)
|
|
.sort()
|
|
.forEach((cls) => {
|
|
console.log(` ${cls}`)
|
|
})
|
|
|
|
console.log(`\n💡 Consider adding mappings for these classes to complete the migration.`)
|
|
} else {
|
|
console.log(`\n✅ All applicable Tailwind classes have been converted to semantic tokens!`)
|
|
}
|
|
|
|
return Array.from(unconvertedClasses)
|
|
}
|
|
|
|
/**
|
|
* Legacy conversion function (for backward compatibility)
|
|
*/
|
|
function convertFileToSemantic(filePath) {
|
|
return convertFileToSemanticWithConflicts(filePath, CONFLICT_STRATEGIES.PRIORITY_BASED, false)
|
|
}
|
|
|
|
function convertDirectoryToSemantic(directory, fileExtensions) {
|
|
return convertDirectoryToSemanticWithConflicts(directory, CONFLICT_STRATEGIES.PRIORITY_BASED, fileExtensions)
|
|
}
|
|
|
|
// CLI Interface with enhanced conflict handling
|
|
const args = process.argv.slice(2)
|
|
const command = args[0]
|
|
const target = args[1] || './src'
|
|
|
|
switch (command) {
|
|
case 'convert':
|
|
console.log(`🚀 Starting Tailwind → Semantic Token migration for: ${target}`)
|
|
console.log(`📝 Using priority-based conflict resolution`)
|
|
convertDirectoryToSemantic(target)
|
|
break
|
|
|
|
case 'convert-smart':
|
|
console.log(`🚀 Starting smart Tailwind → Semantic Token migration for: ${target}`)
|
|
console.log(`🧠 Using context-aware conflict resolution`)
|
|
convertDirectoryToSemanticWithConflicts(target, CONFLICT_STRATEGIES.CONTEXT_AWARE)
|
|
break
|
|
|
|
case 'interactive':
|
|
if (!args[1]) {
|
|
console.error('Please provide a file path for interactive mode')
|
|
process.exit(1)
|
|
}
|
|
console.log(`🤝 Starting interactive conversion for: ${args[1]}`)
|
|
interactiveConflictResolution(args[1]).then(() => {
|
|
console.log('Interactive conversion completed!')
|
|
})
|
|
break
|
|
|
|
case 'conflicts':
|
|
console.log(`🔍 Analyzing conflicts in: ${target}`)
|
|
generateConflictReport(target)
|
|
break
|
|
|
|
case 'report':
|
|
console.log(`📋 Generating Tailwind usage report for: ${target}`)
|
|
generateTailwindReport(target)
|
|
break
|
|
|
|
case 'file': {
|
|
if (!args[1]) {
|
|
console.error('Please provide a file path')
|
|
process.exit(1)
|
|
}
|
|
console.log(`🔄 Converting single file: ${args[1]}`)
|
|
const result = convertFileToSemantic(args[1])
|
|
if (!result) {
|
|
console.log('No changes needed')
|
|
}
|
|
break
|
|
}
|
|
|
|
case 'file-smart':
|
|
{
|
|
if (!args[1]) {
|
|
console.error('Please provide a file path')
|
|
process.exit(1)
|
|
}
|
|
console.log(`🧠 Smart converting single file: ${args[1]}`)
|
|
const smartResult = convertFileToSemanticWithConflicts(args[1], CONFLICT_STRATEGIES.CONTEXT_AWARE)
|
|
if (!smartResult) {
|
|
console.log('No changes needed')
|
|
}
|
|
}
|
|
break
|
|
|
|
default:
|
|
console.log(`
|
|
🎨 Advanced Tailwind → Semantic Token Migration Script
|
|
|
|
Usage:
|
|
node migrate-colors.js convert [directory] # Convert using priority-based resolution
|
|
node migrate-colors.js convert-smart [directory] # Convert using context-aware resolution
|
|
node migrate-colors.js interactive [filepath] # Interactive conflict resolution
|
|
node migrate-colors.js conflicts [directory] # Analyze conflicts before converting
|
|
node migrate-colors.js report [directory] # Generate Tailwind usage report
|
|
node migrate-colors.js file [filepath] # Convert single file (priority-based)
|
|
node migrate-colors.js file-smart [filepath] # Convert single file (context-aware)
|
|
|
|
Conflict Resolution Strategies:
|
|
🏆 Priority-based: Uses predefined priority scores for each semantic token
|
|
🧠 Context-aware: Analyzes surrounding code to make intelligent choices
|
|
🤝 Interactive: Lets you manually choose resolution for each conflict
|
|
|
|
Examples:
|
|
node migrate-colors.js conflicts ./src # Analyze conflicts first
|
|
node migrate-colors.js convert-smart ./src # Smart conversion
|
|
node migrate-colors.js interactive ./src/Button.jsx # Manual resolution
|
|
|
|
Conflict Examples:
|
|
text-gray-700 can become:
|
|
• text-nc-content-gray-subtle (for body text)
|
|
• text-nc-content-inverted-secondary (on dark backgrounds)
|
|
|
|
fill-red-500 can become:
|
|
• fill-nc-fill-warning (for warning states)
|
|
• fill-nc-fill-red-medium (for general red elements)
|
|
|
|
Features:
|
|
✅ Intelligent conflict resolution based on code context
|
|
✅ Interactive mode for manual decision making
|
|
✅ Comprehensive conflict analysis and reporting
|
|
✅ Priority-based fallback for edge cases
|
|
✅ Support for all Tailwind variants (responsive, state, etc.)
|
|
`)
|
|
break
|
|
}
|