purgetss
Version:
A package that simplifies mobile app creation for Titanium developers.
325 lines (284 loc) • 11.4 kB
JavaScript
/**
* PurgeTSS v7.1 - Configuration Manager
*
* Centralized configuration management for PurgeTSS project configs.
* Extracted from src/index.js during refactorization.
*
* @fileoverview Configuration loading and management utilities
* @version 7.1.0
* @author César Estrada
* @since 2025-06-15
*/
import fs from 'fs'
import { createRequire } from 'module'
import defaultTheme from 'tailwindcss/defaultTheme.js'
import {
projectsConfigJS,
projectsPurgeTSSFolder,
projectsPurge_TSS_Fonts_Folder,
projectsPurge_TSS_Brand_Folder,
projectsPurge_TSS_Images_Folder,
srcConfigFile
} from './constants.js'
import { logger } from './logger.js'
import { makeSureFolderExists } from './utils.js'
import { validateConfig } from './validation/config-validator.js'
// Create require for ESM compatibility
const require = createRequire(import.meta.url)
/**
* Parse a padding value from either a number or a percentage string.
*
* 20 → 20
* '20' → 20
* '20%' → 20
*
* Used for the `brand.padding.*` values so users can write
* self-documenting values like `androidAdaptive: '19%'` in their config.
*
* @param {number|string} value
* @param {string} fieldName - Config path for error messages (e.g. 'brand.padding.androidAdaptive')
* @returns {number} Integer 0-40
*/
function parsePadding(value, fieldName) {
if (typeof value === 'number') return value
if (typeof value === 'string') {
const match = value.trim().match(/^(\d+)%?$/)
if (match) return parseInt(match[1], 10)
}
throw new Error(`Invalid ${fieldName}: expected number or '<N>%' string, got ${JSON.stringify(value)}`)
}
/**
* Ensure config file exists - SIMPLE logic
* 1. If config.cjs exists → use it
* 2. If config.js exists → rename to config.cjs
* 3. If nothing exists → create config.cjs
*/
export function ensureConfig() {
// Ensure the full purgetss/ subfolder layout exists on every init — keeps
// fonts/, brand/, and images/ discoverable from day one instead of
// appearing lazily on first use of their respective commands.
makeSureFolderExists(projectsPurgeTSSFolder)
makeSureFolderExists(projectsPurge_TSS_Fonts_Folder)
makeSureFolderExists(projectsPurge_TSS_Brand_Folder)
makeSureFolderExists(projectsPurge_TSS_Images_Folder)
// 1. ¿Existe config.cjs? → Úsalo
if (fs.existsSync(projectsConfigJS)) {
return
}
// 2. ¿Existe config.js? → Renómbralo
const oldConfigPath = `${projectsPurgeTSSFolder}/config.js`
if (fs.existsSync(oldConfigPath)) {
makeSureFolderExists(projectsPurgeTSSFolder)
fs.renameSync(oldConfigPath, projectsConfigJS)
logger.info('Migrated config.js to config.cjs for ESM compatibility')
return
}
// 3. No existe nada → Crear config.cjs
makeSureFolderExists(projectsPurgeTSSFolder)
fs.copyFileSync(srcConfigFile, projectsConfigJS)
logger.file('./purgetss/config.cjs')
}
/**
* @deprecated Use ensureConfig() instead
* Migrate config.js to config.cjs for ESM compatibility
* This must be called BEFORE any config file creation
*/
export function migrateConfigIfNeeded() {
const oldConfigPath = `${projectsPurgeTSSFolder}/config.js`
// If only config.js exists, migrate it directly
if (fs.existsSync(oldConfigPath) && !fs.existsSync(projectsConfigJS)) {
makeSureFolderExists(projectsPurgeTSSFolder)
fs.renameSync(oldConfigPath, projectsConfigJS)
logger.info('Migrated config.js to config.cjs for ESM compatibility')
}
// If both exist, preserve config.js (let the user decide)
else if (fs.existsSync(oldConfigPath) && fs.existsSync(projectsConfigJS)) {
logger.warn('Both config.js and config.cjs exist. Please manually merge and remove config.js')
}
}
// Tracks configs already warned about in this process so the deprecation
// notice prints once per session even if getConfigFile() is called many times.
const _warnedLegacyBrand = new Set()
/**
* Migrate the flat pre-7cb5890 `brand:` schema (padding as number, iosPadding,
* bgColor, darkBgColor, top-level notification/splash) into the grouped
* schema downstream code expects. Mutates in place; if both legacy and new
* keys coexist, the new key wins. Emits ONE warning per config path per session.
*/
function normalizeLegacyBrand(configFile, sourcePath) {
const brand = configFile.brand
if (!brand || typeof brand !== 'object') return
const hits = []
if (brand.padding != null && typeof brand.padding !== 'object') {
const value = brand.padding
brand.padding = { androidLegacy: value, androidAdaptive: value }
hits.push(`brand.padding: ${JSON.stringify(value)} → brand.padding.androidLegacy + brand.padding.androidAdaptive`)
}
if ('iosPadding' in brand) {
brand.padding = (brand.padding && typeof brand.padding === 'object') ? brand.padding : {}
brand.padding.ios = brand.padding.ios ?? brand.iosPadding
hits.push('brand.iosPadding → brand.padding.ios')
delete brand.iosPadding
}
if ('bgColor' in brand) {
brand.colors = brand.colors ?? {}
brand.colors.background = brand.colors.background ?? brand.bgColor
hits.push('brand.bgColor → brand.colors.background')
delete brand.bgColor
}
if ('darkBgColor' in brand) {
brand.ios = brand.ios ?? {}
brand.ios.darkBackground = brand.ios.darkBackground ?? brand.darkBgColor
hits.push('brand.darkBgColor → brand.ios.darkBackground')
delete brand.darkBgColor
}
if ('notification' in brand) {
brand.android = brand.android ?? {}
brand.android.notification = brand.android.notification ?? brand.notification
hits.push('brand.notification → brand.android.notification')
delete brand.notification
}
if ('splash' in brand) {
brand.android = brand.android ?? {}
brand.android.splash = brand.android.splash ?? brand.splash
hits.push('brand.splash → brand.android.splash')
delete brand.splash
}
if (hits.length > 0 && !_warnedLegacyBrand.has(sourcePath)) {
_warnedLegacyBrand.add(sourcePath)
logger.warn('Legacy brand: schema detected in purgetss/config.cjs — auto-migrated in memory:')
for (const hit of hits) logger.item(` • ${hit}`)
logger.item(' Update purgetss/config.cjs to the new grouped schema to silence this warning.')
}
}
/**
* Get configuration file with fallback to default template
* Maintains exact same logic as original getConfigFile()
*
* @returns {Object} Configuration object with defaults applied
*/
export function getConfigFile() {
const sourcePath = fs.existsSync(projectsConfigJS) ? projectsConfigJS : srcConfigFile
const configFile = require(sourcePath)
validateConfig(configFile, sourcePath)
normalizeLegacyBrand(configFile, sourcePath)
// Apply default values following template structure
configFile.purge = configFile.purge ?? {}
configFile.purge.mode = configFile.purge.mode ?? 'all'
configFile.purge.method = configFile.purge.method ?? 'sync'
configFile.purge.options = configFile.purge.options ?? {}
configFile.purge.options.missing = configFile.purge.options.missing ?? true
configFile.purge.options.widgets = configFile.purge.options.widgets ?? false
configFile.purge.options.safelist = configFile.purge.options.safelist ?? []
configFile.purge.options.plugins = configFile.purge.options.plugins ?? []
configFile.brand = configFile.brand ?? {}
configFile.brand.logos = configFile.brand.logos ?? {}
configFile.brand.padding = configFile.brand.padding ?? {}
configFile.brand.padding.ios = parsePadding(configFile.brand.padding.ios ?? 4, 'brand.padding.ios')
configFile.brand.padding.androidLegacy = parsePadding(configFile.brand.padding.androidLegacy ?? 10, 'brand.padding.androidLegacy')
configFile.brand.padding.androidAdaptive = parsePadding(configFile.brand.padding.androidAdaptive ?? 19, 'brand.padding.androidAdaptive')
configFile.brand.padding.featureGraphic = parsePadding(configFile.brand.padding.featureGraphic ?? 12, 'brand.padding.featureGraphic')
configFile.brand.android = configFile.brand.android ?? {}
configFile.brand.android.notification = configFile.brand.android.notification ?? false
configFile.brand.android.splash = configFile.brand.android.splash ?? false
configFile.brand.ios = configFile.brand.ios ?? {}
configFile.brand.ios.dark = configFile.brand.ios.dark ?? true
configFile.brand.ios.tinted = configFile.brand.ios.tinted ?? true
configFile.brand.ios.darkBackground = configFile.brand.ios.darkBackground ?? null
configFile.brand.colors = configFile.brand.colors ?? {}
configFile.brand.colors.background = configFile.brand.colors.background ?? '#FFFFFF'
configFile.images = configFile.images ?? {}
configFile.images.quality = configFile.images.quality ?? 85
configFile.images.format = configFile.images.format ?? null
configFile.theme = configFile.theme ?? {}
configFile.theme.extend = configFile.theme.extend ?? {}
return configFile
}
/**
* Get configuration options with defaults
* Maintains exact same logic as original getConfigOptions()
*
* @returns {Object} Configuration options with defaults applied
*/
export function getConfigOptions() {
const configFile = getConfigFile()
const configOptions = (configFile.purge && configFile.purge.options)
? configFile.purge.options
: {}
if (configOptions) {
// Legacy mode removed in v7.1 - always use modern mode
configOptions.widgets = configOptions.widgets ?? false
configOptions.missing = configOptions.missing ?? true
configOptions.plugins = configOptions.plugins ?? []
}
return configOptions
}
/**
* Check if config file exists in project
*
* @returns {boolean} True if project has custom config
*/
export function hasProjectConfig() {
return fs.existsSync(projectsConfigJS)
}
/**
* Get config file path (project or default)
*
* @returns {string} Path to active config file
*/
export function getActiveConfigPath() {
return hasProjectConfig() ? projectsConfigJS : srcConfigFile
}
/**
* Load config file without defaults (raw)
*
* @returns {Object} Raw configuration object
*/
export function loadRawConfig() {
const configPath = getActiveConfigPath()
return require(configPath)
}
/**
* Get the global config file instance (lazy-loaded)
* Maintains compatibility with legacy code that expects `configFile` variable
*
* @returns {Object} Configuration file with defaults applied
*/
export function getGlobalConfigFile() {
return getConfigFile()
}
/**
* Get the global config options instance (lazy-loaded)
* Maintains compatibility with legacy code that expects `configOptions` variable
*
* @returns {Object} Configuration options with defaults applied
*/
export function getGlobalConfigOptions() {
const file = getConfigFile()
const options = (file.purge && file.purge.options) ? file.purge.options : {}
if (options) {
options.widgets = options.widgets ?? false
options.missing = options.missing ?? true
options.plugins = options.plugins ?? []
}
return options
}
// Legacy exports removed - use getConfigFile() and getConfigOptions() functions instead
/**
* Export defaultTheme from tailwindcss for backward compatibility
*/
export { defaultTheme }
/**
* Export for backward compatibility
*/
export default {
getConfigFile,
getConfigOptions,
hasProjectConfig,
getActiveConfigPath,
loadRawConfig,
getGlobalConfigFile,
getGlobalConfigOptions,
migrateConfigIfNeeded,
defaultTheme
}