purgetss
Version:
A package that simplifies mobile app creation for Titanium developers.
276 lines (235 loc) • 13.8 kB
JavaScript
/* eslint-disable camelcase */
/**
* PurgeTSS v7.1.0 - Core Purger: Tailwind
* utilities.tss purging engine - removes unused classes
*
* COPIED from src/index.js during refactorization - NO CHANGES to logic.
*
* @since 7.1.0
* @author César Estrada
*/
import fs from 'fs'
import _ from 'lodash'
import chalk from 'chalk'
import * as helpers from '../../shared/helpers.js'
import { logger } from '../../shared/logger.js'
import { deriveAlphaKey } from '../../shared/semantic-helpers.js'
import {
// eslint-disable-next-line camelcase
projectsTailwind_TSS,
projectsConfigJS
} from '../../shared/constants.js'
import { getFileUpdatedDate } from '../../shared/utils.js'
// Import timing helpers
import { localStart, localFinish } from '../../cli/utils/cli-helpers.js'
// Import shared functions instead of redefining them
import { createDefinitionsFile } from '../../cli/commands/init.js'
import { checkIfColorModule } from '../../cli/commands/shades.js'
import { buildTailwindBasedOnConfigOptions } from '../../core/builders/tailwind-builder.js'
/**
* Clean class name by removing platform and modifier prefixes
* COPIED exactly from original cleanClassNameFn() function
*/
function cleanClassNameFn(className) {
return className.replace('ios:', '').replace('android:', '').replace('handheld:', '').replace('tablet:', '').replace('children:', '').replace('child:', '').replace('open:', '').replace('close:', '').replace('complete:', '').replace('drag:', '').replace('drop:', '').replace('bounds:', '')
}
/**
* Purge Tailwind classes - COPIED exactly from original purgeTailwind() function
* NO CHANGES to logic, preserving 100% of original functionality
*
* @param {Array} uniqueClasses - Array of unique class names found in XML files
* @returns {string} Purged utilities.tss classes as string
*/
export function purgeTailwind(uniqueClasses, debug = false) {
if (debug) localStart()
// In debug mode, the section label is emitted by localFinish together with
// the timing (inline). In non-debug mode this line is the progress indicator.
if (!debug) logger.info('Purging', chalk.yellow('utilities.tss'), 'styles...')
let purgedClasses = ''
let tailwindClasses = fs.readFileSync(projectsTailwind_TSS, 'utf8').split(/\r?\n/)
if (`// config.js file updated on: ${getFileUpdatedDate(projectsConfigJS)}` !== tailwindClasses[3]) {
logger.info(chalk.yellow('config.js'), 'file changed!, rebuilding utilities.tss...')
checkIfColorModule()
buildTailwindBasedOnConfigOptions()
createDefinitionsFile()
tailwindClasses = fs.readFileSync(projectsTailwind_TSS, 'utf8').split(/\r?\n/)
}
// let complexClasses = [];
const cleanUniqueClasses = []
const classesWithOpacityValues = []
let arbitraryValues = '\n// Arbitrary Values\n'
uniqueClasses.forEach((className, index) => {
const cleanClassName = cleanClassNameFn(className)
if (cleanClassName.indexOf(':') !== -1) {
// complexClasses.push(cleanClassName);
} else if (cleanClassName.includes('(')) {
const line = helpers.formatArbitraryValues(cleanClassName, true)
if (line) arbitraryValues += helpers.checkPlatformAndDevice(line, className)
} else if (helpers.checkColorClasses(cleanClassName)) {
const decimalValue = cleanClassName.split('/')[1]
let transparency = Math.round(decimalValue * 255 / 100).toString(16)
if (transparency.length === 1) transparency = '0' + transparency
const classNameWithTransparency = uniqueClasses[index]
const className = cleanClassName.substring(0, cleanClassName.lastIndexOf('/'))
classesWithOpacityValues.push({ decimalValue, transparency, className, classNameWithTransparency })
} else {
cleanUniqueClasses.push(className)
}
})
// TODO: Process complex Classes
// complexClasses.forEach((className) => {
// let classes = className.split(':');
// let line = '';
// classes.forEach((className) => {
// let classLine = tailwindClasses[22] + '\n';
// if (classLine) line += classLine;
// });
// if (line) purgedClasses += line;
// });
const deviceClasses = []
const titaniumClasses = []
const anArrayOfCustomClasses = []
const anArrayOfDefaultClasses = []
const anArrayOfAnimationClasses = []
const endOfCustomClasses = tailwindClasses.indexOf('// End of Custom Classes')
// let colorClasses = 0;
// let restOfClasses = 0;
tailwindClasses.forEach((tailwindClass, key) => {
if (tailwindClass !== '' && !tailwindClass.includes('//')) {
// if (tailwindClass.includes('color') || tailwindClass.includes('Color')) colorClasses++;
// else restOfClasses++;
const cleanTailwindClass = `${tailwindClass.split(':')[0].replace('.', '').replace(/'/g, '').replace(/ *\[[^\]]*]/, '').replace(/^#/, '')}`
const classIndex = cleanUniqueClasses.indexOf(cleanTailwindClass)
if (classIndex > -1) {
if (cleanTailwindClass.charAt(0) === cleanTailwindClass.charAt(0).toUpperCase() && cleanTailwindClass.charAt(0) !== '-') {
titaniumClasses.push(helpers.checkPlatformAndDevice(tailwindClass, cleanUniqueClasses[classIndex]))
} else if (tailwindClass.includes('animationProperties')) {
anArrayOfAnimationClasses.push(helpers.checkPlatformAndDevice(tailwindClass, cleanUniqueClasses[classIndex]))
} else if (key > endOfCustomClasses) {
anArrayOfDefaultClasses.push(helpers.checkPlatformAndDevice(tailwindClass, cleanUniqueClasses[classIndex]))
} else {
anArrayOfCustomClasses.push(helpers.checkPlatformAndDevice(tailwindClass, cleanUniqueClasses[classIndex]))
}
}
if (cleanUniqueClasses.indexOf(`ios:${cleanTailwindClass}`) > -1) {
deviceClasses.push(helpers.checkPlatformAndDevice(tailwindClass, cleanUniqueClasses[cleanUniqueClasses.indexOf(`ios:${cleanTailwindClass}`)]))
}
if (cleanUniqueClasses.indexOf(`android:${cleanTailwindClass}`) > -1) {
deviceClasses.push(helpers.checkPlatformAndDevice(tailwindClass, cleanUniqueClasses[cleanUniqueClasses.indexOf(`android:${cleanTailwindClass}`)]))
}
if (cleanUniqueClasses.indexOf(`tablet:${cleanTailwindClass}`) > -1) {
deviceClasses.push(helpers.checkPlatformAndDevice(tailwindClass, cleanUniqueClasses[cleanUniqueClasses.indexOf(`tablet:${cleanTailwindClass}`)]))
}
if (cleanUniqueClasses.indexOf(`handheld:${cleanTailwindClass}`) > -1) {
deviceClasses.push(helpers.checkPlatformAndDevice(tailwindClass, cleanUniqueClasses[cleanUniqueClasses.indexOf(`handheld:${cleanTailwindClass}`)]))
}
if (cleanUniqueClasses.indexOf(`children:${cleanTailwindClass}`) > -1) {
anArrayOfAnimationClasses.push(helpers.checkPlatformAndDevice(tailwindClass, cleanUniqueClasses[cleanUniqueClasses.indexOf(`children:${cleanTailwindClass}`)]))
}
if (cleanUniqueClasses.indexOf(`child:${cleanTailwindClass}`) > -1) {
anArrayOfAnimationClasses.push(helpers.checkPlatformAndDevice(tailwindClass, cleanUniqueClasses[cleanUniqueClasses.indexOf(`child:${cleanTailwindClass}`)]))
}
if (cleanUniqueClasses.indexOf(`open:${cleanTailwindClass}`) > -1) {
anArrayOfAnimationClasses.push(helpers.checkPlatformAndDevice(tailwindClass, cleanUniqueClasses[cleanUniqueClasses.indexOf(`open:${cleanTailwindClass}`)]))
}
if (cleanUniqueClasses.indexOf(`close:${cleanTailwindClass}`) > -1) {
anArrayOfAnimationClasses.push(helpers.checkPlatformAndDevice(tailwindClass, cleanUniqueClasses[cleanUniqueClasses.indexOf(`close:${cleanTailwindClass}`)]))
}
if (cleanUniqueClasses.indexOf(`drag:${cleanTailwindClass}`) > -1) {
anArrayOfAnimationClasses.push(helpers.checkPlatformAndDevice(tailwindClass, cleanUniqueClasses[cleanUniqueClasses.indexOf(`drag:${cleanTailwindClass}`)]))
}
if (cleanUniqueClasses.indexOf(`drop:${cleanTailwindClass}`) > -1) {
anArrayOfAnimationClasses.push(helpers.checkPlatformAndDevice(tailwindClass, cleanUniqueClasses[cleanUniqueClasses.indexOf(`drop:${cleanTailwindClass}`)]))
}
if (cleanUniqueClasses.indexOf(`complete:${cleanTailwindClass}`) > -1) {
anArrayOfAnimationClasses.push(helpers.checkPlatformAndDevice(tailwindClass, cleanUniqueClasses[cleanUniqueClasses.indexOf(`complete:${cleanTailwindClass}`)]))
}
if (cleanUniqueClasses.indexOf(`bounds:${cleanTailwindClass}`) > -1) {
anArrayOfAnimationClasses.push(helpers.checkPlatformAndDevice(tailwindClass, cleanUniqueClasses[cleanUniqueClasses.indexOf(`bounds:${cleanTailwindClass}`)]))
}
}
})
// console.log('colorClasses', colorClasses);
// console.log('restOfClasses', restOfClasses);
// console.log('Total:', colorClasses + restOfClasses);
purgedClasses += (titaniumClasses.length) ? '\n// Ti Elements\n' + titaniumClasses.sort().join('') : ''
purgedClasses += (anArrayOfCustomClasses.length) ? '\n// Custom Styles\n' + anArrayOfCustomClasses.sort().join('') : ''
purgedClasses += (anArrayOfDefaultClasses.length) ? '\n// Main Styles\n' + anArrayOfDefaultClasses.sort().join('') : ''
purgedClasses += (deviceClasses.length) ? '\n// Platform and Device Modifiers\n' + deviceClasses.sort().join('') : ''
purgedClasses += (anArrayOfAnimationClasses.length) ? '\n// Animation Module\n' + anArrayOfAnimationClasses.sort().join('') : ''
// Color Opacity Modifiers
if (classesWithOpacityValues.length > 0) {
purgedClasses += '\n// Color Opacity Modifiers\n'
classesWithOpacityValues.forEach(opacityValue => {
const opacityIndex = _.findIndex(tailwindClasses, line => line.startsWith(`'.${opacityValue.className}'`))
const classProperties = tailwindClasses[opacityIndex]
if (opacityIndex > -1 && classProperties && !classProperties.includes('#')) {
const derivedLine = tryDeriveSemanticOpacityLine(classProperties, opacityValue)
if (derivedLine) {
purgedClasses += switchPlatform(helpers.checkPlatformAndDevice(derivedLine, opacityValue.classNameWithTransparency))
} else {
console.warn('')
console.warn(chalk.yellow(` Skipping ".${opacityValue.className}/${opacityValue.decimalValue}" — semantic color, no hex to blend.`))
console.warn(chalk.yellow(` Use a PurgeTSS built-in color, bg-(#AARRGGBB), or "purgetss semantic --single ... --alpha ${opacityValue.decimalValue}".`))
console.warn('')
}
}
if (opacityIndex > -1 && classProperties && classProperties.includes('#')) {
const hexMatches = classProperties.match(/#[0-9a-f]{6}/gi)
const defaultHexValue = (classProperties.includes('from')) ? hexMatches[1] : hexMatches[0]
let classWithoutDecimalOpacity = `${classProperties.replace(new RegExp(defaultHexValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), `#${opacityValue.transparency}${defaultHexValue.substring(1)}`)}`
// Special case: #000000
if (classProperties.includes('from') && defaultHexValue === '#000000') classWithoutDecimalOpacity = classWithoutDecimalOpacity.replace('00000000', '000000')
let defaultTextValue = classProperties.match(/'[^']*'/i)[0]
defaultTextValue = defaultTextValue.substring(1, defaultTextValue.length)
const finalClassName = `${classWithoutDecimalOpacity.replace(defaultTextValue, `.${defaultTextValue.substring(1, defaultTextValue.length - 1)}/${opacityValue.decimalValue}'`)}`
purgedClasses += switchPlatform(helpers.checkPlatformAndDevice(finalClassName, opacityValue.classNameWithTransparency))
}
})
}
// Add arbitrary values
purgedClasses += (arbitraryValues !== '\n// Arbitrary Values\n') ? arbitraryValues : ''
if (debug) localFinish('Purging ' + chalk.yellow('utilities.tss') + ' styles...')
return purgedClasses
}
// Auto-derive a semantic key with applied alpha and emit a TSS line for the
// `class/N` form. Returns the rewritten line (with selector renamed to include
// `/N` and the semantic value swapped for the derived key), or `null` when no
// candidate matches an entry in semantic.colors.json. Conflict errors from
// `deriveAlphaKey` propagate naturally.
function tryDeriveSemanticOpacityLine(classProperties, opacityValue) {
const bodyMatch = classProperties.match(/\{([^}]*)\}/)
if (!bodyMatch) return null
const candidates = (bodyMatch[1].match(/'([^']+)'/g) || [])
.map(m => m.slice(1, -1))
.filter(v => !v.startsWith('#'))
for (const candidate of candidates) {
const derivedKey = deriveAlphaKey(candidate, opacityValue.decimalValue)
if (derivedKey) {
let line = classProperties.replace(new RegExp(`'${candidate}'`, 'g'), `'${derivedKey}'`)
line = line.replace(
`'.${opacityValue.className}'`,
`'.${opacityValue.className}/${opacityValue.decimalValue}'`
)
return line
}
}
return null
}
/**
* Switch platform specific styles - COPIED exactly from original switchPlatform() function
* NO CHANGES to logic, preserving 100% of original functionality
*
* @param {string} withPlatformDeviceStyle - Style with platform/device modifiers
* @returns {string} Style with platform modifiers moved to correct position
*/
function switchPlatform(withPlatformDeviceStyle) {
// !Move platform specific styles to the end of the class name
if (withPlatformDeviceStyle.search(/\[platform=ios\]|\[platform=android\]/i) > -1) {
return (withPlatformDeviceStyle.includes('[platform=ios]'))
? withPlatformDeviceStyle.replace('[platform=ios]', '').replace(/[^'.][^']+|1/, '$&[platform=ios]')
: withPlatformDeviceStyle.replace('[platform=android]', '').replace(/[^'.][^']+|1/, '$&[platform=android]')
}
return withPlatformDeviceStyle
}
// End of purgeTailwind function