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.
Files changed (50) hide show
  1. package/README.md +93 -11
  2. package/bin/purgetss +140 -1
  3. package/dist/purgetss.ui.js +65 -26
  4. package/dist/utilities.tss +21 -4
  5. package/experimental/completions2.js +1 -1
  6. package/lib/completions/titanium/completions-v3.json +62 -1
  7. package/lib/templates/purgetss.config.js.cjs +15 -1
  8. package/lib/templates/purgetss.ui.js.cjs +64 -25
  9. package/package.json +3 -1
  10. package/src/cli/commands/brand.js +69 -0
  11. package/src/cli/commands/create.js +11 -7
  12. package/src/cli/commands/fonts.js +9 -9
  13. package/src/cli/commands/icon-library.js +18 -16
  14. package/src/cli/commands/images.js +116 -0
  15. package/src/cli/commands/init.js +4 -0
  16. package/src/cli/commands/module.js +4 -2
  17. package/src/cli/commands/purge.js +77 -101
  18. package/src/cli/commands/semantic.js +180 -0
  19. package/src/cli/commands/shades.js +332 -13
  20. package/src/cli/utils/project-detection.js +4 -2
  21. package/src/core/analyzers/class-extractor.js +110 -3
  22. package/src/core/branding/brand-config.js +111 -0
  23. package/src/core/branding/branding-logger.js +40 -0
  24. package/src/core/branding/cleanup-legacy.js +220 -0
  25. package/src/core/branding/ensure-brand-section.js +80 -0
  26. package/src/core/branding/gen-android-adaptive.js +116 -0
  27. package/src/core/branding/gen-android-legacy.js +63 -0
  28. package/src/core/branding/gen-ic-launcher-xml.js +29 -0
  29. package/src/core/branding/gen-ios-dark.js +70 -0
  30. package/src/core/branding/gen-ios-tinted.js +55 -0
  31. package/src/core/branding/gen-ios.js +69 -0
  32. package/src/core/branding/gen-marketplace.js +71 -0
  33. package/src/core/branding/gen-notification.js +76 -0
  34. package/src/core/branding/gen-splash.js +64 -0
  35. package/src/core/branding/index.js +336 -0
  36. package/src/core/branding/post-gen-notes.js +145 -0
  37. package/src/core/branding/prepare-master.js +108 -0
  38. package/src/core/branding/tiapp-reader.js +110 -0
  39. package/src/core/builders/tailwind-helpers.js +1 -1
  40. package/src/core/images/ensure-images-section.js +57 -0
  41. package/src/core/images/gen-scales.js +181 -0
  42. package/src/core/images/index.js +171 -0
  43. package/src/shared/config-manager.js +46 -0
  44. package/src/shared/config-writer.js +84 -0
  45. package/src/shared/constants.js +3 -0
  46. package/src/shared/helpers/typography.js +38 -3
  47. package/src/shared/logger.js +69 -4
  48. package/src/shared/prompt.js +64 -0
  49. package/src/shared/svg-utils.js +80 -0
  50. package/src/shared/utils.js +8 -4
@@ -29,6 +29,7 @@ export const projectsAppTSS = `${cwd}/app/styles/app.tss`
29
29
  export const projects_AppTSS = `${cwd}/app/styles/_app.tss`
30
30
  export const projectsAlloyJMKFile = `${cwd}/app/alloy.jmk`
31
31
  export const projectsFontsFolder = `${cwd}/app/assets/fonts`
32
+ export const projectsSemanticColorsJSON = `${cwd}/app/assets/semantic.colors.json`
32
33
  export const projectsFontAwesomeJS = `${cwd}/app/lib/fontawesome.js`
33
34
 
34
35
  // ============================================================================
@@ -39,6 +40,8 @@ export const projectsPurgeTSSFolder = `${cwd}/purgetss`
39
40
  export const projectsConfigJS = `${cwd}/purgetss/config.cjs`
40
41
  export const projectsTailwind_TSS = `${cwd}/purgetss/styles/utilities.tss`
41
42
  export const projectsPurge_TSS_Fonts_Folder = `${cwd}/purgetss/fonts`
43
+ export const projectsPurge_TSS_Brand_Folder = `${cwd}/purgetss/brand`
44
+ export const projectsPurge_TSS_Images_Folder = `${cwd}/purgetss/images`
42
45
  export const projectsPurge_TSS_Styles_Folder = `${cwd}/purgetss/styles`
43
46
  export const projectsFA_TSS_File = `${cwd}/purgetss/styles/fontawesome.tss`
44
47
 
@@ -26,18 +26,53 @@ function removeFractions(modifiersAndValues, extras = []) {
26
26
 
27
27
  /**
28
28
  * Font family property for text components
29
+ *
30
+ * Built-in platform defaults:
31
+ * - font-sans → Android: 'sans-serif', iOS: 'Helvetica Neue'
32
+ * - font-serif → Android: 'serif', iOS: 'Georgia'
33
+ * - font-mono → 'monospace' (both platforms)
34
+ *
35
+ * User values from config.cjs override defaults cross-platform.
36
+ *
29
37
  * @param {Object} modifiersAndValues - Modifier and value pairs
30
38
  * @returns {string} Generated styles
31
39
  */
32
40
  export function fontFamily(modifiersAndValues) {
41
+ const platformDefaults = {
42
+ sans: { ios: 'Helvetica Neue', android: 'sans-serif' },
43
+ serif: { ios: 'Georgia', android: 'serif' }
44
+ }
45
+
46
+ const crossPlatformDefaults = { mono: 'monospace' }
47
+
48
+ const defaults = { ...modifiersAndValues }
49
+ const ios = {}
50
+ const android = {}
51
+
52
+ _.each(crossPlatformDefaults, (value, key) => {
53
+ if (!(key in defaults)) {
54
+ defaults[key] = value
55
+ }
56
+ })
57
+
58
+ _.each(platformDefaults, (platforms, key) => {
59
+ if (!(key in defaults)) {
60
+ ios[key] = platforms.ios
61
+ android[key] = platforms.android
62
+ }
63
+ })
64
+
65
+ const selectorsAndValues = {}
66
+ if (!_.isEmpty(defaults)) selectorsAndValues.default = defaults
67
+ if (!_.isEmpty(ios)) selectorsAndValues.ios = ios
68
+ if (!_.isEmpty(android)) selectorsAndValues.android = android
69
+
33
70
  return processProperties({
34
71
  prop: 'fontFamily',
35
72
  modules: 'Ti.UI.ActivityIndicator, Ti.UI.Button, Ti.UI.Label, Ti.UI.ListItem, Ti.UI.Picker, Ti.UI.PickerColumn, Ti.UI.PickerRow, Ti.UI.ProgressBar, Ti.UI.Switch, Ti.UI.TableViewRow, Ti.UI.TextArea, Ti.UI.TextField'
36
73
  }, {
37
74
  font: '{ font: { fontFamily: {value} } }'
38
- }, {
39
- default: modifiersAndValues
40
- })
75
+ }, selectorsAndValues)
41
76
  }
42
77
 
43
78
  /**
@@ -19,6 +19,27 @@ const purgeLabel = colores.purgeLabel
19
19
  // Debug mode flag (can be set externally)
20
20
  let purgingDebug = false
21
21
 
22
+ // Section mode: when active, the first info/warn/error/file call acts as a
23
+ // ::PurgeTSS:: header and every subsequent call prints indented 3 spaces
24
+ // without the prefix. Used by long-running flows (e.g. purge) that want a
25
+ // single signed line per run instead of one per step. Always wrap with
26
+ // try/finally in the caller to guarantee endSection() runs.
27
+ let _sectionMode = false
28
+ let _sectionHeaderEmitted = false
29
+
30
+ function _emit(text) {
31
+ if (_sectionMode) {
32
+ if (_sectionHeaderEmitted) {
33
+ console.log(' ' + text)
34
+ } else {
35
+ console.log(purgeLabel, text)
36
+ _sectionHeaderEmitted = true
37
+ }
38
+ } else {
39
+ console.log(purgeLabel, text)
40
+ }
41
+ }
42
+
22
43
  /**
23
44
  * Main logger object with different log levels
24
45
  * Maintains exact same API as original for compatibility
@@ -29,7 +50,7 @@ export const logger = {
29
50
  * @param {...any} args - Arguments to log
30
51
  */
31
52
  info: function(...args) {
32
- console.log(purgeLabel, args.join(' '))
53
+ _emit(args.join(' '))
33
54
  },
34
55
 
35
56
  /**
@@ -37,7 +58,7 @@ export const logger = {
37
58
  * @param {...any} args - Arguments to log
38
59
  */
39
60
  warn: function(...args) {
40
- console.log(purgeLabel, chalk.yellow(args.join(' ')))
61
+ _emit(chalk.yellow(args.join(' ')))
41
62
  },
42
63
 
43
64
  /**
@@ -45,7 +66,7 @@ export const logger = {
45
66
  * @param {...any} args - Arguments to log
46
67
  */
47
68
  error: function(...args) {
48
- console.log(purgeLabel, chalk.red(args.join(' ')))
69
+ _emit(chalk.red(args.join(' ')))
49
70
  },
50
71
 
51
72
  /**
@@ -53,7 +74,51 @@ export const logger = {
53
74
  * @param {...any} args - Arguments to log
54
75
  */
55
76
  file: function(...args) {
56
- console.log(purgeLabel, chalk.yellow(args.join(' ')), 'file created!')
77
+ _emit(chalk.yellow(args.join(' ')) + ' file created!')
78
+ },
79
+
80
+ /**
81
+ * Enable section mode. The next info/warn/error/file call becomes the
82
+ * ::PurgeTSS:: header; subsequent calls print indented without prefix.
83
+ * MUST be paired with endSection() via try/finally to avoid state leaks.
84
+ */
85
+ startSection: function() {
86
+ _sectionMode = true
87
+ _sectionHeaderEmitted = false
88
+ },
89
+
90
+ /**
91
+ * Exit section mode. Safe to call even if section wasn't started.
92
+ */
93
+ endSection: function() {
94
+ _sectionMode = false
95
+ _sectionHeaderEmitted = false
96
+ },
97
+
98
+ /**
99
+ * Log a multi-line block with a single ::PurgeTSS:: header.
100
+ * First arg is the header (printed next to purgeLabel); remaining args are
101
+ * continuation lines indented 3 spaces. Pass '' for a blank separator line.
102
+ *
103
+ * @param {string} header - Header line (printed after purgeLabel)
104
+ * @param {...string} lines - Continuation lines (indented, or '' for blank)
105
+ */
106
+ block: function(header, ...lines) {
107
+ console.log(purgeLabel, header)
108
+ for (const line of lines) {
109
+ console.log(line === '' ? '' : ' ' + line)
110
+ }
111
+ },
112
+
113
+ /**
114
+ * Log a sub-line without the ::PurgeTSS:: prefix, indented 3 spaces.
115
+ * Use inside sequential flows where a prior logger.info emitted the header.
116
+ * Prints synchronously, preserving timing with logger.info.
117
+ *
118
+ * @param {...any} args - Arguments to log
119
+ */
120
+ item: function(...args) {
121
+ console.log(' ' + args.join(' '))
57
122
  }
58
123
  }
59
124
 
@@ -0,0 +1,64 @@
1
+ /**
2
+ * PurgeTSS - Interactive prompt helpers
3
+ *
4
+ * Small wrapper around Node's readline for yes/no confirmations on destructive
5
+ * operations (in-place brand/images writes). Auto-skips when:
6
+ * - the caller sets `yes: true` (e.g. from --yes flag)
7
+ * - the PURGETSS_YES=1 environment variable is set
8
+ * - stdin is not a TTY (CI, hooks, piped input)
9
+ *
10
+ * Default answer is NO on empty input — requires explicit "y" / "yes" to proceed.
11
+ *
12
+ * @fileoverview TTY-aware confirmation prompts
13
+ * @author César Estrada
14
+ */
15
+
16
+ import readline from 'node:readline/promises'
17
+
18
+ /**
19
+ * Ask the user for a yes/no confirmation.
20
+ *
21
+ * @param {string} message - Prompt text shown to the user. A trailing space is
22
+ * added automatically, so pass something like "Continue? [y/N]".
23
+ * @param {Object} [opts]
24
+ * @param {boolean} [opts.yes=false] - Skip prompt and answer yes (e.g. --yes flag).
25
+ * @returns {Promise<boolean>} True when confirmed or skipped; false otherwise.
26
+ */
27
+ export async function confirm(message, { yes = false } = {}) {
28
+ if (yes) return true
29
+ if (process.env.PURGETSS_YES === '1') return true
30
+ if (!process.stdin.isTTY) return true
31
+
32
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
33
+ try {
34
+ const answer = await rl.question(`${message} `)
35
+ return /^y(es)?$/i.test(answer.trim())
36
+ } finally {
37
+ rl.close()
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Three-way confirmation: yes / no / always. Same skip rules as confirm()
43
+ * (--yes flag, PURGETSS_YES env, non-TTY all resolve to 'yes').
44
+ *
45
+ * @param {string} message - Prompt text (a trailing space is added).
46
+ * @param {Object} [opts]
47
+ * @param {boolean} [opts.yes=false] - Skip prompt, answer 'yes'.
48
+ * @returns {Promise<'yes'|'no'|'always'>} The user's choice.
49
+ */
50
+ export async function confirmWithAlways(message, { yes = false } = {}) {
51
+ if (yes) return 'yes'
52
+ if (process.env.PURGETSS_YES === '1') return 'yes'
53
+ if (!process.stdin.isTTY) return 'yes'
54
+
55
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
56
+ try {
57
+ const answer = (await rl.question(`${message} `)).trim().toLowerCase()
58
+ if (answer === 'a' || answer === 'always') return 'always'
59
+ if (answer === 'y' || answer === 'yes') return 'yes'
60
+ return 'no'
61
+ } finally {
62
+ rl.close()
63
+ }
64
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * PurgeTSS - Shared SVG/Sharp utilities
3
+ *
4
+ * Centralizes the logic for safely rasterizing SVGs with Sharp — specifically,
5
+ * handling SVGs that come out of vector editors (Affinity, Illustrator) with
6
+ * absurdly large viewBoxes that would otherwise trigger Sharp's pixel limit.
7
+ *
8
+ * Shared by:
9
+ * - src/core/branding/prepare-master.js (brand pipeline)
10
+ * - src/core/images/gen-scales.js (images pipeline)
11
+ *
12
+ * @fileoverview SVG rasterization helpers for the Sharp pipeline
13
+ * @author César Estrada
14
+ */
15
+
16
+ import fs from 'fs'
17
+ import path from 'path'
18
+ import sharp from 'sharp'
19
+
20
+ // SVGs with natural dimensions above this (in points) are almost always the
21
+ // result of a vector editor baking in transforms — e.g. Affinity exporting a
22
+ // "pliego" as 29559×13542 pt. Callers compensate with adaptive density; this
23
+ // threshold only controls the user-facing warning.
24
+ export const VIEWBOX_WARN_THRESHOLD = 4096
25
+
26
+ /**
27
+ * Compute a Sharp density (DPI) so that rasterization of an SVG lands at
28
+ * approximately `targetSupersampledMax` pixels on the longest side, regardless
29
+ * of the SVG's intrinsic viewBox. Clamped to Sharp's valid range (1..2400).
30
+ *
31
+ * @param {number} naturalMax - Largest SVG dimension in points (at 72 DPI).
32
+ * @param {number} targetSupersampledMax - Desired raster size on the longest side.
33
+ * @returns {number} A safe density value in [1, 2400].
34
+ */
35
+ export function computeSvgDensity(naturalMax, targetSupersampledMax) {
36
+ return Math.min(
37
+ 2400,
38
+ Math.max(1, Math.round((targetSupersampledMax / naturalMax) * 72))
39
+ )
40
+ }
41
+
42
+ /**
43
+ * Read an SVG safely — buffer + metadata with the pixel-limit check disabled
44
+ * (Sharp enforces it even during header parsing for SVG inputs). Optionally
45
+ * emits a warning via the provided logger when the viewBox is disproportionate.
46
+ *
47
+ * The caller is responsible for computing a bounded density (via
48
+ * computeSvgDensity) before actually rendering — this keeps the real pixel
49
+ * output inside Sharp's limit even though the limit was disabled at read time.
50
+ *
51
+ * @param {string} svgPath - Absolute path to the SVG file.
52
+ * @param {Object} [opts]
53
+ * @param {Object} [opts.logger] - Logger exposing a .warning(msg) method. If
54
+ * omitted, no warning is emitted (silent mode, for unit tests).
55
+ * @param {boolean} [opts.withAdvice=false] - Include the "re-export" advice
56
+ * line. Enable for the brand pipeline (where the logo is central); keep
57
+ * false for the images pipeline (which may process many files).
58
+ * @returns {Promise<{buffer: Buffer, meta: Object, naturalMax: number}>}
59
+ */
60
+ export async function readSvgSafely(svgPath, { logger, withAdvice = false } = {}) {
61
+ const buffer = fs.readFileSync(svgPath)
62
+ const meta = await sharp(buffer, { limitInputPixels: false }).metadata()
63
+ const naturalMax = Math.max(meta.width, meta.height)
64
+
65
+ if (logger && naturalMax > VIEWBOX_WARN_THRESHOLD) {
66
+ logger.warning(
67
+ `⚠ ${path.basename(svgPath)} has a disproportionate viewBox (${meta.width}×${meta.height} pt).`
68
+ )
69
+ if (withAdvice) {
70
+ logger.warning(
71
+ ` Re-export from your vector editor with a canvas-sized viewBox if possible.`
72
+ )
73
+ }
74
+ logger.warning(
75
+ ` Adapting rasterization density to compensate.`
76
+ )
77
+ }
78
+
79
+ return { buffer, meta, naturalMax }
80
+ }
@@ -73,8 +73,10 @@ export function alloyProject(silent = false) {
73
73
 
74
74
  if (!fs.existsSync(`${cwd}/app/views`)) {
75
75
  if (!silent) {
76
- logger.info(`Please make sure you are running ${chalk.green('purgetss')} within an Alloy Project.`)
77
- logger.info(`For more information, visit ${chalk.green('https://purgetss.com')}`)
76
+ logger.block(
77
+ `Please make sure you are running ${chalk.green('purgetss')} within an Alloy Project.`,
78
+ `For more information, visit ${chalk.green('https://purgetss.com')}`
79
+ )
78
80
  }
79
81
  return false
80
82
  }
@@ -93,8 +95,10 @@ export function classicProject(silent = false) {
93
95
 
94
96
  if (!fs.existsSync(`${cwd}/Resources`)) {
95
97
  if (!silent) {
96
- logger.info(`Please make sure you are running ${chalk.green('purgetss')} within a Titanium's Classic Project.`)
97
- logger.info(`For more information, visit ${chalk.green('https://purgetss.com')}`)
98
+ logger.block(
99
+ `Please make sure you are running ${chalk.green('purgetss')} within a Titanium's Classic Project.`,
100
+ `For more information, visit ${chalk.green('https://purgetss.com')}`
101
+ )
98
102
  }
99
103
  return false
100
104
  }