purgetss 7.9.0 → 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 +21 -1
- package/bin/purgetss +13 -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 +1 -1
- package/src/cli/commands/images.js +41 -2
- package/src/cli/commands/purge.js +15 -2
- package/src/cli/utils/cli-helpers.js +15 -5
- package/src/cli/utils/unsupported-class-reporter.js +3 -3
- 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/src/core/builders/auto-utilities-builder.js +20 -15
- package/src/core/images/ensure-images-section.js +6 -4
- package/src/core/images/gen-scales.js +82 -17
- package/src/core/images/index.js +117 -12
- package/src/core/purger/icon-purger.js +7 -3
- package/src/core/purger/tailwind-purger.js +3 -1
- 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 +18 -0
- 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/utils.js +46 -8
- package/src/shared/logger.js +12 -0
- package/src/shared/validation/config-validator.js +167 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - SVG pipeline cache
|
|
3
|
+
*
|
|
4
|
+
* Tracks the last successful generation of each SVG so subsequent runs can
|
|
5
|
+
* skip work when nothing changed. Cached file: `purgetss/.cache/svg-images.json`.
|
|
6
|
+
*
|
|
7
|
+
* An entry is invalidated (regen) when any of these change:
|
|
8
|
+
* - The SVG file content (sha1 hash).
|
|
9
|
+
* - The resolved widthDp / heightDp.
|
|
10
|
+
* - The list of target PNG paths.
|
|
11
|
+
* - Any of the target PNGs is missing on disk.
|
|
12
|
+
*
|
|
13
|
+
* Stale entries for SVGs no longer referenced are left untouched here — the
|
|
14
|
+
* planned `purgetss clean` command is responsible for purging orphans.
|
|
15
|
+
*
|
|
16
|
+
* @fileoverview Idempotency cache for the SVG pipeline
|
|
17
|
+
* @author César Estrada
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fs from 'fs'
|
|
21
|
+
import path from 'path'
|
|
22
|
+
import crypto from 'crypto'
|
|
23
|
+
import { projectsPurgeTSSFolder } from '../../shared/constants.js'
|
|
24
|
+
|
|
25
|
+
const CACHE_DIR = path.join(projectsPurgeTSSFolder, '.cache')
|
|
26
|
+
const CACHE_FILE = path.join(CACHE_DIR, 'svg-images.json')
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Load the on-disk cache as a plain object. Returns `{}` if the file is
|
|
30
|
+
* missing or unreadable — corrupted caches simply force a regen rather than
|
|
31
|
+
* halting the build.
|
|
32
|
+
*/
|
|
33
|
+
export function loadCache() {
|
|
34
|
+
try {
|
|
35
|
+
if (!fs.existsSync(CACHE_FILE)) return {}
|
|
36
|
+
const raw = fs.readFileSync(CACHE_FILE, 'utf8')
|
|
37
|
+
const parsed = JSON.parse(raw)
|
|
38
|
+
return (parsed && typeof parsed === 'object') ? parsed : {}
|
|
39
|
+
} catch {
|
|
40
|
+
return {}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Persist the cache to disk, creating `.cache/` if needed.
|
|
46
|
+
*/
|
|
47
|
+
export function saveCache(cache) {
|
|
48
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true })
|
|
49
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2) + '\n', 'utf8')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Compute the sha1 hash of an SVG file. Reads the bytes verbatim so
|
|
54
|
+
* whitespace-only edits also bust the cache (desired — they may change rendering).
|
|
55
|
+
*/
|
|
56
|
+
export function hashFile(absPath) {
|
|
57
|
+
const buf = fs.readFileSync(absPath)
|
|
58
|
+
return crypto.createHash('sha1').update(buf).digest('hex')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Decide whether the cached entry is still valid for the given resolved inputs.
|
|
63
|
+
*
|
|
64
|
+
* @param {Object} cached - Previous cache entry (or undefined).
|
|
65
|
+
* @param {string} svgHash - sha1 of the current SVG.
|
|
66
|
+
* @param {number} widthDp - Currently resolved widthDp.
|
|
67
|
+
* @param {number} heightDp - Currently resolved heightDp.
|
|
68
|
+
* @param {string[]} targets - Absolute paths the pipeline would emit this run.
|
|
69
|
+
* @returns {boolean} True if every target PNG is present and inputs match.
|
|
70
|
+
*/
|
|
71
|
+
export function isCacheHit(cached, svgHash, widthDp, heightDp, targets) {
|
|
72
|
+
if (!cached) return false
|
|
73
|
+
if (cached.svgHash !== svgHash) return false
|
|
74
|
+
if (cached.widthDp !== widthDp) return false
|
|
75
|
+
if (cached.heightDp !== heightDp) return false
|
|
76
|
+
if (!Array.isArray(cached.targets) || cached.targets.length !== targets.length) return false
|
|
77
|
+
|
|
78
|
+
const cachedPaths = new Set(cached.targets.map(t => (typeof t === 'string' ? t : t.path)))
|
|
79
|
+
for (const t of targets) {
|
|
80
|
+
if (!cachedPaths.has(t)) return false
|
|
81
|
+
if (!fs.existsSync(t)) return false
|
|
82
|
+
}
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build the cache entry shape we persist after a successful generation.
|
|
88
|
+
*/
|
|
89
|
+
export function makeCacheEntry(svgHash, widthDp, heightDp, targets) {
|
|
90
|
+
return {
|
|
91
|
+
svgHash,
|
|
92
|
+
widthDp,
|
|
93
|
+
heightDp,
|
|
94
|
+
targets: [...targets]
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - SVG dimension deriver
|
|
3
|
+
*
|
|
4
|
+
* Reduces all the references to a single SVG into one final `{ widthDp, heightDp }`
|
|
5
|
+
* pair that the image generator can consume as the `1×` baseline. Strategy:
|
|
6
|
+
*
|
|
7
|
+
* 1. For each reference, resolve the cascade against the purged app.tss
|
|
8
|
+
* class→props map.
|
|
9
|
+
* 2. Collect every numeric width across references; the SVG's width is the
|
|
10
|
+
* max (a single view rendered at 800dp can't share PNGs with a view at
|
|
11
|
+
* 128dp without visible blur).
|
|
12
|
+
* 3. Resolve height: explicit numeric width wins; otherwise fall back to a
|
|
13
|
+
* proportional value derived from the SVG's viewBox aspect ratio.
|
|
14
|
+
*
|
|
15
|
+
* Edge cases handled:
|
|
16
|
+
* - Every reference resolves to a non-numeric width (`Ti.UI.SIZE`, percentage,
|
|
17
|
+
* unknown class, etc.) → SVG is skipped with a warning.
|
|
18
|
+
* - SVG file missing in `purgetss/images/` → skipped with a warning.
|
|
19
|
+
* - SVG has an invalid/missing viewBox → skipped with an error.
|
|
20
|
+
*
|
|
21
|
+
* @fileoverview Per-SVG dimension reducer that feeds the image generator
|
|
22
|
+
* @author César Estrada
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import fs from 'fs'
|
|
26
|
+
import path from 'path'
|
|
27
|
+
import { resolveDimensions } from './resolve-classes.js'
|
|
28
|
+
import { readSvgSafely } from '../../shared/svg-utils.js'
|
|
29
|
+
|
|
30
|
+
// Hard cap on the largest target PNG we are willing to emit, applied at the
|
|
31
|
+
// `xxxhdpi` / `@3x` step downstream. Keeps Sharp from producing absurdly large
|
|
32
|
+
// PNGs when the user accidentally pins a 4K width to a class.
|
|
33
|
+
export const MAX_DIMENSION_PX = 4096
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Reduce raw SVG references into per-SVG resolved dimensions.
|
|
37
|
+
*
|
|
38
|
+
* @param {Object} args
|
|
39
|
+
* @param {Map<string, Array<{ classes: string[] }>>} args.refsBySvg - Map keyed by SVG relpath
|
|
40
|
+
* (relative to imagesFolder, normalized to forward slashes), values are the list of
|
|
41
|
+
* references seen for that SVG.
|
|
42
|
+
* @param {Map} args.tssMap - Output of parseTssMap().
|
|
43
|
+
* @param {string} args.imagesFolder - Absolute path to `purgetss/images/`.
|
|
44
|
+
* @param {Object} args.logger - Logger with `.warning(msg)` and `.info(msg)`.
|
|
45
|
+
* @returns {Promise<Map<string, { widthDp: number, heightDp: number }>>}
|
|
46
|
+
* Resolved entries only; skipped SVGs are omitted (warnings logged inline).
|
|
47
|
+
*/
|
|
48
|
+
export async function deriveDimensions({ refsBySvg, tssMap, imagesFolder, logger }) {
|
|
49
|
+
const resolved = new Map()
|
|
50
|
+
|
|
51
|
+
for (const [relPath, refs] of refsBySvg) {
|
|
52
|
+
const absPath = path.join(imagesFolder, relPath)
|
|
53
|
+
if (!fs.existsSync(absPath)) {
|
|
54
|
+
logger.warning(`⚠ SVG not found: purgetss/images/${relPath} — skipping`)
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const numericWidths = []
|
|
59
|
+
const numericHeights = []
|
|
60
|
+
for (const ref of refs) {
|
|
61
|
+
const { width, height } = resolveDimensions(ref.classes, tssMap)
|
|
62
|
+
if (typeof width === 'number' && Number.isFinite(width) && width > 0) {
|
|
63
|
+
numericWidths.push(width)
|
|
64
|
+
}
|
|
65
|
+
if (typeof height === 'number' && Number.isFinite(height) && height > 0) {
|
|
66
|
+
numericHeights.push(height)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (numericWidths.length === 0 && numericHeights.length === 0) {
|
|
71
|
+
logger.warning(
|
|
72
|
+
`⚠ ${relPath}: no class resolved width or height to a number — skipping. ` +
|
|
73
|
+
'Add a w-* or h-* utility on the view, or pin the size manually in purgetss/config.cjs > images.files.'
|
|
74
|
+
)
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
await readViewBox(absPath)
|
|
80
|
+
} catch (err) {
|
|
81
|
+
logger.warning(`⚠ ${relPath}: ${err.message} — skipping`)
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Symmetric materialization: only the dimensions the developer pinned
|
|
86
|
+
// explicitly land in config.cjs. The other side is derived from the SVG
|
|
87
|
+
// aspect by gen-scales on every run, so it stays in sync with viewBox
|
|
88
|
+
// edits and class changes without stale "stuck" values getting cemented
|
|
89
|
+
// into config the way auto-derived heights used to.
|
|
90
|
+
const widthDp = numericWidths.length > 0 ? Math.max(...numericWidths) : null
|
|
91
|
+
const heightDp = numericHeights.length > 0 ? Math.max(...numericHeights) : null
|
|
92
|
+
|
|
93
|
+
resolved.set(relPath, { widthDp, heightDp })
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return resolved
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function readViewBox(absPath) {
|
|
100
|
+
// Plan: error when the SVG declares neither viewBox nor width+height
|
|
101
|
+
// attributes. Sharp can infer a bounding box from the rendered content,
|
|
102
|
+
// but treating that as "valid" hides authoring mistakes (e.g. an export
|
|
103
|
+
// that lost its viewBox), so we enforce the explicit declaration first.
|
|
104
|
+
const head = fs.readFileSync(absPath, 'utf8').slice(0, 4096)
|
|
105
|
+
const svgTag = head.match(/<svg\b[^>]*>/i)?.[0] ?? ''
|
|
106
|
+
const hasViewBox = /\bviewBox\s*=\s*['"][^'"]+['"]/.test(svgTag)
|
|
107
|
+
const hasWidth = /\bwidth\s*=\s*['"][^'"]+['"]/.test(svgTag)
|
|
108
|
+
const hasHeight = /\bheight\s*=\s*['"][^'"]+['"]/.test(svgTag)
|
|
109
|
+
if (!hasViewBox && !(hasWidth && hasHeight)) {
|
|
110
|
+
throw new Error('SVG has no viewBox or explicit width/height attribute')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const { meta } = await readSvgSafely(absPath, {})
|
|
114
|
+
const vbW = meta.width
|
|
115
|
+
const vbH = meta.height
|
|
116
|
+
if (!Number.isFinite(vbW) || !Number.isFinite(vbH) || vbW <= 0 || vbH <= 0) {
|
|
117
|
+
throw new Error('SVG has no usable viewBox / width / height')
|
|
118
|
+
}
|
|
119
|
+
return { vbW, vbH }
|
|
120
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - SVG image pipeline (orchestrator)
|
|
3
|
+
*
|
|
4
|
+
* Post-step of the regular purge. Once `app.tss` is finalized, this module:
|
|
5
|
+
*
|
|
6
|
+
* 1. Parses app.tss into a class → props map.
|
|
7
|
+
* 2. Scans every view (.xml) and controller (.js) for SVG references paired
|
|
8
|
+
* with their classes / `class=""` attribute.
|
|
9
|
+
* 3. Reduces each SVG to a single resolved `{ widthDp, heightDp }` (max of
|
|
10
|
+
* every reference; falls back to viewBox aspect for height when `h-*` is
|
|
11
|
+
* missing or non-numeric).
|
|
12
|
+
* 4. Upserts `images.files` in config.cjs (never decreases — manual overrides
|
|
13
|
+
* always win).
|
|
14
|
+
* 5. Generates the iOS @1x/@2x/@3x and Android mdpi…xxxhdpi PNGs from the
|
|
15
|
+
* same SVG master, using a hash-based cache to skip unchanged inputs.
|
|
16
|
+
*
|
|
17
|
+
* The pipeline never rewrites XML/Controller files. Titanium falls back to the
|
|
18
|
+
* generated PNGs at runtime when an `image="/.../foo.svg"` reference resolves
|
|
19
|
+
* against a `.png` with the same basename in the platform assets folder.
|
|
20
|
+
*
|
|
21
|
+
* @fileoverview Compile-time SVG → multi-density PNG pipeline for Titanium
|
|
22
|
+
* @author César Estrada
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import fs from 'fs'
|
|
26
|
+
import path from 'path'
|
|
27
|
+
import {
|
|
28
|
+
cwd,
|
|
29
|
+
projectsPurge_TSS_Images_Folder
|
|
30
|
+
} from '../../shared/constants.js'
|
|
31
|
+
import { detectProjectType } from '../branding/tiapp-reader.js'
|
|
32
|
+
import { genAndroidScales, genIphoneScales, ANDROID_SCALES, IPHONE_SCALES } from '../images/gen-scales.js'
|
|
33
|
+
import { parseTssMap } from './tss-reader.js'
|
|
34
|
+
import { deriveDimensions } from './derive-dimensions.js'
|
|
35
|
+
import { syncConfigImages } from './sync-images.js'
|
|
36
|
+
import { loadCache, saveCache, hashFile, isCacheHit, makeCacheEntry } from './cache.js'
|
|
37
|
+
import { extractSvgRefsFromXml } from '../analyzers/class-extractor.js'
|
|
38
|
+
import { extractSvgRefsFromController } from '../analyzers/controller-svg-refs.js'
|
|
39
|
+
import { getConfigFile } from '../../shared/config-manager.js'
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Run the SVG image pipeline as a post-step of `purgetss` (purge command).
|
|
43
|
+
*
|
|
44
|
+
* Silent (no-op) when no SVG references are found. Logs progress at each step
|
|
45
|
+
* so the user can trace cache hits, dimension changes, and skipped SVGs.
|
|
46
|
+
*
|
|
47
|
+
* @param {Object} args
|
|
48
|
+
* @param {string} args.tssContent - Final purged TSS string (in memory).
|
|
49
|
+
* @param {string[]} args.viewPaths - Absolute paths to view .xml files.
|
|
50
|
+
* @param {string[]} args.controllerPaths - Absolute paths to controller .js files.
|
|
51
|
+
* @param {Object} args.logger - Logger with `.info/.warning/.success/.file`.
|
|
52
|
+
* @returns {Promise<void>}
|
|
53
|
+
*/
|
|
54
|
+
export async function runSvgPipeline({ tssContent, viewPaths, controllerPaths, logger }) {
|
|
55
|
+
|
|
56
|
+
const imagesFolder = projectsPurge_TSS_Images_Folder
|
|
57
|
+
if (!fs.existsSync(imagesFolder)) return
|
|
58
|
+
|
|
59
|
+
const refsBySvg = collectRefs({ viewPaths, controllerPaths })
|
|
60
|
+
if (refsBySvg.size === 0) return
|
|
61
|
+
|
|
62
|
+
logger.info('Resolving SVG dimensions from app.tss...')
|
|
63
|
+
const tssMap = parseTssMap(tssContent)
|
|
64
|
+
const derived = await deriveDimensions({ refsBySvg, tssMap, imagesFolder, logger })
|
|
65
|
+
if (derived.size === 0) return
|
|
66
|
+
|
|
67
|
+
// Sync config.cjs first so external runs (`purgetss images`) reflect the
|
|
68
|
+
// current resolution even if the cache short-circuits actual generation.
|
|
69
|
+
// The returned `effective` map merges derived values with any pre-existing
|
|
70
|
+
// manual overrides (config wins when its width is higher). The actual file
|
|
71
|
+
// write is gated by `images.autoSync` so devs who prefer hand-managed config
|
|
72
|
+
// can opt out.
|
|
73
|
+
const autoSync = readAutoSyncFlag()
|
|
74
|
+
const { stats, effective } = syncConfigImages(derived, { logger, write: autoSync })
|
|
75
|
+
if (autoSync && (stats.inserted || stats.updated)) {
|
|
76
|
+
logger.info(
|
|
77
|
+
`config.cjs > images.files: ${stats.inserted} inserted, ${stats.updated} updated, ${stats.untouched} untouched`
|
|
78
|
+
)
|
|
79
|
+
} else if (!autoSync && (stats.inserted || stats.updated)) {
|
|
80
|
+
logger.info(
|
|
81
|
+
`images.autoSync is off — would have ${stats.inserted ? `inserted ${stats.inserted}` : ''}${stats.inserted && stats.updated ? ', ' : ''}${stats.updated ? `updated ${stats.updated}` : ''} entry/entries in config.cjs > images.files`
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await generatePngs({ derived: effective, imagesFolder, logger })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function collectRefs({ viewPaths, controllerPaths }) {
|
|
89
|
+
const refsBySvg = new Map()
|
|
90
|
+
const push = (src, classes) => {
|
|
91
|
+
const relPath = normalizeSvgSrc(src)
|
|
92
|
+
if (!relPath) return
|
|
93
|
+
if (!refsBySvg.has(relPath)) refsBySvg.set(relPath, [])
|
|
94
|
+
refsBySvg.get(relPath).push({ classes })
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const file of viewPaths) {
|
|
98
|
+
const text = fs.readFileSync(file, 'utf8')
|
|
99
|
+
if (!text) continue
|
|
100
|
+
let refs
|
|
101
|
+
try {
|
|
102
|
+
refs = extractSvgRefsFromXml(text, file)
|
|
103
|
+
} catch {
|
|
104
|
+
// Malformed XML is reported by the regular purge path already; the SVG
|
|
105
|
+
// pipeline silently skips so we don't double-error on the same file.
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
for (const ref of refs) push(ref.src, ref.classes)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const file of controllerPaths) {
|
|
112
|
+
const text = fs.readFileSync(file, 'utf8')
|
|
113
|
+
if (!text) continue
|
|
114
|
+
const refs = extractSvgRefsFromController(text)
|
|
115
|
+
for (const ref of refs) push(ref.src, ref.classes)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return refsBySvg
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Map `/images/logos/foo.svg` → `logos/foo.svg`. Anything outside the
|
|
122
|
+
// `/images/` namespace is unknown to this pipeline and returns null.
|
|
123
|
+
function normalizeSvgSrc(src) {
|
|
124
|
+
if (typeof src !== 'string') return null
|
|
125
|
+
const stripped = src.replace(/^\/+/, '')
|
|
126
|
+
if (!stripped.startsWith('images/')) return null
|
|
127
|
+
return stripped.slice('images/'.length)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function generatePngs({ derived, imagesFolder, logger }) {
|
|
131
|
+
const projectType = detectProjectType(cwd)
|
|
132
|
+
const { androidBaseDir, iphoneBaseDir } = resolveOutputDirs(cwd, projectType)
|
|
133
|
+
const cache = loadCache()
|
|
134
|
+
let generated = 0
|
|
135
|
+
let cached = 0
|
|
136
|
+
|
|
137
|
+
// The SVG pipeline always emits PNG, even if `images.format` is set to
|
|
138
|
+
// 'webp' / 'jpeg' / etc. for the standalone `purgetss images` command.
|
|
139
|
+
// Verified empirically: Titanium's `image="/.../foo.svg"` runtime fallback
|
|
140
|
+
// resolves to `.png` only — `.webp` and other formats are not picked up.
|
|
141
|
+
// Honoring images.format here would silently generate files Titanium can't
|
|
142
|
+
// load. The standalone command keeps respecting format for raster sources
|
|
143
|
+
// (where the reference uses the actual extension).
|
|
144
|
+
for (const [relPath, { widthDp, heightDp }] of derived) {
|
|
145
|
+
const absSvg = path.join(imagesFolder, relPath)
|
|
146
|
+
const relForOutput = swapExt(relPath, '.png')
|
|
147
|
+
const targets = enumerateTargets({ relForOutput, androidBaseDir, iphoneBaseDir })
|
|
148
|
+
|
|
149
|
+
const svgHash = hashFile(absSvg)
|
|
150
|
+
if (isCacheHit(cache[relPath], svgHash, widthDp, heightDp, targets)) {
|
|
151
|
+
cached++
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
await genAndroidScales(absSvg, relPath, androidBaseDir, { baseWidth: widthDp, baseHeight: heightDp })
|
|
157
|
+
await genIphoneScales(absSvg, relPath, iphoneBaseDir, { baseWidth: widthDp, baseHeight: heightDp })
|
|
158
|
+
} catch (err) {
|
|
159
|
+
logger.warning(`✗ ${relPath}: ${err.message}`)
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
cache[relPath] = makeCacheEntry(svgHash, widthDp, heightDp, targets)
|
|
164
|
+
generated++
|
|
165
|
+
logger.info(`✓ ${relPath} → ${widthDp}×${heightDp}dp`)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
saveCache(cache)
|
|
169
|
+
if (generated || cached) {
|
|
170
|
+
logger.info(`SVG pipeline: ${generated} generated, ${cached} cached.`)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function resolveOutputDirs(projectRoot, projectType) {
|
|
175
|
+
if (projectType === 'classic') {
|
|
176
|
+
return {
|
|
177
|
+
androidBaseDir: path.join(projectRoot, 'Resources', 'android', 'images'),
|
|
178
|
+
iphoneBaseDir: path.join(projectRoot, 'Resources', 'iphone', 'images')
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
androidBaseDir: path.join(projectRoot, 'app', 'assets', 'android', 'images'),
|
|
183
|
+
iphoneBaseDir: path.join(projectRoot, 'app', 'assets', 'iphone', 'images')
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function enumerateTargets({ relForOutput, androidBaseDir, iphoneBaseDir }) {
|
|
188
|
+
const out = []
|
|
189
|
+
const parsed = path.parse(relForOutput)
|
|
190
|
+
for (const { name } of ANDROID_SCALES) {
|
|
191
|
+
out.push(path.join(androidBaseDir, name, relForOutput))
|
|
192
|
+
}
|
|
193
|
+
for (const { suffix } of IPHONE_SCALES) {
|
|
194
|
+
out.push(path.join(iphoneBaseDir, parsed.dir, `${parsed.name}${suffix}${parsed.ext}`))
|
|
195
|
+
}
|
|
196
|
+
return out
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function swapExt(relPath, newExt) {
|
|
200
|
+
const parsed = path.parse(relPath)
|
|
201
|
+
return path.join(parsed.dir, parsed.name + newExt)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Read `images.autoSync` from config; defaults to true. The SVG pipeline
|
|
205
|
+
// ignores `images.format` / `images.quality` on purpose — see the comment in
|
|
206
|
+
// generatePngs about Titanium's .svg → .png-only fallback.
|
|
207
|
+
function readAutoSyncFlag() {
|
|
208
|
+
try {
|
|
209
|
+
const cfg = getConfigFile()
|
|
210
|
+
if (cfg && typeof cfg.images === 'object' && cfg.images.autoSync === false) {
|
|
211
|
+
return false
|
|
212
|
+
}
|
|
213
|
+
} catch { /* fall through to default */ }
|
|
214
|
+
return true
|
|
215
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - Class cascade resolver for the SVG pipeline
|
|
3
|
+
*
|
|
4
|
+
* Given an ordered list of classes (as they appear in `class=""` / `classes:`)
|
|
5
|
+
* and the class→props map produced by tss-reader, apply later-wins cascading
|
|
6
|
+
* to return the final width and height.
|
|
7
|
+
*
|
|
8
|
+
* V1 limitations (out of scope, see plan):
|
|
9
|
+
* - Tag selectors (`'View': { ... }`) not applied
|
|
10
|
+
* - ID selectors (`'#foo': { ... }`) not applied
|
|
11
|
+
* - Platform/device modifiers (`'[platform=ios]'`) not applied
|
|
12
|
+
*
|
|
13
|
+
* If a project needs any of these, add the SVG to `images.files` manually.
|
|
14
|
+
*
|
|
15
|
+
* @fileoverview Cascade resolver: classes[] + tssMap → { width, height }
|
|
16
|
+
* @author César Estrada
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve width/height for a list of classes by applying later-wins cascading.
|
|
21
|
+
* Classes that don't exist in the map are skipped silently — they may be
|
|
22
|
+
* unknown utilities, typos, or classes registered via tag/ID selectors that
|
|
23
|
+
* V1 doesn't see.
|
|
24
|
+
*
|
|
25
|
+
* Returned value shape per dimension:
|
|
26
|
+
* - number → resolved to a finite dp value
|
|
27
|
+
* - 'auto' → resolved to Ti.UI.SIZE
|
|
28
|
+
* - 'fill' → resolved to Ti.UI.FILL
|
|
29
|
+
* - 'percent' → resolved to a percentage / non-numeric (can't size a PNG)
|
|
30
|
+
* - null → no class in the list set this dimension
|
|
31
|
+
*
|
|
32
|
+
* @param {string[]} classes - Class tokens in source order.
|
|
33
|
+
* @param {Map} tssMap - Output of parseTssMap().
|
|
34
|
+
* @returns {{ width: number|'auto'|'fill'|'percent'|null, height: number|'auto'|'fill'|'percent'|null }}
|
|
35
|
+
*/
|
|
36
|
+
export function resolveDimensions(classes, tssMap) {
|
|
37
|
+
let width = null
|
|
38
|
+
let height = null
|
|
39
|
+
for (const cls of classes) {
|
|
40
|
+
const entry = tssMap.get(cls)
|
|
41
|
+
if (!entry) continue
|
|
42
|
+
if (entry.width !== undefined) width = entry.width
|
|
43
|
+
if (entry.height !== undefined) height = entry.height
|
|
44
|
+
}
|
|
45
|
+
return { width, height }
|
|
46
|
+
}
|