purgetss 7.7.1 → 7.11.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 +28 -0
- package/bin/purgetss +23 -0
- package/dist/purgetss.ui.js +1 -1
- package/lib/templates/create/index.xml +1 -1
- package/lib/templates/purgetss.config.js.cjs +3 -1
- package/package.json +2 -2
- package/src/cli/commands/build.js +9 -4
- package/src/cli/commands/images.js +49 -2
- package/src/cli/commands/purge.js +31 -4
- package/src/cli/commands/shades.js +2 -2
- package/src/cli/utils/cli-helpers.js +15 -5
- package/src/cli/utils/unsupported-class-reporter.js +209 -0
- package/src/core/analyzers/class-extractor.js +54 -0
- package/src/core/analyzers/controller-svg-refs.js +154 -0
- package/src/core/branding/brand-config.js +7 -0
- package/src/core/branding/ensure-brand-section.js +4 -3
- package/src/core/branding/gen-feature-graphic.js +57 -0
- package/src/core/branding/index.js +28 -4
- package/src/core/branding/post-gen-notes.js +2 -2
- package/{experimental/completions2.js → src/core/builders/auto-utilities-builder.js} +74 -40
- package/src/core/builders/tailwind-builder.js +2 -2
- package/src/core/builders/tailwind-helpers.js +0 -444
- package/src/core/images/ensure-images-section.js +6 -4
- package/src/core/images/gen-scales.js +96 -13
- package/src/core/images/index.js +121 -9
- package/src/core/purger/icon-purger.js +7 -3
- package/src/core/purger/tailwind-purger.js +43 -5
- package/src/core/svg/cache.js +96 -0
- package/src/core/svg/derive-dimensions.js +120 -0
- package/src/core/svg/index.js +215 -0
- package/src/core/svg/resolve-classes.js +46 -0
- package/src/core/svg/sync-images.js +278 -0
- package/src/core/svg/tss-reader.js +134 -0
- package/src/dev/builders/tailwind-builder.js +3 -11
- package/src/shared/config-manager.js +72 -3
- package/src/shared/error-reporter.js +117 -0
- package/src/shared/helpers/colors.js +57 -13
- package/src/shared/helpers/core.js +0 -19
- package/src/shared/helpers/utils.js +146 -36
- package/src/shared/logger.js +12 -0
- package/src/shared/semantic-helpers.js +143 -0
- package/src/shared/validation/config-validator.js +167 -0
|
@@ -71,6 +71,60 @@ export function extractClasses(currentText, currentFile) {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Extract SVG image references from XML content.
|
|
76
|
+
*
|
|
77
|
+
* For each XML node whose `image` or `backgroundImage` attribute ends in `.svg`,
|
|
78
|
+
* capture the SVG src alongside the same node's `class` attribute (split into
|
|
79
|
+
* tokens). Powers the SVG image pipeline so it can pair each reference with the
|
|
80
|
+
* classes that determine its rendered size.
|
|
81
|
+
*
|
|
82
|
+
* Multiple references to the same SVG from different nodes are returned as
|
|
83
|
+
* separate entries — the caller is responsible for de-duplicating and reducing
|
|
84
|
+
* to a single resolved dimension.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} currentText - XML content to parse.
|
|
87
|
+
* @param {string} currentFile - File path for error reporting.
|
|
88
|
+
* @returns {Array<{ src: string, classes: string[] }>} References found.
|
|
89
|
+
*/
|
|
90
|
+
export function extractSvgRefsFromXml(currentText, currentFile) {
|
|
91
|
+
try {
|
|
92
|
+
const jsontext = convert.xml2json(encodeHTML(currentText), { compact: true })
|
|
93
|
+
const json = JSON.parse(jsontext)
|
|
94
|
+
const refs = []
|
|
95
|
+
walkXmlForSvgRefs(json, refs)
|
|
96
|
+
return refs
|
|
97
|
+
} catch (error) {
|
|
98
|
+
throw chalk.red(`::PurgeTSS:: Error processing: "${currentFile}"\n`, error)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function walkXmlForSvgRefs(node, out) {
|
|
103
|
+
if (!node || typeof node !== 'object') return
|
|
104
|
+
if (Array.isArray(node)) {
|
|
105
|
+
for (const item of node) walkXmlForSvgRefs(item, out)
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const attrs = node._attributes
|
|
110
|
+
if (attrs && typeof attrs === 'object') {
|
|
111
|
+
const candidates = []
|
|
112
|
+
if (typeof attrs.image === 'string') candidates.push(attrs.image)
|
|
113
|
+
if (typeof attrs.backgroundImage === 'string') candidates.push(attrs.backgroundImage)
|
|
114
|
+
for (const src of candidates) {
|
|
115
|
+
if (!src.toLowerCase().endsWith('.svg')) continue
|
|
116
|
+
const cls = typeof attrs.class === 'string' ? attrs.class : ''
|
|
117
|
+
const classes = cls.split(/\s+/).filter(Boolean)
|
|
118
|
+
out.push({ src, classes })
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const key of Object.keys(node)) {
|
|
123
|
+
if (key === '_attributes') continue
|
|
124
|
+
walkXmlForSvgRefs(node[key], out)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
74
128
|
/**
|
|
75
129
|
* Extract only classes from XML content - COPIED exactly from original extractClassesOnly() function
|
|
76
130
|
* NO CHANGES to logic, preserving 100% of original functionality
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - SVG reference extractor for controllers
|
|
3
|
+
*
|
|
4
|
+
* Companion to class-extractor.js for the SVG image pipeline. Walks the AST of
|
|
5
|
+
* each controller file looking for ObjectExpressions that pair an image (or
|
|
6
|
+
* backgroundImage) property pointing to an .svg with a sibling `classes`
|
|
7
|
+
* property. Typical shape:
|
|
8
|
+
*
|
|
9
|
+
* $.UI.create('ImageView', {
|
|
10
|
+
* image: '/images/logos/logo.svg',
|
|
11
|
+
* classes: 'w-32 h-auto'
|
|
12
|
+
* })
|
|
13
|
+
*
|
|
14
|
+
* Only the in-place shape counts — references built from concatenated/dynamic
|
|
15
|
+
* strings cannot be detected statically and must be declared manually in
|
|
16
|
+
* config.cjs > images.files (per the plan).
|
|
17
|
+
*
|
|
18
|
+
* @fileoverview Extract SVG references from controller .js files
|
|
19
|
+
* @author César Estrada
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import * as acorn from 'acorn'
|
|
23
|
+
|
|
24
|
+
const AST_META_KEYS = new Set(['type', 'loc', 'range', 'start', 'end', 'sourceType', 'comments'])
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse a controller's source and return SVG references paired with their
|
|
28
|
+
* `classes` siblings inside the same object literal.
|
|
29
|
+
*
|
|
30
|
+
* Falls back to a conservative regex scan if the parser rejects the source.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} data - Controller file content.
|
|
33
|
+
* @returns {Array<{ src: string, classes: string[] }>}
|
|
34
|
+
*/
|
|
35
|
+
export function extractSvgRefsFromController(data) {
|
|
36
|
+
try {
|
|
37
|
+
const ast = acorn.parse(data, {
|
|
38
|
+
ecmaVersion: 'latest',
|
|
39
|
+
sourceType: 'script',
|
|
40
|
+
allowReturnOutsideFunction: true,
|
|
41
|
+
allowAwaitOutsideFunction: true,
|
|
42
|
+
allowImportExportEverywhere: true,
|
|
43
|
+
allowHashBang: true
|
|
44
|
+
})
|
|
45
|
+
const out = []
|
|
46
|
+
walkAST(ast, out)
|
|
47
|
+
return out
|
|
48
|
+
} catch {
|
|
49
|
+
return extractSvgRefsRegex(data)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function walkAST(node, out) {
|
|
54
|
+
if (!node || typeof node !== 'object') return
|
|
55
|
+
if (Array.isArray(node)) {
|
|
56
|
+
for (const child of node) walkAST(child, out)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
if (!node.type) return
|
|
60
|
+
|
|
61
|
+
if (node.type === 'ObjectExpression') {
|
|
62
|
+
inspectObject(node, out)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const key of Object.keys(node)) {
|
|
66
|
+
if (AST_META_KEYS.has(key)) continue
|
|
67
|
+
walkAST(node[key], out)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function inspectObject(obj, out) {
|
|
72
|
+
let svgSrc = null
|
|
73
|
+
let classes = null
|
|
74
|
+
|
|
75
|
+
for (const prop of obj.properties) {
|
|
76
|
+
if (!prop || prop.type !== 'Property' || prop.computed || prop.shorthand) continue
|
|
77
|
+
const keyName = propKeyName(prop)
|
|
78
|
+
if (!keyName) continue
|
|
79
|
+
|
|
80
|
+
if (keyName === 'image' || keyName === 'backgroundImage') {
|
|
81
|
+
const literal = stringLiteralValue(prop.value)
|
|
82
|
+
if (literal && literal.toLowerCase().endsWith('.svg')) {
|
|
83
|
+
svgSrc = literal
|
|
84
|
+
}
|
|
85
|
+
} else if (keyName === 'classes') {
|
|
86
|
+
classes = collectClassTokens(prop.value)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (svgSrc) {
|
|
91
|
+
out.push({ src: svgSrc, classes: classes || [] })
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function propKeyName(prop) {
|
|
96
|
+
if (prop.key.type === 'Identifier') return prop.key.name
|
|
97
|
+
if (prop.key.type === 'Literal' && typeof prop.key.value === 'string') return prop.key.value
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function stringLiteralValue(node) {
|
|
102
|
+
if (!node) return null
|
|
103
|
+
if (node.type === 'Literal' && typeof node.value === 'string') return node.value
|
|
104
|
+
if (node.type === 'TemplateLiteral' && node.expressions.length === 0 && node.quasis.length === 1) {
|
|
105
|
+
const cooked = node.quasis[0].value.cooked
|
|
106
|
+
return typeof cooked === 'string' ? cooked : null
|
|
107
|
+
}
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function collectClassTokens(node) {
|
|
112
|
+
if (!node) return []
|
|
113
|
+
if (node.type === 'Literal' && typeof node.value === 'string') {
|
|
114
|
+
return node.value.split(/\s+/).filter(Boolean)
|
|
115
|
+
}
|
|
116
|
+
if (node.type === 'TemplateLiteral' && node.expressions.length === 0 && node.quasis.length === 1) {
|
|
117
|
+
const cooked = node.quasis[0].value.cooked
|
|
118
|
+
return typeof cooked === 'string' ? cooked.split(/\s+/).filter(Boolean) : []
|
|
119
|
+
}
|
|
120
|
+
if (node.type === 'ArrayExpression') {
|
|
121
|
+
const tokens = []
|
|
122
|
+
for (const el of node.elements) {
|
|
123
|
+
if (el && el.type === 'Literal' && typeof el.value === 'string') {
|
|
124
|
+
tokens.push(...el.value.split(/\s+/).filter(Boolean))
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return tokens
|
|
128
|
+
}
|
|
129
|
+
return []
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Conservative regex fallback: look for objects that pair both keys on the
|
|
133
|
+
// same {...} chunk. Misses anything spread across complex expressions, but the
|
|
134
|
+
// AST path already covers the realistic cases — this is just a safety net.
|
|
135
|
+
function extractSvgRefsRegex(data) {
|
|
136
|
+
const out = []
|
|
137
|
+
const objRegex = /\{[^{}]*\}/g
|
|
138
|
+
for (const match of data.matchAll(objRegex)) {
|
|
139
|
+
const chunk = match[0]
|
|
140
|
+
const imgMatch = chunk.match(/\b(?:image|backgroundImage)\s*:\s*['"`]([^'"`]+\.svg)['"`]/i)
|
|
141
|
+
if (!imgMatch) continue
|
|
142
|
+
const classesMatch = chunk.match(/\bclasses\s*:\s*(?:['"`]([^'"`]+)['"`]|\[([^\]]+)\])/)
|
|
143
|
+
let classes = []
|
|
144
|
+
if (classesMatch) {
|
|
145
|
+
const raw = classesMatch[1] || classesMatch[2] || ''
|
|
146
|
+
classes = raw
|
|
147
|
+
.split(/[,\s]+/)
|
|
148
|
+
.map(t => t.trim().replace(/^['"`]|['"`]$/g, ''))
|
|
149
|
+
.filter(Boolean)
|
|
150
|
+
}
|
|
151
|
+
out.push({ src: imgMatch[1], classes })
|
|
152
|
+
}
|
|
153
|
+
return out
|
|
154
|
+
}
|
|
@@ -66,6 +66,7 @@ export function resolveBrandConfig(cliOptions, cliLogo, projectRoot) {
|
|
|
66
66
|
?? 19
|
|
67
67
|
|
|
68
68
|
const androidLegacyPadding = cliOptions.androidLegacyPadding
|
|
69
|
+
?? cliOptions.padding
|
|
69
70
|
?? padding.androidLegacy
|
|
70
71
|
?? 10
|
|
71
72
|
|
|
@@ -73,6 +74,10 @@ export function resolveBrandConfig(cliOptions, cliLogo, projectRoot) {
|
|
|
73
74
|
?? padding.ios
|
|
74
75
|
?? 4
|
|
75
76
|
|
|
77
|
+
const featureGraphicPadding = cliOptions.featureGraphicPadding
|
|
78
|
+
?? padding.featureGraphic
|
|
79
|
+
?? 12
|
|
80
|
+
|
|
76
81
|
const bgColor = cliOptions.bgColor
|
|
77
82
|
?? colors.background
|
|
78
83
|
?? '#FFFFFF'
|
|
@@ -88,6 +93,7 @@ export function resolveBrandConfig(cliOptions, cliLogo, projectRoot) {
|
|
|
88
93
|
darkLogo: pickLogo(cliOptions.darkLogo, logos.iosDark, brandDir, 'logo-dark', projectRoot),
|
|
89
94
|
tintedLogo: pickLogo(cliOptions.tintedLogo, logos.iosTinted, brandDir, 'logo-tinted', projectRoot),
|
|
90
95
|
splashLogo: pickLogo(cliOptions.splashLogo, logos.androidSplash, brandDir, 'logo-splash', projectRoot),
|
|
96
|
+
featureLogo: pickLogo(cliOptions.featureLogo, logos.featureGraphic, brandDir, 'logo-feature', projectRoot),
|
|
91
97
|
|
|
92
98
|
bgColor,
|
|
93
99
|
bgColorExplicit: Boolean(cliOptions.bgColor ?? colors.background),
|
|
@@ -95,6 +101,7 @@ export function resolveBrandConfig(cliOptions, cliLogo, projectRoot) {
|
|
|
95
101
|
androidAdaptivePadding,
|
|
96
102
|
androidLegacyPadding,
|
|
97
103
|
iosPadding,
|
|
104
|
+
featureGraphicPadding,
|
|
98
105
|
|
|
99
106
|
// Kitchen-sink defaults: adaptive + marketplace are always generated; only
|
|
100
107
|
// notification and splash are opt-in. Config can pre-enable them.
|
|
@@ -25,9 +25,10 @@ import { logger } from './branding-logger.js'
|
|
|
25
25
|
const BRAND_BLOCK = ` brand: {
|
|
26
26
|
logos: {}, // empty = auto-discovers from purgetss/brand/
|
|
27
27
|
padding: {
|
|
28
|
-
ios: '4%',
|
|
29
|
-
androidLegacy: '10%',
|
|
30
|
-
androidAdaptive: '19%' // adaptive foreground padding near the Android safe-zone
|
|
28
|
+
ios: '4%', // iOS aesthetic. Range: 2% bold — 8% conservative. No launcher mask.
|
|
29
|
+
androidLegacy: '10%', // legacy ic_launcher.png padding
|
|
30
|
+
androidAdaptive: '19%', // adaptive foreground padding near the Android safe-zone
|
|
31
|
+
featureGraphic: '12%' // Google Play Feature Graphic vertical padding (1024×500)
|
|
31
32
|
},
|
|
32
33
|
android: {
|
|
33
34
|
splash: false, // also generate splash_icon.png × 5
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - gen-feature-graphic
|
|
3
|
+
*
|
|
4
|
+
* Google Play Feature Graphic:
|
|
5
|
+
* MarketplaceArtworkFeature.png 1024×500 (Play Store listing top banner)
|
|
6
|
+
*
|
|
7
|
+
* Always flattened on bgColor — Google Play requires opaque artwork.
|
|
8
|
+
*
|
|
9
|
+
* Layout: a square logo block centered both horizontally and vertically inside
|
|
10
|
+
* the 1024×500 canvas. Padding is vertical-driven (top/bottom) — the inner
|
|
11
|
+
* box becomes side = 500 - 2*pad. The logo is scaled with `fit: 'inside'`
|
|
12
|
+
* so wide/tall logos preserve aspect ratio inside that square.
|
|
13
|
+
*
|
|
14
|
+
* @fileoverview Google Play Feature Graphic for Titanium branding
|
|
15
|
+
* @author César Estrada
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from 'fs'
|
|
19
|
+
import path from 'path'
|
|
20
|
+
import sharp from 'sharp'
|
|
21
|
+
|
|
22
|
+
const CANVAS_WIDTH = 1024
|
|
23
|
+
const CANVAS_HEIGHT = 500
|
|
24
|
+
|
|
25
|
+
export async function genFeatureGraphic(featureMaster, paddingPct, outRoot, opts = {}) {
|
|
26
|
+
const { bgColor = '#FFFFFF' } = opts
|
|
27
|
+
fs.mkdirSync(outRoot, { recursive: true })
|
|
28
|
+
|
|
29
|
+
const padPx = Math.floor((CANVAS_HEIGHT * paddingPct) / 100)
|
|
30
|
+
const inner = CANVAS_HEIGHT - 2 * padPx
|
|
31
|
+
const outPath = path.join(outRoot, 'MarketplaceArtworkFeature.png')
|
|
32
|
+
|
|
33
|
+
const resized = await sharp(featureMaster)
|
|
34
|
+
.resize({
|
|
35
|
+
width: inner,
|
|
36
|
+
height: inner,
|
|
37
|
+
fit: 'inside',
|
|
38
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
39
|
+
})
|
|
40
|
+
.toBuffer()
|
|
41
|
+
|
|
42
|
+
await sharp({
|
|
43
|
+
create: {
|
|
44
|
+
width: CANVAS_WIDTH,
|
|
45
|
+
height: CANVAS_HEIGHT,
|
|
46
|
+
channels: 4,
|
|
47
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
.composite([{ input: resized, gravity: 'center' }])
|
|
51
|
+
.flatten({ background: bgColor })
|
|
52
|
+
.removeAlpha()
|
|
53
|
+
.png({ compressionLevel: 9 })
|
|
54
|
+
.toFile(outPath)
|
|
55
|
+
|
|
56
|
+
return outPath
|
|
57
|
+
}
|
|
@@ -48,6 +48,7 @@ import { genAndroidAdaptive } from './gen-android-adaptive.js'
|
|
|
48
48
|
import { genAndroidLegacy } from './gen-android-legacy.js'
|
|
49
49
|
import { genAndroidDefault } from './gen-android-default.js'
|
|
50
50
|
import { genMarketplace } from './gen-marketplace.js'
|
|
51
|
+
import { genFeatureGraphic } from './gen-feature-graphic.js'
|
|
51
52
|
import { genNotification } from './gen-notification.js'
|
|
52
53
|
import { genSplash } from './gen-splash.js'
|
|
53
54
|
import { genIcLauncherXml } from './gen-ic-launcher-xml.js'
|
|
@@ -60,6 +61,7 @@ export async function runBranding(opts) {
|
|
|
60
61
|
logo,
|
|
61
62
|
iconLogo = null,
|
|
62
63
|
splashLogo = null,
|
|
64
|
+
featureLogo = null,
|
|
63
65
|
monochromeLogo = null,
|
|
64
66
|
darkLogo = null,
|
|
65
67
|
darkBgColor = null,
|
|
@@ -71,6 +73,7 @@ export async function runBranding(opts) {
|
|
|
71
73
|
androidAdaptivePadding = 19,
|
|
72
74
|
androidLegacyPadding = 10,
|
|
73
75
|
iosPadding = 4,
|
|
76
|
+
featureGraphicPadding = 12,
|
|
74
77
|
notification = false,
|
|
75
78
|
splash = false,
|
|
76
79
|
cleanupLegacy: runCleanup = false,
|
|
@@ -84,7 +87,7 @@ export async function runBranding(opts) {
|
|
|
84
87
|
confirmOverwrites = true
|
|
85
88
|
} = opts
|
|
86
89
|
|
|
87
|
-
validateOptions({ logo, bgColor, darkBgColor, androidAdaptivePadding, androidLegacyPadding, iosPadding, cleanupLegacy: runCleanup })
|
|
90
|
+
validateOptions({ logo, bgColor, darkBgColor, androidAdaptivePadding, androidLegacyPadding, iosPadding, featureGraphicPadding, cleanupLegacy: runCleanup })
|
|
88
91
|
|
|
89
92
|
const projectType = detectProjectType(projectRoot)
|
|
90
93
|
const isInPlace = inPlace && !output
|
|
@@ -97,7 +100,7 @@ export async function runBranding(opts) {
|
|
|
97
100
|
if (logo) {
|
|
98
101
|
logger.property('Logo: ', logo)
|
|
99
102
|
logger.property('Background: ', bgColor)
|
|
100
|
-
logger.property('Padding: ', `Android adaptive ${androidAdaptivePadding}% / Android legacy ${androidLegacyPadding}% / iOS ${iosPadding}% per side`)
|
|
103
|
+
logger.property('Padding: ', `Android adaptive ${androidAdaptivePadding}% / Android legacy ${androidLegacyPadding}% / iOS ${iosPadding}% per side / Feature Graphic ${featureGraphicPadding}% vertical`)
|
|
101
104
|
console.log()
|
|
102
105
|
logger.property(isInPlace ? 'Writing IN PLACE to: ' : 'Staging: ', isInPlace ? projectRoot : stagingRoot)
|
|
103
106
|
}
|
|
@@ -159,6 +162,8 @@ export async function runBranding(opts) {
|
|
|
159
162
|
lines.push(`${stagingRoot}/DefaultIcon-Tinted.png (${tintedSrc})`)
|
|
160
163
|
}
|
|
161
164
|
lines.push(`${stagingRoot}/iTunesConnect.png + MarketplaceArtwork.png`)
|
|
165
|
+
const featureSrc = featureLogo ? `from ${featureLogo}` : 'from main logo'
|
|
166
|
+
lines.push(`${stagingRoot}/MarketplaceArtworkFeature.png (${featureSrc}, ${featureGraphicPadding}% vertical padding)`)
|
|
162
167
|
lines.push(`${androidResStaging}/mipmap-{mdpi,hdpi,xhdpi,xxhdpi,xxxhdpi}/ic_launcher_{foreground,background,monochrome}.png`)
|
|
163
168
|
lines.push(`${androidResStaging}/mipmap-{...}/ic_launcher.png (legacy)`)
|
|
164
169
|
lines.push(`${androidResStaging}/mipmap-anydpi-v26/ic_launcher.xml`)
|
|
@@ -265,6 +270,20 @@ export async function runBranding(opts) {
|
|
|
265
270
|
})
|
|
266
271
|
generated.push(mkt.itunesConnect, mkt.marketplaceArtwork)
|
|
267
272
|
|
|
273
|
+
let featureMaster = tight
|
|
274
|
+
if (featureLogo) {
|
|
275
|
+
if (!fs.existsSync(featureLogo)) {
|
|
276
|
+
throw new Error(`Feature Graphic logo not found: ${featureLogo}`)
|
|
277
|
+
}
|
|
278
|
+
const featureBase = path.join(tempDir, '_logo_feature')
|
|
279
|
+
const featureResult = await prepareMaster(featureLogo, featureBase)
|
|
280
|
+
featureMaster = featureResult.tight
|
|
281
|
+
}
|
|
282
|
+
const featureSrcLabel = featureLogo ? 'from --feature-logo' : 'from main logo'
|
|
283
|
+
logger.bullet(`MarketplaceArtworkFeature.png (1024×500, ${featureSrcLabel}, ${featureGraphicPadding}% vertical padding, flattened on ${bgColor})`)
|
|
284
|
+
const featurePath = await genFeatureGraphic(featureMaster, featureGraphicPadding, stagingRoot, { bgColor })
|
|
285
|
+
generated.push(featurePath)
|
|
286
|
+
|
|
268
287
|
// ---- Section: Android --------------------------------------------------
|
|
269
288
|
logger.section('Android')
|
|
270
289
|
|
|
@@ -323,7 +342,9 @@ export async function runBranding(opts) {
|
|
|
323
342
|
path.join(tempDir, '_logo_tinted_square.png'),
|
|
324
343
|
path.join(tempDir, '_logo_tinted_tight.png'),
|
|
325
344
|
path.join(tempDir, '_logo_splash_square.png'),
|
|
326
|
-
path.join(tempDir, '_logo_splash_tight.png')
|
|
345
|
+
path.join(tempDir, '_logo_splash_tight.png'),
|
|
346
|
+
path.join(tempDir, '_logo_feature_square.png'),
|
|
347
|
+
path.join(tempDir, '_logo_feature_tight.png')
|
|
327
348
|
]
|
|
328
349
|
for (const tmp of tmpFiles) {
|
|
329
350
|
if (fs.existsSync(tmp)) fs.unlinkSync(tmp)
|
|
@@ -385,7 +406,7 @@ function getStagingAndroidAssetsRoot(stagingRoot, projectType) {
|
|
|
385
406
|
return null
|
|
386
407
|
}
|
|
387
408
|
|
|
388
|
-
function validateOptions({ logo, bgColor, darkBgColor, androidAdaptivePadding, androidLegacyPadding, iosPadding, cleanupLegacy }) {
|
|
409
|
+
function validateOptions({ logo, bgColor, darkBgColor, androidAdaptivePadding, androidLegacyPadding, iosPadding, featureGraphicPadding, cleanupLegacy }) {
|
|
389
410
|
if (!logo && !cleanupLegacy) {
|
|
390
411
|
throw new Error('Logo image path is required (unless using --cleanup-legacy alone).')
|
|
391
412
|
}
|
|
@@ -404,4 +425,7 @@ function validateOptions({ logo, bgColor, darkBgColor, androidAdaptivePadding, a
|
|
|
404
425
|
if (iosPadding < 0 || iosPadding > 40) {
|
|
405
426
|
throw new Error(`--ios-padding must be between 0 and 40 (got: ${iosPadding}).`)
|
|
406
427
|
}
|
|
428
|
+
if (featureGraphicPadding < 0 || featureGraphicPadding > 40) {
|
|
429
|
+
throw new Error(`--feature-graphic-padding must be between 0 and 40 (got: ${featureGraphicPadding}).`)
|
|
430
|
+
}
|
|
407
431
|
}
|
|
@@ -37,12 +37,12 @@ function printCompactSummary(opts) {
|
|
|
37
37
|
logger.bullet(`Rebuild: ${chalk.gray('ti clean && ti build -p android -T emulator')}`)
|
|
38
38
|
} else if (projectType === 'alloy') {
|
|
39
39
|
logger.bullet(`Preview in ${chalk.yellow('Preview.app')}, then copy to project:`)
|
|
40
|
-
console.log(chalk.gray(` cp ${stagingRoot}/{DefaultIcon,DefaultIcon-ios,DefaultIcon-Dark,DefaultIcon-Tinted,iTunesConnect,MarketplaceArtwork}.png ${projectRoot}/`))
|
|
40
|
+
console.log(chalk.gray(` cp ${stagingRoot}/{DefaultIcon,DefaultIcon-ios,DefaultIcon-Dark,DefaultIcon-Tinted,iTunesConnect,MarketplaceArtwork,MarketplaceArtworkFeature}.png ${projectRoot}/`))
|
|
41
41
|
console.log(chalk.gray(` cp -R ${stagingRoot}/app/platform/android/res/. ${projectRoot}/app/platform/android/res/`))
|
|
42
42
|
logger.bullet(`Cleanup staging: ${chalk.gray('rm -rf ' + stagingRoot)}`)
|
|
43
43
|
} else if (projectType === 'classic') {
|
|
44
44
|
logger.bullet(`Preview in ${chalk.yellow('Preview.app')}, then copy to project:`)
|
|
45
|
-
console.log(chalk.gray(` cp ${stagingRoot}/{DefaultIcon,DefaultIcon-ios,DefaultIcon-Dark,DefaultIcon-Tinted,iTunesConnect,MarketplaceArtwork}.png ${projectRoot}/`))
|
|
45
|
+
console.log(chalk.gray(` cp ${stagingRoot}/{DefaultIcon,DefaultIcon-ios,DefaultIcon-Dark,DefaultIcon-Tinted,iTunesConnect,MarketplaceArtwork,MarketplaceArtworkFeature}.png ${projectRoot}/`))
|
|
46
46
|
console.log(chalk.gray(` cp -R ${stagingRoot}/platform/android/res/. ${projectRoot}/platform/android/res/`))
|
|
47
47
|
logger.bullet(`Cleanup staging: ${chalk.gray('rm -rf ' + stagingRoot)}`)
|
|
48
48
|
} else {
|
|
@@ -7,28 +7,21 @@ import path from 'path'
|
|
|
7
7
|
import { fileURLToPath } from 'url'
|
|
8
8
|
import { createRequire } from 'module'
|
|
9
9
|
import _ from 'lodash'
|
|
10
|
-
import chalk from 'chalk'
|
|
11
10
|
let saveGlossary = false
|
|
12
11
|
|
|
13
12
|
const __filename = fileURLToPath(import.meta.url)
|
|
14
13
|
const __dirname = path.dirname(__filename)
|
|
15
14
|
const require = createRequire(import.meta.url)
|
|
16
15
|
const cwd = process.cwd()
|
|
17
|
-
import { colores } from '
|
|
16
|
+
import { colores } from '../../shared/brand-colors.js'
|
|
18
17
|
export { colores }
|
|
19
|
-
const purgeLabel = colores.purgeLabel
|
|
20
18
|
|
|
21
|
-
import * as helpers from '
|
|
22
|
-
import { getConfigFile } from '
|
|
23
|
-
import { projectsConfigJS } from '
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
info: (...args) => console.log(purgeLabel, args.join(' ')),
|
|
28
|
-
warn: (...args) => console.log(purgeLabel, chalk.yellow(args.join(' '))),
|
|
29
|
-
error: (...args) => console.log(purgeLabel, chalk.red(args.join(' '))),
|
|
30
|
-
file: (...args) => console.log(purgeLabel, chalk.yellow(args.join(' ')), 'file created!')
|
|
31
|
-
}
|
|
19
|
+
import * as helpers from '../../shared/helpers.js'
|
|
20
|
+
import { getConfigFile } from '../../shared/config-manager.js'
|
|
21
|
+
import { projectsConfigJS } from '../../shared/constants.js'
|
|
22
|
+
import { logger } from '../../shared/logger.js'
|
|
23
|
+
import { registerSemanticName } from '../../shared/semantic-helpers.js'
|
|
24
|
+
const tiCompletionsFile = require('../../../lib/completions/titanium/completions-v3.json')
|
|
32
25
|
|
|
33
26
|
// Keys whose numeric values are interpreted with `ti.ui.defaultunit` from tiapp.xml.
|
|
34
27
|
// The glossary .md files for these keys receive an inline "// Unit: ..." note
|
|
@@ -174,7 +167,7 @@ function buildSubfolderIndex(folder) {
|
|
|
174
167
|
}
|
|
175
168
|
|
|
176
169
|
function glossaryBaseFolder() {
|
|
177
|
-
if (!fs.existsSync(projectsConfigJS)) return path.resolve(__dirname, '
|
|
170
|
+
if (!fs.existsSync(projectsConfigJS)) return path.resolve(__dirname, '../../../dist/glossary/')
|
|
178
171
|
if (saveGlossary) return cwd + '/purgetss/glossary/'
|
|
179
172
|
return ''
|
|
180
173
|
}
|
|
@@ -206,7 +199,20 @@ function scaffoldGlossary() {
|
|
|
206
199
|
}
|
|
207
200
|
}
|
|
208
201
|
|
|
209
|
-
|
|
202
|
+
// getConfigFile() runs at module import time, before bin/purgetss's catch
|
|
203
|
+
// handler is wired up. If the config validator throws a presentable error
|
|
204
|
+
// (isSyntaxError), print it cleanly and exit instead of letting Node surface
|
|
205
|
+
// the raw stack on top of an already-formatted report.
|
|
206
|
+
let configFile
|
|
207
|
+
try {
|
|
208
|
+
configFile = getConfigFile()
|
|
209
|
+
} catch (err) {
|
|
210
|
+
if (err && err.isSyntaxError) {
|
|
211
|
+
console.error(err.message)
|
|
212
|
+
process.exit(1)
|
|
213
|
+
}
|
|
214
|
+
throw err
|
|
215
|
+
}
|
|
210
216
|
configFile.purge = configFile.purge ?? { mode: 'all' }
|
|
211
217
|
configFile.theme = configFile.theme ?? {}
|
|
212
218
|
configFile.theme.extend = configFile.theme.extend ?? {}
|
|
@@ -236,7 +242,7 @@ function autoBuildUtilitiesTSS(options = {}) {
|
|
|
236
242
|
|
|
237
243
|
saveGlossary = options.glossary ?? false
|
|
238
244
|
scaffoldGlossary()
|
|
239
|
-
let tailwindStyles = fs.readFileSync(path.resolve(__dirname, '
|
|
245
|
+
let tailwindStyles = fs.readFileSync(path.resolve(__dirname, '../../../lib/templates/tailwind/custom-template.tss'), 'utf8')
|
|
240
246
|
tailwindStyles += (fs.existsSync(projectsConfigJS)) ? `// config.js file updated on: ${getFileUpdatedDate(projectsConfigJS)}\n` : '// default config.js file\n'
|
|
241
247
|
|
|
242
248
|
const baseValues = combineDefaultThemeWithConfigFile()
|
|
@@ -255,7 +261,7 @@ function autoBuildUtilitiesTSS(options = {}) {
|
|
|
255
261
|
saveFile(cwd + '/purgetss/styles/utilities.tss', tailwindStyles)
|
|
256
262
|
logger.file('./purgetss/styles/utilities.tss')
|
|
257
263
|
} else {
|
|
258
|
-
saveFile(path.resolve(__dirname, '
|
|
264
|
+
saveFile(path.resolve(__dirname, '../../../dist/utilities.tss'), tailwindStyles)
|
|
259
265
|
logger.file('./dist/utilities.tss')
|
|
260
266
|
}
|
|
261
267
|
}
|
|
@@ -311,7 +317,7 @@ function processCompletionsClasses(_completionsWithBaseValues) {
|
|
|
311
317
|
|
|
312
318
|
function generateGlossary(_key, _theClasses, _keyName = null) {
|
|
313
319
|
let baseDestinationFolder = ''
|
|
314
|
-
if (!fs.existsSync(projectsConfigJS)) baseDestinationFolder = path.resolve(__dirname, '
|
|
320
|
+
if (!fs.existsSync(projectsConfigJS)) baseDestinationFolder = path.resolve(__dirname, '../../../dist/glossary/')
|
|
315
321
|
else if (saveGlossary) baseDestinationFolder = cwd + '/purgetss/glossary/'
|
|
316
322
|
|
|
317
323
|
if (baseDestinationFolder !== '') {
|
|
@@ -378,7 +384,7 @@ function getTiUIComponents(_base) {
|
|
|
378
384
|
|
|
379
385
|
function processCompoundClasses({ ..._base }) {
|
|
380
386
|
let compoundClasses = ''
|
|
381
|
-
const compoundTemplate = require('
|
|
387
|
+
const compoundTemplate = require('../../../lib/templates/tailwind/compoundTemplate.json')
|
|
382
388
|
|
|
383
389
|
_.each(compoundTemplate, (value, key) => {
|
|
384
390
|
compoundClasses += generateGlossary(key, helpers.processProperties(value.description, value.template, value.base ?? { default: _base[key] }))
|
|
@@ -534,6 +540,11 @@ function combineDefaultThemeWithConfigFile() {
|
|
|
534
540
|
}
|
|
535
541
|
|
|
536
542
|
_.merge(base.colors, themeOrDefaultValues.colors, configFile.theme.extend.colors)
|
|
543
|
+
// Track semantic color names so opacity modifiers (bg-X/65) can later
|
|
544
|
+
// auto-derive an alpha-applied entry in semantic.colors.json. A value is
|
|
545
|
+
// "semantic" when it's a string that isn't a hex literal or a Ti reserved
|
|
546
|
+
// keyword.
|
|
547
|
+
_.each(base.colors, value => collectSemanticReferences(value))
|
|
537
548
|
_.merge(base.size, themeOrDefaultValues.spacing, configFile.theme.extend.spacing)
|
|
538
549
|
_.merge(base.spacing, themeOrDefaultValues.spacing, configFile.theme.extend.spacing)
|
|
539
550
|
|
|
@@ -572,8 +583,16 @@ function combineDefaultThemeWithConfigFile() {
|
|
|
572
583
|
delete base.zIndex.auto
|
|
573
584
|
|
|
574
585
|
// ! Process custom Window, View and ImageView
|
|
575
|
-
//
|
|
586
|
+
// Track whether the user defined each Ti Element at the theme.X (replace) level
|
|
587
|
+
// BEFORE merging extend into theme. This mirrors the Tailwind convention:
|
|
588
|
+
// theme.X → REPLACE the framework's defaults entirely
|
|
589
|
+
// theme.extend.X → MERGE with the framework's defaults
|
|
590
|
+
// Without this distinction, presets (like Window's backgroundColor: '#FFFFFF')
|
|
591
|
+
// leak into a strict-replace config and surface as ghost properties in app.tss.
|
|
592
|
+
const userReplaced = {}
|
|
576
593
|
_.each(['Window', 'View', 'ImageView'], comp => {
|
|
594
|
+
userReplaced[comp] = !!configFile.theme[comp] && !configFile.theme.extend[comp]
|
|
595
|
+
|
|
577
596
|
if (configFile.theme.extend[comp]) {
|
|
578
597
|
configFile.theme[comp] = _.merge({}, configFile.theme[comp], configFile.theme.extend[comp])
|
|
579
598
|
delete configFile.theme.extend[comp]
|
|
@@ -584,11 +603,18 @@ function combineDefaultThemeWithConfigFile() {
|
|
|
584
603
|
}
|
|
585
604
|
})
|
|
586
605
|
|
|
587
|
-
//
|
|
588
|
-
//
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
606
|
+
// Apply framework defaults only when NOT in strict-replace mode. In replace
|
|
607
|
+
// mode the user's config is the single source of truth — presets must not be
|
|
608
|
+
// silently re-injected.
|
|
609
|
+
if (!userReplaced.Window) {
|
|
610
|
+
configFile.theme.Window = _.merge({ default: { backgroundColor: '#FFFFFF' } }, configFile.theme.Window)
|
|
611
|
+
}
|
|
612
|
+
if (!userReplaced.ImageView) {
|
|
613
|
+
configFile.theme.ImageView = _.merge({ ios: { hires: true } }, configFile.theme.ImageView)
|
|
614
|
+
}
|
|
615
|
+
if (!userReplaced.View) {
|
|
616
|
+
configFile.theme.View = _.merge({ default: { width: 'Ti.UI.SIZE', height: 'Ti.UI.SIZE' } }, configFile.theme.View)
|
|
617
|
+
}
|
|
592
618
|
|
|
593
619
|
base.Window = configFile.theme.Window
|
|
594
620
|
base.ImageView = configFile.theme.ImageView
|
|
@@ -610,6 +636,22 @@ function checkDeletePlugins() {
|
|
|
610
636
|
return Array.isArray(deletePlugins) ? deletePlugins : Object.keys(deletePlugins).map(key => key)
|
|
611
637
|
}
|
|
612
638
|
|
|
639
|
+
// Walk a color config value (possibly nested object of shades) and register
|
|
640
|
+
// any leaf string that points to a semantic color name in
|
|
641
|
+
// `semantic.colors.json` (e.g. `surface: 'surfaceColor'`,
|
|
642
|
+
// `brand: { DEFAULT: 'brandColor' }`).
|
|
643
|
+
const _semanticReservedValues = new Set(['transparent', 'currentColor', 'inherit'])
|
|
644
|
+
function collectSemanticReferences(value) {
|
|
645
|
+
if (typeof value === 'string') {
|
|
646
|
+
if (value.startsWith('#') || _semanticReservedValues.has(value)) return
|
|
647
|
+
registerSemanticName(value)
|
|
648
|
+
return
|
|
649
|
+
}
|
|
650
|
+
if (value && typeof value === 'object') {
|
|
651
|
+
_.each(value, v => collectSemanticReferences(v))
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
613
655
|
// ! Helper Functions
|
|
614
656
|
function removeDeprecatedColors(theObject) {
|
|
615
657
|
delete theObject.blueGray
|
|
@@ -808,15 +850,14 @@ function generateCombinedClasses(key, data) {
|
|
|
808
850
|
const comments = processComments(key, data)
|
|
809
851
|
|
|
810
852
|
if (Object.entries(data.base).length) {
|
|
811
|
-
|
|
812
|
-
if (typeof value === 'object') {
|
|
813
|
-
_.each(value, (
|
|
814
|
-
myClasses += `'.${setModifier(removeUneededVariablesFromPropertyName(camelCaseToDash(key + '-' + _key + '-' + __key)))}': { ${key}: ${helpers.parseValue(_value)} }\n`
|
|
815
|
-
})
|
|
853
|
+
const walk = (value, segments) => {
|
|
854
|
+
if (value && typeof value === 'object') {
|
|
855
|
+
_.each(value, (childValue, childKey) => walk(childValue, [...segments, childKey]))
|
|
816
856
|
} else {
|
|
817
|
-
myClasses += `'.${setModifier(removeUneededVariablesFromPropertyName(camelCaseToDash(
|
|
857
|
+
myClasses += `'.${setModifier(removeUneededVariablesFromPropertyName(camelCaseToDash(segments.join('-'))))}': { ${key}: ${helpers.parseValue(value)} }\n`
|
|
818
858
|
}
|
|
819
|
-
}
|
|
859
|
+
}
|
|
860
|
+
walk(data.base, [key])
|
|
820
861
|
} else {
|
|
821
862
|
_.each(data.values, (_value, _key) => {
|
|
822
863
|
if (!_value.includes('deprecated')) myClasses += formatClass(key, _value, data.type === 'Array')
|
|
@@ -828,13 +869,6 @@ function generateCombinedClasses(key, data) {
|
|
|
828
869
|
return false
|
|
829
870
|
}
|
|
830
871
|
|
|
831
|
-
function saveAutoTSS(key, classes) {
|
|
832
|
-
if (fs.existsSync(projectsConfigJS) && saveGlossary) {
|
|
833
|
-
makeSureFolderExists(cwd + '/purgetss/experimental/tailwind-classes/')
|
|
834
|
-
saveFile(cwd + `/purgetss/experimental/tailwind-classes/${key}.tss`, classes)
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
|
|
838
872
|
// inputType is marked as Array in completions but accepts a single value
|
|
839
873
|
const nonArrayOverrides = new Set(['inputType'])
|
|
840
874
|
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
// Import functions from their new modular locations
|
|
10
10
|
import * as helpers from '../../shared/helpers.js'
|
|
11
|
-
import { autoBuildUtilitiesTSS } from '
|
|
11
|
+
import { autoBuildUtilitiesTSS } from './auto-utilities-builder.js'
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Build Tailwind (
|
|
14
|
+
* Build Tailwind (Auto-builds utilities.tss from config.cjs (active production path))
|
|
15
15
|
* @param {Object} options - Build options
|
|
16
16
|
*/
|
|
17
17
|
export function buildTailwind(options) {
|