purgetss
Version:
A package that simplifies mobile app creation for Titanium developers.
214 lines (193 loc) • 7.92 kB
JavaScript
/**
* PurgeTSS - cleanup-legacy
*
* Context-aware removal of legacy branding artifacts.
*
* Reads tiapp.xml via tiapp-reader and categorizes targets into:
*
* SAFE always deleted — universally obsolete (dead qualifiers)
* CONDITIONAL deleted only when project config guarantees they're unused
* (e.g. iOS legacy launch images when storyboard is enabled)
* AGGRESSIVE behind --aggressive — strongly defensible but some edge
* cases (e.g. ldpi drawables on <1% of devices)
*
* Always prints the plan before acting. Respects dryRun.
*
* @fileoverview Legacy-artifact cleanup for Titanium branding
* @author César Estrada
*/
import fs from 'fs'
import path from 'path'
import { logger } from './branding-logger.js'
import { readTiapp, hasAdaptiveIcons } from './tiapp-reader.js'
export async function cleanupLegacy({ projectRoot, projectType, aggressive = false, dryRun = false }) {
const tiappPath = path.join(projectRoot, 'tiapp.xml')
const tiapp = readTiapp(tiappPath)
const adaptive = hasAdaptiveIcons(projectRoot)
const layout = getLayoutPaths(projectRoot, projectType)
const safe = []
const conditional = []
const aggressiveTargets = []
// ---- SAFE -----------------------------------------------------------
if (layout.androidImages && fs.existsSync(layout.androidImages)) {
const reason = 'Android long/notlong qualifier (dead since Android 3.0, 2011)'
for (const entry of listChildDirs(layout.androidImages, /^res-(long|notlong)-/)) {
safe.push({ path: entry, reason })
}
}
// ---- CONDITIONAL ----------------------------------------------------
if (tiapp.storyboardEnabled && layout.iphoneDir && fs.existsSync(layout.iphoneDir)) {
const reason = 'iOS legacy launch image (storyboard enabled → not consulted)'
for (const entry of listChildFiles(layout.iphoneDir, /^Default(-.+)?(@2x)?\.png$/)) {
conditional.push({ path: entry, reason })
}
}
if (tiapp.portraitOnly && layout.androidImages && fs.existsSync(layout.androidImages)) {
const reason = 'Landscape variant (app is portrait-only)'
for (const entry of listChildDirs(layout.androidImages, /(^res-.+-land-|^res-land-)/)) {
conditional.push({ path: entry, reason })
}
}
if (adaptive && layout.androidAssets) {
const appicon = path.join(layout.androidAssets, 'appicon.png')
if (fs.existsSync(appicon)) {
conditional.push({
path: appicon,
reason: 'Legacy appicon.png (adaptive launcher takes precedence)'
})
}
}
// ---- AGGRESSIVE -----------------------------------------------------
if (aggressive) {
const reason = 'ldpi density (<1% global market)'
if (layout.androidImages && fs.existsSync(layout.androidImages)) {
for (const entry of listChildDirs(layout.androidImages, /(^res-ldpi$|^res-.+-ldpi$)/)) {
aggressiveTargets.push({ path: entry, reason })
}
}
const ldpiDirs = [
path.join(projectRoot, 'app', 'platform', 'android', 'res', 'drawable-ldpi'),
path.join(projectRoot, 'app', 'platform', 'android', 'res', 'values-ldpi'),
path.join(projectRoot, 'app', 'platform', 'android', 'res', 'mipmap-ldpi'),
path.join(projectRoot, 'platform', 'android', 'res', 'drawable-ldpi'),
path.join(projectRoot, 'platform', 'android', 'res', 'values-ldpi'),
path.join(projectRoot, 'platform', 'android', 'res', 'mipmap-ldpi')
]
for (const d of ldpiDirs) {
if (fs.existsSync(d)) {
aggressiveTargets.push({ path: d, reason: 'ldpi resource folder (<1% global market)' })
}
}
}
printPlan({
projectRoot, projectType, tiapp, adaptive, aggressive,
safe, conditional, aggressiveTargets
})
const total = safe.length + conditional.length + aggressiveTargets.length
if (total === 0) {
logger.success('No legacy artifacts detected. Project is already clean.')
return { removed: 0, bytes: 0 }
}
if (dryRun) {
logger.info('Dry-run mode — nothing deleted. Re-run without --dry-run to apply.')
return { removed: 0, bytes: 0 }
}
let removed = 0
let bytes = 0
for (const bucket of [safe, conditional, aggressiveTargets]) {
for (const { path: target } of bucket) {
const size = getSizeBytes(target)
fs.rmSync(target, { recursive: true, force: true })
bytes += size
removed += 1
logger.success(`Removed ${path.relative(projectRoot, target)}`)
}
}
return { removed, bytes }
}
function getLayoutPaths(projectRoot, projectType) {
if (projectType === 'alloy') {
return {
androidImages: path.join(projectRoot, 'app', 'assets', 'android', 'images'),
iphoneDir: path.join(projectRoot, 'app', 'assets', 'iphone'),
androidAssets: path.join(projectRoot, 'app', 'assets', 'android')
}
}
if (projectType === 'classic') {
return {
androidImages: path.join(projectRoot, 'Resources', 'android', 'images'),
iphoneDir: path.join(projectRoot, 'Resources', 'iphone'),
androidAssets: path.join(projectRoot, 'Resources', 'android')
}
}
return { androidImages: null, iphoneDir: null, androidAssets: null }
}
function listChildDirs(parent, regex) {
if (!fs.existsSync(parent)) return []
return fs.readdirSync(parent)
.filter((name) => regex.test(name))
.map((name) => path.join(parent, name))
.filter((p) => fs.statSync(p).isDirectory())
}
function listChildFiles(parent, regex) {
if (!fs.existsSync(parent)) return []
return fs.readdirSync(parent)
.filter((name) => regex.test(name))
.map((name) => path.join(parent, name))
.filter((p) => fs.statSync(p).isFile())
}
function getSizeBytes(target) {
try {
const stat = fs.statSync(target)
if (stat.isFile()) return stat.size
let total = 0
for (const name of fs.readdirSync(target)) {
total += getSizeBytes(path.join(target, name))
}
return total
} catch {
return 0
}
}
function formatKb(bytes) {
return `${Math.round(bytes / 1024)}K`
}
function printPlan({ projectRoot, projectType, tiapp, adaptive, aggressive, safe, conditional, aggressiveTargets }) {
console.log()
logger.warning('⚠ WARNING — --cleanup-legacy deletes files permanently.')
logger.warning(' Recommended: commit your project with git before running without --dry-run.')
console.log()
console.log('Cleanup plan')
console.log(` Project: ${projectRoot} (${projectType})`)
console.log(` Storyboard: ${tiapp.storyboardEnabled ? 'enabled' : 'disabled'}`)
console.log(` Orientation: ${tiapp.portraitOnly ? 'portrait-only' : 'landscape allowed'}`)
console.log(` Adaptive icons: ${adaptive ? 'present' : 'not detected'}`)
console.log(` Aggressive mode: ${aggressive ? 'on (includes ldpi)' : 'off'}`)
if (safe.length) {
console.log()
logger.success('SAFE — universally obsolete')
for (const { path: p, reason } of safe) {
console.log(` ${formatKb(getSizeBytes(p)).padEnd(6)} ${path.relative(projectRoot, p)}`)
console.log(` ${reason}`)
}
}
if (conditional.length) {
console.log()
logger.warning('CONDITIONAL — safe given your project config')
for (const { path: p, reason } of conditional) {
console.log(` ${formatKb(getSizeBytes(p)).padEnd(6)} ${path.relative(projectRoot, p)}`)
console.log(` ${reason}`)
}
}
if (aggressiveTargets.length) {
console.log()
logger.error('AGGRESSIVE — --aggressive enabled')
for (const { path: p, reason } of aggressiveTargets) {
console.log(` ${formatKb(getSizeBytes(p)).padEnd(6)} ${path.relative(projectRoot, p)}`)
console.log(` ${reason}`)
}
}
const total = safe.length + conditional.length + aggressiveTargets.length
console.log()
console.log(` Total: ${total} item(s) to remove`)
}