UNPKG

purgetss

Version:

A package that simplifies mobile app creation for Titanium developers.

164 lines (142 loc) 5.95 kB
/** * PurgeTSS - Images Command * * Generates multi-density variants of UI images for Titanium Alloy or Classic * projects. Auto-discovers sources in `./purgetss/images/` by default; accepts * a path argument to override (file or directory). * * Precedence for every option: CLI flag > `images:` section in config > default. * * @fileoverview Assets command entry point * @author César Estrada */ import fs from 'fs' import path from 'path' import chalk from 'chalk' import { runImages } from '../../core/images/index.js' import { logger } from '../../core/branding/branding-logger.js' import { ensureImagesSection } from '../../core/images/ensure-images-section.js' import { getConfigFile } from '../../shared/config-manager.js' import { projectsPurge_TSS_Images_Folder } from '../../shared/constants.js' const VALID_FORMATS = new Set(['webp', 'jpeg', 'jpg', 'png', 'avif', 'gif', 'tiff']) export async function images(cliSource, options = {}) { if (options.debug) logger.setDebugMode(true) const projectRoot = options.project ? path.resolve(options.project) : process.cwd() if (!options.project) ensureImagesSection() const cfg = loadImagesSection() // --android and --ios are mutually exclusive. if (options.android && options.ios) { logger.error('--android and --ios are mutually exclusive. Pass neither to generate both, or pick one.') process.exit(1) } if (options.width !== undefined) { if (!Number.isFinite(options.width) || !Number.isInteger(options.width) || options.width < 1 || options.width > 8192) { logger.error(`Invalid --width '${options.width}'. Must be an integer between 1 and 8192.`) process.exit(1) } } let opacity = null if (options.opacity !== undefined) { if (!Number.isFinite(options.opacity) || !Number.isInteger(options.opacity) || options.opacity < 0 || options.opacity > 100) { logger.error(`Invalid --opacity '${options.opacity}'. Must be an integer between 0 and 100.`) process.exit(1) } opacity = options.opacity } let padding = null if (options.padding !== undefined) { if (!Number.isFinite(options.padding) || !Number.isInteger(options.padding) || options.padding < 0 || options.padding > 40) { logger.error(`Invalid --padding '${options.padding}'. Must be an integer between 0 and 40.`) process.exit(1) } padding = options.padding } let outputRelpath = null if (options.output !== undefined && options.output !== null && options.output !== '') { const raw = String(options.output) if (path.isAbsolute(raw)) { logger.error(`Invalid --output '${raw}'. Must be a relative path inside the project images folder, not absolute.`) process.exit(1) } const segments = raw.split(/[\\/]/) if (segments.includes('..')) { logger.error(`Invalid --output '${raw}'. '..' segments are not allowed (must stay inside the project images folder).`) process.exit(1) } // Strip any trailing extension — --format (or source ext) decides actual extension. const parsed = path.parse(raw) outputRelpath = parsed.dir ? path.join(parsed.dir, parsed.name) : parsed.name } const format = options.format ?? cfg.format ?? null if (format && !VALID_FORMATS.has(format.toLowerCase())) { logger.error(`Invalid --format '${format}'. Valid: ${[...VALID_FORMATS].join(', ')}`) process.exit(1) } const source = resolveSource(cliSource, projectRoot) if (!source) { printMissingSourceHelp(projectRoot) process.exit(1) } try { await runImages({ source, projectRoot, androidOnly: Boolean(options.android), iphoneOnly: Boolean(options.ios), format: format ? format.toLowerCase() : null, quality: options.quality ?? cfg.quality ?? 85, baseWidth: options.width ?? null, opacity, padding, outputRelpath, dryRun: Boolean(options.dryRun), yes: Boolean(options.yes), confirmOverwrites: cfg.confirmOverwrites !== false, filesOverrides: Array.isArray(cfg.files) ? cfg.files : [] }) } catch (err) { logger.error(err.message) if (options.debug) console.error(err.stack) process.exit(1) } } function loadImagesSection() { try { const cfg = getConfigFile() if (cfg && typeof cfg.images === 'object') return cfg.images } catch {} return {} } function resolveSource(cliSource, projectRoot) { const imagesFolder = projectRoot === process.cwd() ? projectsPurge_TSS_Images_Folder : path.join(projectRoot, 'purgetss', 'images') if (cliSource) { if (path.isAbsolute(cliSource)) { return fs.existsSync(cliSource) ? cliSource : null } // Relative paths: try purgetss/images/ first (convention), then cwd. // Lets users write short paths like `background/pink.png` without the prefix. const insideImages = path.resolve(imagesFolder, cliSource) if (fs.existsSync(insideImages)) return insideImages const cwdResolved = path.resolve(projectRoot, cliSource) if (fs.existsSync(cwdResolved)) return cwdResolved return null } return fs.existsSync(imagesFolder) ? imagesFolder : null } function printMissingSourceHelp(projectRoot) { const rel = (p) => path.relative(projectRoot, p) || '.' const imagesDir = path.join(projectRoot, 'purgetss', 'images') logger.error('No source images found.') console.log() console.log(` Expected images inside ${chalk.cyan(rel(imagesDir) + '/')}.`) console.log(' The folder already exists — drop your images into it (subdirectories are preserved):') console.log(` ${chalk.cyan('cp my-ui-asset.png ' + rel(imagesDir) + '/')}`) console.log() console.log(' Alternatives:') console.log(` ${chalk.gray('•')} Pass a file or directory explicitly:`) console.log(` ${chalk.cyan('purgetss images ./docs/screenshots')}`) console.log(` ${chalk.cyan('purgetss images ./logo.png')}`) console.log() }