UNPKG

purgetss

Version:

A package that simplifies mobile app creation for Titanium developers.

121 lines (108 loc) 5.08 kB
/** * PurgeTSS - SVG dimension deriver * * Reduces all the references to a single SVG into one final `{ widthDp, heightDp }` * pair that the image generator can consume as the `1×` baseline. Strategy: * * 1. For each reference, resolve the cascade against the purged app.tss * class→props map. * 2. Collect every numeric width across references; the SVG's width is the * max (a single view rendered at 800dp can't share PNGs with a view at * 128dp without visible blur). * 3. Resolve height: explicit numeric width wins; otherwise fall back to a * proportional value derived from the SVG's viewBox aspect ratio. * * Edge cases handled: * - Every reference resolves to a non-numeric width (`Ti.UI.SIZE`, percentage, * unknown class, etc.) → SVG is skipped with a warning. * - SVG file missing in `purgetss/images/` → skipped with a warning. * - SVG has an invalid/missing viewBox → skipped with an error. * * @fileoverview Per-SVG dimension reducer that feeds the image generator * @author César Estrada */ import fs from 'fs' import path from 'path' import { resolveDimensions } from './resolve-classes.js' import { readSvgSafely } from '../../shared/svg-utils.js' // Hard cap on the largest target PNG we are willing to emit, applied at the // `xxxhdpi` / `@3x` step downstream. Keeps Sharp from producing absurdly large // PNGs when the user accidentally pins a 4K width to a class. export const MAX_DIMENSION_PX = 4096 /** * Reduce raw SVG references into per-SVG resolved dimensions. * * @param {Object} args * @param {Map<string, Array<{ classes: string[] }>>} args.refsBySvg - Map keyed by SVG relpath * (relative to imagesFolder, normalized to forward slashes), values are the list of * references seen for that SVG. * @param {Map} args.tssMap - Output of parseTssMap(). * @param {string} args.imagesFolder - Absolute path to `purgetss/images/`. * @param {Object} args.logger - Logger with `.warning(msg)` and `.info(msg)`. * @returns {Promise<Map<string, { widthDp: number, heightDp: number }>>} * Resolved entries only; skipped SVGs are omitted (warnings logged inline). */ export async function deriveDimensions({ refsBySvg, tssMap, imagesFolder, logger }) { const resolved = new Map() for (const [relPath, refs] of refsBySvg) { const absPath = path.join(imagesFolder, relPath) if (!fs.existsSync(absPath)) { logger.warning(`⚠ SVG not found: purgetss/images/${relPath} — skipping`) continue } const numericWidths = [] const numericHeights = [] for (const ref of refs) { const { width, height } = resolveDimensions(ref.classes, tssMap) if (typeof width === 'number' && Number.isFinite(width) && width > 0) { numericWidths.push(width) } if (typeof height === 'number' && Number.isFinite(height) && height > 0) { numericHeights.push(height) } } if (numericWidths.length === 0 && numericHeights.length === 0) { logger.warning( `⚠ ${relPath}: no class resolved width or height to a number — skipping. ` + 'Add a w-* or h-* utility on the view, or pin the size manually in purgetss/config.cjs > images.files.' ) continue } try { await readViewBox(absPath) } catch (err) { logger.warning(`⚠ ${relPath}: ${err.message} — skipping`) continue } // Symmetric materialization: only the dimensions the developer pinned // explicitly land in config.cjs. The other side is derived from the SVG // aspect by gen-scales on every run, so it stays in sync with viewBox // edits and class changes without stale "stuck" values getting cemented // into config the way auto-derived heights used to. const widthDp = numericWidths.length > 0 ? Math.max(...numericWidths) : null const heightDp = numericHeights.length > 0 ? Math.max(...numericHeights) : null resolved.set(relPath, { widthDp, heightDp }) } return resolved } async function readViewBox(absPath) { // Plan: error when the SVG declares neither viewBox nor width+height // attributes. Sharp can infer a bounding box from the rendered content, // but treating that as "valid" hides authoring mistakes (e.g. an export // that lost its viewBox), so we enforce the explicit declaration first. const head = fs.readFileSync(absPath, 'utf8').slice(0, 4096) const svgTag = head.match(/<svg\b[^>]*>/i)?.[0] ?? '' const hasViewBox = /\bviewBox\s*=\s*['"][^'"]+['"]/.test(svgTag) const hasWidth = /\bwidth\s*=\s*['"][^'"]+['"]/.test(svgTag) const hasHeight = /\bheight\s*=\s*['"][^'"]+['"]/.test(svgTag) if (!hasViewBox && !(hasWidth && hasHeight)) { throw new Error('SVG has no viewBox or explicit width/height attribute') } const { meta } = await readSvgSafely(absPath, {}) const vbW = meta.width const vbH = meta.height if (!Number.isFinite(vbW) || !Number.isFinite(vbH) || vbW <= 0 || vbH <= 0) { throw new Error('SVG has no usable viewBox / width / height') } return { vbW, vbH } }