purgetss 7.5.2 → 7.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -11
- package/bin/purgetss +140 -1
- package/dist/purgetss.ui.js +65 -26
- package/dist/utilities.tss +21 -4
- package/experimental/completions2.js +1 -1
- package/lib/completions/titanium/completions-v3.json +62 -1
- package/lib/templates/purgetss.config.js.cjs +15 -1
- package/lib/templates/purgetss.ui.js.cjs +64 -25
- package/package.json +3 -1
- package/src/cli/commands/brand.js +69 -0
- package/src/cli/commands/create.js +11 -7
- package/src/cli/commands/fonts.js +9 -9
- package/src/cli/commands/icon-library.js +18 -16
- package/src/cli/commands/images.js +116 -0
- package/src/cli/commands/init.js +4 -0
- package/src/cli/commands/module.js +4 -2
- package/src/cli/commands/purge.js +77 -101
- package/src/cli/commands/semantic.js +180 -0
- package/src/cli/commands/shades.js +332 -13
- package/src/cli/utils/project-detection.js +4 -2
- package/src/core/analyzers/class-extractor.js +110 -3
- package/src/core/branding/brand-config.js +111 -0
- package/src/core/branding/branding-logger.js +40 -0
- package/src/core/branding/cleanup-legacy.js +220 -0
- package/src/core/branding/ensure-brand-section.js +80 -0
- package/src/core/branding/gen-android-adaptive.js +116 -0
- package/src/core/branding/gen-android-legacy.js +63 -0
- package/src/core/branding/gen-ic-launcher-xml.js +29 -0
- package/src/core/branding/gen-ios-dark.js +70 -0
- package/src/core/branding/gen-ios-tinted.js +55 -0
- package/src/core/branding/gen-ios.js +69 -0
- package/src/core/branding/gen-marketplace.js +71 -0
- package/src/core/branding/gen-notification.js +76 -0
- package/src/core/branding/gen-splash.js +64 -0
- package/src/core/branding/index.js +336 -0
- package/src/core/branding/post-gen-notes.js +145 -0
- package/src/core/branding/prepare-master.js +108 -0
- package/src/core/branding/tiapp-reader.js +110 -0
- package/src/core/builders/tailwind-helpers.js +1 -1
- package/src/core/images/ensure-images-section.js +57 -0
- package/src/core/images/gen-scales.js +181 -0
- package/src/core/images/index.js +171 -0
- package/src/shared/config-manager.js +46 -0
- package/src/shared/config-writer.js +84 -0
- package/src/shared/constants.js +3 -0
- package/src/shared/helpers/typography.js +38 -3
- package/src/shared/logger.js +69 -4
- package/src/shared/prompt.js +64 -0
- package/src/shared/svg-utils.js +80 -0
- package/src/shared/utils.js +8 -4
|
@@ -14,6 +14,7 @@ import _ from 'lodash'
|
|
|
14
14
|
import chalk from 'chalk'
|
|
15
15
|
import convert from 'xml-js'
|
|
16
16
|
import traverse from 'traverse'
|
|
17
|
+
import * as acorn from 'acorn'
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Get unique classes from all XML and controller files - COPIED exactly from original getUniqueClasses() function
|
|
@@ -98,7 +99,7 @@ export function extractClassesOnly(currentText, currentFile) {
|
|
|
98
99
|
* @param {string} line - Line of code from controller file
|
|
99
100
|
* @returns {Array} Array of class names found in line
|
|
100
101
|
*/
|
|
101
|
-
function
|
|
102
|
+
function extractWordsFromLineRegex(line) {
|
|
102
103
|
const patterns = [
|
|
103
104
|
{
|
|
104
105
|
// apply: 'classes'
|
|
@@ -155,12 +156,12 @@ function extractWordsFromLine(line) {
|
|
|
155
156
|
* @param {string} data - Controller file content
|
|
156
157
|
* @returns {Array} Array of class names found in controller
|
|
157
158
|
*/
|
|
158
|
-
function
|
|
159
|
+
function processControllersRegex(data) {
|
|
159
160
|
const allWords = []
|
|
160
161
|
const lines = data.split(/\r?\n/)
|
|
161
162
|
|
|
162
163
|
lines.forEach(line => {
|
|
163
|
-
const words =
|
|
164
|
+
const words = extractWordsFromLineRegex(line)
|
|
164
165
|
if (words.length > 0) {
|
|
165
166
|
allWords.push(...words)
|
|
166
167
|
}
|
|
@@ -169,6 +170,112 @@ function processControllers(data) {
|
|
|
169
170
|
return allWords
|
|
170
171
|
}
|
|
171
172
|
|
|
173
|
+
const AST_META_KEYS = new Set(['type', 'loc', 'range', 'start', 'end', 'sourceType', 'comments'])
|
|
174
|
+
|
|
175
|
+
function collectLiterals(node, out) {
|
|
176
|
+
if (!node || typeof node !== 'object' || !node.type) return
|
|
177
|
+
|
|
178
|
+
switch (node.type) {
|
|
179
|
+
case 'Literal':
|
|
180
|
+
if (typeof node.value === 'string') {
|
|
181
|
+
node.value.split(/\s+/).forEach(token => { if (token) out.push(token) })
|
|
182
|
+
}
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
case 'TemplateLiteral':
|
|
186
|
+
if (node.expressions.length === 0 && node.quasis.length > 0) {
|
|
187
|
+
const cooked = node.quasis[0].value.cooked
|
|
188
|
+
if (typeof cooked === 'string') {
|
|
189
|
+
cooked.split(/\s+/).forEach(token => { if (token) out.push(token) })
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
case 'ArrayExpression':
|
|
195
|
+
for (const element of node.elements) {
|
|
196
|
+
if (element && element.type !== 'SpreadElement') collectLiterals(element, out)
|
|
197
|
+
}
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
case 'ConditionalExpression':
|
|
201
|
+
collectLiterals(node.consequent, out)
|
|
202
|
+
collectLiterals(node.alternate, out)
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
default:
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function walkAST(node, out) {
|
|
211
|
+
if (!node || typeof node !== 'object') return
|
|
212
|
+
|
|
213
|
+
if (Array.isArray(node)) {
|
|
214
|
+
for (const child of node) walkAST(child, out)
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!node.type) return
|
|
219
|
+
|
|
220
|
+
if (node.type === 'Property' && !node.computed && !node.shorthand && node.key) {
|
|
221
|
+
const keyName = node.key.type === 'Identifier'
|
|
222
|
+
? node.key.name
|
|
223
|
+
: (node.key.type === 'Literal' ? node.key.value : undefined)
|
|
224
|
+
if (keyName === 'classes' || keyName === 'apply') {
|
|
225
|
+
collectLiterals(node.value, out)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (node.type === 'CallExpression' && node.callee && node.arguments.length >= 2) {
|
|
230
|
+
const callee = node.callee
|
|
231
|
+
let match = false
|
|
232
|
+
if (
|
|
233
|
+
callee.type === 'MemberExpression' &&
|
|
234
|
+
!callee.computed &&
|
|
235
|
+
callee.property &&
|
|
236
|
+
callee.property.type === 'Identifier' &&
|
|
237
|
+
/Class$/.test(callee.property.name)
|
|
238
|
+
) {
|
|
239
|
+
match = true
|
|
240
|
+
} else if (callee.type === 'Identifier' && callee.name === 'resetClass') {
|
|
241
|
+
match = true
|
|
242
|
+
}
|
|
243
|
+
if (match) collectLiterals(node.arguments[1], out)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
for (const key of Object.keys(node)) {
|
|
247
|
+
if (AST_META_KEYS.has(key)) continue
|
|
248
|
+
walkAST(node[key], out)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Process a controller file's source and return utility classes referenced by
|
|
254
|
+
* the whitelisted expression shapes (`classes:`, `apply:`, `.xxxClass(target, value)`,
|
|
255
|
+
* `resetClass(target, value)`). Falls back to the per-line regex scanner when
|
|
256
|
+
* the parser rejects the source.
|
|
257
|
+
*
|
|
258
|
+
* @param {string} data - Controller file content
|
|
259
|
+
* @returns {Array} Array of class name tokens
|
|
260
|
+
*/
|
|
261
|
+
export function processControllers(data) {
|
|
262
|
+
try {
|
|
263
|
+
const ast = acorn.parse(data, {
|
|
264
|
+
ecmaVersion: 'latest',
|
|
265
|
+
sourceType: 'script',
|
|
266
|
+
allowReturnOutsideFunction: true,
|
|
267
|
+
allowAwaitOutsideFunction: true,
|
|
268
|
+
allowImportExportEverywhere: true,
|
|
269
|
+
allowHashBang: true
|
|
270
|
+
})
|
|
271
|
+
const out = []
|
|
272
|
+
walkAST(ast, out)
|
|
273
|
+
return out
|
|
274
|
+
} catch {
|
|
275
|
+
return processControllersRegex(data)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
172
279
|
/**
|
|
173
280
|
* Encode HTML entities - COPIED exactly from original encodeHTML() function
|
|
174
281
|
* NO CHANGES to logic, preserving 100% of original functionality
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - Brand command config resolver
|
|
3
|
+
*
|
|
4
|
+
* Merges three sources of configuration, in this precedence order:
|
|
5
|
+
* 1. CLI flags (highest priority)
|
|
6
|
+
* 2. `brand: { ... }` section from purgetss/config.cjs
|
|
7
|
+
* 3. Built-in defaults (lowest priority)
|
|
8
|
+
*
|
|
9
|
+
* Also auto-discovers logo images inside `./purgetss/brand/` following the
|
|
10
|
+
* project convention:
|
|
11
|
+
* logo.{svg,png} → required (main logo)
|
|
12
|
+
* logo-mono.{svg,png} → optional (monochrome layer + notifications)
|
|
13
|
+
* logo-dark.{svg,png} → optional (iOS 18+ dark variant)
|
|
14
|
+
* logo-tinted.{svg,png} → optional (iOS 18+ tinted variant)
|
|
15
|
+
*
|
|
16
|
+
* CLI --monochrome-logo / --dark-logo / --tinted-logo always override
|
|
17
|
+
* discovery, and the positional argument overrides the main logo.
|
|
18
|
+
*
|
|
19
|
+
* @fileoverview Brand config + logo discovery
|
|
20
|
+
* @author César Estrada
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import fs from 'fs'
|
|
24
|
+
import path from 'path'
|
|
25
|
+
import { getConfigFile } from '../../shared/config-manager.js'
|
|
26
|
+
|
|
27
|
+
const BRAND_DIR = 'purgetss/brand'
|
|
28
|
+
const SUPPORTED_EXTS = ['svg', 'png']
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Find the first existing file matching <baseName>.<ext> for each supported ext.
|
|
32
|
+
* @param {string} baseDir - Absolute path to the search directory
|
|
33
|
+
* @param {string} baseName - Filename without extension (e.g. 'logo-mono')
|
|
34
|
+
* @returns {string|null} Absolute path to the first match, or null
|
|
35
|
+
*/
|
|
36
|
+
function findLogoFile(baseDir, baseName) {
|
|
37
|
+
for (const ext of SUPPORTED_EXTS) {
|
|
38
|
+
const candidate = path.join(baseDir, `${baseName}.${ext}`)
|
|
39
|
+
if (fs.existsSync(candidate)) return candidate
|
|
40
|
+
}
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build the final opts object for runBranding().
|
|
46
|
+
*
|
|
47
|
+
* @param {Object} cliOptions - Raw options from Commander
|
|
48
|
+
* @param {string|undefined} cliLogo - Positional logo arg from Commander
|
|
49
|
+
* @param {string} projectRoot - Absolute project root
|
|
50
|
+
* @returns {Object} Resolved options
|
|
51
|
+
*/
|
|
52
|
+
export function resolveBrandConfig(cliOptions, cliLogo, projectRoot) {
|
|
53
|
+
const brandConfig = loadBrandSection()
|
|
54
|
+
const brandDir = path.join(projectRoot, BRAND_DIR)
|
|
55
|
+
|
|
56
|
+
const resolved = {
|
|
57
|
+
logo: pickLogo(cliLogo, brandConfig.logo, brandDir, 'logo', projectRoot),
|
|
58
|
+
monochromeLogo: pickLogo(cliOptions.monochromeLogo, brandConfig.monochromeLogo, brandDir, 'logo-mono', projectRoot),
|
|
59
|
+
darkLogo: pickLogo(cliOptions.darkLogo, brandConfig.darkLogo, brandDir, 'logo-dark', projectRoot),
|
|
60
|
+
tintedLogo: pickLogo(cliOptions.tintedLogo, brandConfig.tintedLogo, brandDir, 'logo-tinted', projectRoot),
|
|
61
|
+
|
|
62
|
+
bgColor: cliOptions.bgColor ?? brandConfig.bgColor ?? '#FFFFFF',
|
|
63
|
+
bgColorExplicit: Boolean(cliOptions.bgColor ?? brandConfig.bgColor),
|
|
64
|
+
darkBgColor: cliOptions.darkBgColor ?? brandConfig.darkBgColor ?? null,
|
|
65
|
+
padding: cliOptions.padding ?? brandConfig.padding ?? 15,
|
|
66
|
+
iosPadding: cliOptions.iosPadding ?? brandConfig.iosPadding ?? 4,
|
|
67
|
+
|
|
68
|
+
// Kitchen-sink defaults: adaptive + marketplace are always generated; only
|
|
69
|
+
// notification and splash are opt-in. Config can pre-enable them.
|
|
70
|
+
notification: Boolean(cliOptions.notification ?? brandConfig.notification ?? false),
|
|
71
|
+
splash: Boolean(cliOptions.splash ?? brandConfig.splash ?? false),
|
|
72
|
+
|
|
73
|
+
withDark: cliOptions.dark !== false && (brandConfig.dark ?? true),
|
|
74
|
+
withTinted: cliOptions.tinted !== false && (brandConfig.tinted ?? true),
|
|
75
|
+
|
|
76
|
+
cleanupLegacy: Boolean(cliOptions.cleanupLegacy),
|
|
77
|
+
aggressive: Boolean(cliOptions.aggressive),
|
|
78
|
+
projectRoot,
|
|
79
|
+
output: cliOptions.output || null,
|
|
80
|
+
dryRun: Boolean(cliOptions.dryRun),
|
|
81
|
+
notes: Boolean(cliOptions.notes),
|
|
82
|
+
confirmOverwrites: brandConfig.confirmOverwrites !== false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return resolved
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @returns {Object} The `brand` section of the resolved config, or {} if missing/invalid.
|
|
90
|
+
*/
|
|
91
|
+
function loadBrandSection() {
|
|
92
|
+
try {
|
|
93
|
+
const cfg = getConfigFile()
|
|
94
|
+
if (cfg && typeof cfg === 'object' && cfg.brand && typeof cfg.brand === 'object') {
|
|
95
|
+
return cfg.brand
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// Config file missing or invalid — fall back to empty defaults.
|
|
99
|
+
}
|
|
100
|
+
return {}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Resolve a logo path with proper precedence.
|
|
105
|
+
* Config-relative paths are resolved against projectRoot.
|
|
106
|
+
*/
|
|
107
|
+
function pickLogo(cliValue, configValue, brandDir, baseName, projectRoot) {
|
|
108
|
+
if (cliValue) return path.resolve(cliValue)
|
|
109
|
+
if (configValue) return path.isAbsolute(configValue) ? configValue : path.resolve(projectRoot, configValue)
|
|
110
|
+
return findLogoFile(brandDir, baseName)
|
|
111
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - Branding Logger
|
|
3
|
+
*
|
|
4
|
+
* Chalk-based presentation helpers used exclusively by the branding pipeline.
|
|
5
|
+
* Separate from the main PurgeTSS logger because the branding output style
|
|
6
|
+
* (sections, bullets, property rows) is much richer than the one-line logging
|
|
7
|
+
* used elsewhere in the tool.
|
|
8
|
+
*
|
|
9
|
+
* @fileoverview Logger helpers for the branding pipeline
|
|
10
|
+
* @author César Estrada
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import chalk from 'chalk'
|
|
14
|
+
|
|
15
|
+
let debugMode = false
|
|
16
|
+
|
|
17
|
+
export const logger = {
|
|
18
|
+
setDebugMode(enabled) { debugMode = enabled },
|
|
19
|
+
|
|
20
|
+
info(message) { console.log(chalk.blue(message)) },
|
|
21
|
+
|
|
22
|
+
section(name) {
|
|
23
|
+
console.log()
|
|
24
|
+
console.log(chalk.cyan(`▸ ${name}`))
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
bullet(message) { console.log(` ${chalk.cyan('•')} ${message}`) },
|
|
28
|
+
|
|
29
|
+
property(label, value) { console.log(`${chalk.blue(label)}${value}`) },
|
|
30
|
+
|
|
31
|
+
success(message) { console.log(chalk.green(message)) },
|
|
32
|
+
|
|
33
|
+
warning(message) { console.log(chalk.yellow(message)) },
|
|
34
|
+
|
|
35
|
+
error(message) { console.error(chalk.red(message)) },
|
|
36
|
+
|
|
37
|
+
debug(message) {
|
|
38
|
+
if (debugMode) console.log(chalk.cyan(message))
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - cleanup-legacy
|
|
3
|
+
*
|
|
4
|
+
* Context-aware removal of legacy branding artifacts.
|
|
5
|
+
*
|
|
6
|
+
* Reads tiapp.xml via tiapp-reader and categorizes targets into:
|
|
7
|
+
*
|
|
8
|
+
* SAFE always deleted — universally obsolete (dead qualifiers)
|
|
9
|
+
* CONDITIONAL deleted only when project config guarantees they're unused
|
|
10
|
+
* (e.g. iOS legacy launch images when storyboard is enabled)
|
|
11
|
+
* AGGRESSIVE behind --aggressive — strongly defensible but some edge
|
|
12
|
+
* cases (e.g. ldpi drawables on <1% of devices)
|
|
13
|
+
*
|
|
14
|
+
* Always prints the plan before acting. Respects dryRun.
|
|
15
|
+
*
|
|
16
|
+
* @fileoverview Legacy-artifact cleanup for Titanium branding
|
|
17
|
+
* @author César Estrada
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fs from 'fs'
|
|
21
|
+
import path from 'path'
|
|
22
|
+
import { logger } from './branding-logger.js'
|
|
23
|
+
import { readTiapp, hasAdaptiveIcons } from './tiapp-reader.js'
|
|
24
|
+
|
|
25
|
+
export async function cleanupLegacy({ projectRoot, projectType, aggressive = false, dryRun = false }) {
|
|
26
|
+
const tiappPath = path.join(projectRoot, 'tiapp.xml')
|
|
27
|
+
const tiapp = readTiapp(tiappPath)
|
|
28
|
+
const adaptive = hasAdaptiveIcons(projectRoot)
|
|
29
|
+
|
|
30
|
+
const layout = getLayoutPaths(projectRoot, projectType)
|
|
31
|
+
|
|
32
|
+
const safe = []
|
|
33
|
+
const conditional = []
|
|
34
|
+
const aggressiveTargets = []
|
|
35
|
+
|
|
36
|
+
// ---- SAFE -----------------------------------------------------------
|
|
37
|
+
if (layout.androidImages && fs.existsSync(layout.androidImages)) {
|
|
38
|
+
const reason = 'Android long/notlong qualifier (dead since Android 3.0, 2011)'
|
|
39
|
+
for (const entry of listChildDirs(layout.androidImages, /^res-(long|notlong)-/)) {
|
|
40
|
+
safe.push({ path: entry, reason })
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---- CONDITIONAL ----------------------------------------------------
|
|
45
|
+
if (tiapp.storyboardEnabled && layout.iphoneDir && fs.existsSync(layout.iphoneDir)) {
|
|
46
|
+
const reason = 'iOS legacy launch image (storyboard enabled → not consulted)'
|
|
47
|
+
for (const entry of listChildFiles(layout.iphoneDir, /^Default(-.+)?(@2x)?\.png$/)) {
|
|
48
|
+
conditional.push({ path: entry, reason })
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (tiapp.portraitOnly && layout.androidImages && fs.existsSync(layout.androidImages)) {
|
|
53
|
+
const reason = 'Landscape variant (app is portrait-only)'
|
|
54
|
+
for (const entry of listChildDirs(layout.androidImages, /(^res-.+-land-|^res-land-)/)) {
|
|
55
|
+
conditional.push({ path: entry, reason })
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (adaptive && layout.androidAssets) {
|
|
60
|
+
const defaultPng = path.join(layout.androidAssets, 'default.png')
|
|
61
|
+
if (fs.existsSync(defaultPng)) {
|
|
62
|
+
conditional.push({
|
|
63
|
+
path: defaultPng,
|
|
64
|
+
reason: 'Legacy Android splash PNG (adaptive launcher handles splash now)'
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
const appicon = path.join(layout.androidAssets, 'appicon.png')
|
|
68
|
+
if (fs.existsSync(appicon)) {
|
|
69
|
+
conditional.push({
|
|
70
|
+
path: appicon,
|
|
71
|
+
reason: 'Legacy appicon.png (adaptive launcher takes precedence)'
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---- AGGRESSIVE -----------------------------------------------------
|
|
77
|
+
if (aggressive) {
|
|
78
|
+
const reason = 'ldpi density (<1% global market)'
|
|
79
|
+
if (layout.androidImages && fs.existsSync(layout.androidImages)) {
|
|
80
|
+
for (const entry of listChildDirs(layout.androidImages, /(^res-ldpi$|^res-.+-ldpi$)/)) {
|
|
81
|
+
aggressiveTargets.push({ path: entry, reason })
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const ldpiDirs = [
|
|
85
|
+
path.join(projectRoot, 'app', 'platform', 'android', 'res', 'drawable-ldpi'),
|
|
86
|
+
path.join(projectRoot, 'app', 'platform', 'android', 'res', 'values-ldpi'),
|
|
87
|
+
path.join(projectRoot, 'app', 'platform', 'android', 'res', 'mipmap-ldpi'),
|
|
88
|
+
path.join(projectRoot, 'platform', 'android', 'res', 'drawable-ldpi'),
|
|
89
|
+
path.join(projectRoot, 'platform', 'android', 'res', 'values-ldpi'),
|
|
90
|
+
path.join(projectRoot, 'platform', 'android', 'res', 'mipmap-ldpi')
|
|
91
|
+
]
|
|
92
|
+
for (const d of ldpiDirs) {
|
|
93
|
+
if (fs.existsSync(d)) {
|
|
94
|
+
aggressiveTargets.push({ path: d, reason: 'ldpi resource folder (<1% global market)' })
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
printPlan({
|
|
100
|
+
projectRoot, projectType, tiapp, adaptive, aggressive,
|
|
101
|
+
safe, conditional, aggressiveTargets
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const total = safe.length + conditional.length + aggressiveTargets.length
|
|
105
|
+
if (total === 0) {
|
|
106
|
+
logger.success('No legacy artifacts detected. Project is already clean.')
|
|
107
|
+
return { removed: 0, bytes: 0 }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (dryRun) {
|
|
111
|
+
logger.info('Dry-run mode — nothing deleted. Re-run without --dry-run to apply.')
|
|
112
|
+
return { removed: 0, bytes: 0 }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let removed = 0
|
|
116
|
+
let bytes = 0
|
|
117
|
+
for (const bucket of [safe, conditional, aggressiveTargets]) {
|
|
118
|
+
for (const { path: target } of bucket) {
|
|
119
|
+
const size = getSizeBytes(target)
|
|
120
|
+
fs.rmSync(target, { recursive: true, force: true })
|
|
121
|
+
bytes += size
|
|
122
|
+
removed += 1
|
|
123
|
+
logger.success(`Removed ${path.relative(projectRoot, target)}`)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { removed, bytes }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function getLayoutPaths(projectRoot, projectType) {
|
|
130
|
+
if (projectType === 'alloy') {
|
|
131
|
+
return {
|
|
132
|
+
androidImages: path.join(projectRoot, 'app', 'assets', 'android', 'images'),
|
|
133
|
+
iphoneDir: path.join(projectRoot, 'app', 'assets', 'iphone'),
|
|
134
|
+
androidAssets: path.join(projectRoot, 'app', 'assets', 'android')
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (projectType === 'classic') {
|
|
138
|
+
return {
|
|
139
|
+
androidImages: path.join(projectRoot, 'Resources', 'android', 'images'),
|
|
140
|
+
iphoneDir: path.join(projectRoot, 'Resources', 'iphone'),
|
|
141
|
+
androidAssets: path.join(projectRoot, 'Resources', 'android')
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { androidImages: null, iphoneDir: null, androidAssets: null }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function listChildDirs(parent, regex) {
|
|
148
|
+
if (!fs.existsSync(parent)) return []
|
|
149
|
+
return fs.readdirSync(parent)
|
|
150
|
+
.filter((name) => regex.test(name))
|
|
151
|
+
.map((name) => path.join(parent, name))
|
|
152
|
+
.filter((p) => fs.statSync(p).isDirectory())
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function listChildFiles(parent, regex) {
|
|
156
|
+
if (!fs.existsSync(parent)) return []
|
|
157
|
+
return fs.readdirSync(parent)
|
|
158
|
+
.filter((name) => regex.test(name))
|
|
159
|
+
.map((name) => path.join(parent, name))
|
|
160
|
+
.filter((p) => fs.statSync(p).isFile())
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getSizeBytes(target) {
|
|
164
|
+
try {
|
|
165
|
+
const stat = fs.statSync(target)
|
|
166
|
+
if (stat.isFile()) return stat.size
|
|
167
|
+
let total = 0
|
|
168
|
+
for (const name of fs.readdirSync(target)) {
|
|
169
|
+
total += getSizeBytes(path.join(target, name))
|
|
170
|
+
}
|
|
171
|
+
return total
|
|
172
|
+
} catch {
|
|
173
|
+
return 0
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function formatKb(bytes) {
|
|
178
|
+
return `${Math.round(bytes / 1024)}K`
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function printPlan({ projectRoot, projectType, tiapp, adaptive, aggressive, safe, conditional, aggressiveTargets }) {
|
|
182
|
+
console.log()
|
|
183
|
+
logger.warning('⚠ WARNING — --cleanup-legacy deletes files permanently.')
|
|
184
|
+
logger.warning(' Recommended: commit your project with git before running without --dry-run.')
|
|
185
|
+
console.log()
|
|
186
|
+
console.log('Cleanup plan')
|
|
187
|
+
console.log(` Project: ${projectRoot} (${projectType})`)
|
|
188
|
+
console.log(` Storyboard: ${tiapp.storyboardEnabled ? 'enabled' : 'disabled'}`)
|
|
189
|
+
console.log(` Orientation: ${tiapp.portraitOnly ? 'portrait-only' : 'landscape allowed'}`)
|
|
190
|
+
console.log(` Adaptive icons: ${adaptive ? 'present' : 'not detected'}`)
|
|
191
|
+
console.log(` Aggressive mode: ${aggressive ? 'on (includes ldpi)' : 'off'}`)
|
|
192
|
+
|
|
193
|
+
if (safe.length) {
|
|
194
|
+
console.log()
|
|
195
|
+
logger.success('SAFE — universally obsolete')
|
|
196
|
+
for (const { path: p, reason } of safe) {
|
|
197
|
+
console.log(` ${formatKb(getSizeBytes(p)).padEnd(6)} ${path.relative(projectRoot, p)}`)
|
|
198
|
+
console.log(` ${reason}`)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (conditional.length) {
|
|
202
|
+
console.log()
|
|
203
|
+
logger.warning('CONDITIONAL — safe given your project config')
|
|
204
|
+
for (const { path: p, reason } of conditional) {
|
|
205
|
+
console.log(` ${formatKb(getSizeBytes(p)).padEnd(6)} ${path.relative(projectRoot, p)}`)
|
|
206
|
+
console.log(` ${reason}`)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (aggressiveTargets.length) {
|
|
210
|
+
console.log()
|
|
211
|
+
logger.error('AGGRESSIVE — --aggressive enabled')
|
|
212
|
+
for (const { path: p, reason } of aggressiveTargets) {
|
|
213
|
+
console.log(` ${formatKb(getSizeBytes(p)).padEnd(6)} ${path.relative(projectRoot, p)}`)
|
|
214
|
+
console.log(` ${reason}`)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const total = safe.length + conditional.length + aggressiveTargets.length
|
|
218
|
+
console.log()
|
|
219
|
+
console.log(` Total: ${total} item(s) to remove`)
|
|
220
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - Ensure `brand:` section exists in purgetss/config.cjs
|
|
3
|
+
*
|
|
4
|
+
* When a project was initialized before `brand` was introduced, its
|
|
5
|
+
* config.cjs won't have a `brand:` section. On first invocation of
|
|
6
|
+
* `purgetss brand`, we patch the file to insert the default block
|
|
7
|
+
* between `purge:` and `theme:`. The user's existing keys are untouched.
|
|
8
|
+
*
|
|
9
|
+
* Rationale:
|
|
10
|
+
* - Keeps the config self-documenting — the user sees the defaults in
|
|
11
|
+
* their own file rather than having to look them up in docs.
|
|
12
|
+
* - Mirrors how `ensureConfig()` creates the file from the template when
|
|
13
|
+
* it's missing altogether.
|
|
14
|
+
* - Non-destructive: only adds if absent, never overwrites existing values.
|
|
15
|
+
*
|
|
16
|
+
* @fileoverview Auto-injects the `brand:` section on first brand run
|
|
17
|
+
* @author César Estrada
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fs from 'fs'
|
|
21
|
+
import chalk from 'chalk'
|
|
22
|
+
import { projectsConfigJS, projectsPurge_TSS_Brand_Folder } from '../../shared/constants.js'
|
|
23
|
+
import { logger } from './branding-logger.js'
|
|
24
|
+
|
|
25
|
+
const BRAND_BLOCK = ` brand: {
|
|
26
|
+
splash: false, // also generate splash_icon.png × 5
|
|
27
|
+
padding: '15%', // Android safe-zone. Range: 12% tight (mature logos) — 20% conservative. Spec floor 19.44%.
|
|
28
|
+
iosPadding: '4%', // iOS aesthetic. Range: 2% bold — 8% conservative. No launcher mask.
|
|
29
|
+
darkBgColor: null, // opaque dark bg for DefaultIcon-Dark.png (null = transparent per Apple HIG)
|
|
30
|
+
bgColor: '#FFFFFF', // Android adaptive bg + iOS/marketplace flatten
|
|
31
|
+
notification: false, // also generate ic_stat_notify.png × 5
|
|
32
|
+
confirmOverwrites: true // prompt before overwriting files (set false to skip)
|
|
33
|
+
},
|
|
34
|
+
`
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* If the project's config.cjs exists but has no `brand:` key, inject the
|
|
38
|
+
* default block between `purge:` and `theme:`. Prints a notice when it does.
|
|
39
|
+
*
|
|
40
|
+
* Silently skips if:
|
|
41
|
+
* - config.cjs doesn't exist (will be created by ensureConfig() elsewhere)
|
|
42
|
+
* - `brand:` is already present
|
|
43
|
+
* - file can't be patched safely (structure doesn't match expectation)
|
|
44
|
+
*/
|
|
45
|
+
export function ensureBrandSection() {
|
|
46
|
+
// Always make sure the logos folder exists — mirrors how init creates
|
|
47
|
+
// `purgetss/fonts/` empty so the user can see where assets go.
|
|
48
|
+
if (!fs.existsSync(projectsPurge_TSS_Brand_Folder)) {
|
|
49
|
+
fs.mkdirSync(projectsPurge_TSS_Brand_Folder, { recursive: true })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!fs.existsSync(projectsConfigJS)) return
|
|
53
|
+
|
|
54
|
+
const original = fs.readFileSync(projectsConfigJS, 'utf8')
|
|
55
|
+
|
|
56
|
+
// Already present — nothing to do.
|
|
57
|
+
if (/^\s*brand\s*:/m.test(original)) return
|
|
58
|
+
|
|
59
|
+
// Insert before the `theme:` key. Regex captures the indentation so we
|
|
60
|
+
// preserve whatever the user's style is (2-space, 4-space, etc.).
|
|
61
|
+
const match = original.match(/(^\s*)theme\s*:/m)
|
|
62
|
+
if (!match) {
|
|
63
|
+
// Non-standard layout — don't risk corrupting it. User can add manually.
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const patched = original.replace(match[0], `${BRAND_BLOCK}${match[0]}`)
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
fs.writeFileSync(projectsConfigJS, patched, 'utf8')
|
|
71
|
+
console.log()
|
|
72
|
+
logger.success(`Added ${chalk.cyan('brand:')} section to ${chalk.cyan('./purgetss/config.cjs')} with default values.`)
|
|
73
|
+
console.log(' Edit that block to customize brand defaults (bgColor, padding, etc.).')
|
|
74
|
+
console.log(' CLI flags always win over config values.')
|
|
75
|
+
console.log()
|
|
76
|
+
} catch (err) {
|
|
77
|
+
logger.warning(`Could not auto-add brand: section to config.cjs (${err.message}).`)
|
|
78
|
+
logger.warning('The command will still run using built-in defaults.')
|
|
79
|
+
}
|
|
80
|
+
}
|