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.
Files changed (36) hide show
  1. package/README.md +21 -1
  2. package/bin/purgetss +13 -0
  3. package/dist/purgetss.ui.js +1 -1
  4. package/lib/templates/create/index.xml +1 -1
  5. package/lib/templates/purgetss.config.js.cjs +3 -1
  6. package/package.json +1 -1
  7. package/src/cli/commands/images.js +41 -2
  8. package/src/cli/commands/purge.js +15 -2
  9. package/src/cli/utils/cli-helpers.js +15 -5
  10. package/src/cli/utils/unsupported-class-reporter.js +3 -3
  11. package/src/core/analyzers/class-extractor.js +54 -0
  12. package/src/core/analyzers/controller-svg-refs.js +154 -0
  13. package/src/core/branding/brand-config.js +7 -0
  14. package/src/core/branding/ensure-brand-section.js +4 -3
  15. package/src/core/branding/gen-feature-graphic.js +57 -0
  16. package/src/core/branding/index.js +28 -4
  17. package/src/core/branding/post-gen-notes.js +2 -2
  18. package/src/core/builders/auto-utilities-builder.js +20 -15
  19. package/src/core/images/ensure-images-section.js +6 -4
  20. package/src/core/images/gen-scales.js +82 -17
  21. package/src/core/images/index.js +117 -12
  22. package/src/core/purger/icon-purger.js +7 -3
  23. package/src/core/purger/tailwind-purger.js +3 -1
  24. package/src/core/svg/cache.js +96 -0
  25. package/src/core/svg/derive-dimensions.js +120 -0
  26. package/src/core/svg/index.js +215 -0
  27. package/src/core/svg/resolve-classes.js +46 -0
  28. package/src/core/svg/sync-images.js +278 -0
  29. package/src/core/svg/tss-reader.js +134 -0
  30. package/src/dev/builders/tailwind-builder.js +18 -0
  31. package/src/shared/config-manager.js +72 -3
  32. package/src/shared/error-reporter.js +117 -0
  33. package/src/shared/helpers/colors.js +57 -13
  34. package/src/shared/helpers/utils.js +46 -8
  35. package/src/shared/logger.js +12 -0
  36. package/src/shared/validation/config-validator.js +167 -0
@@ -1,5 +1,5 @@
1
1
  import _ from 'lodash'
2
- import { processProperties, processComments, parseValue, setModifier2, removeLastDash, addTransparencyToValue } from './utils.js'
2
+ import { processProperties, processComments, parseValue, setModifier2, removeLastDash, addTransparencyToValue, defaultModifier, camelCaseToDash } from './utils.js'
3
3
  /**
4
4
  * Active tint color for tabs
5
5
  * @param {Object} modifiersAndValues - Modifier and value pairs
@@ -646,9 +646,20 @@ export function backgroundGradient(modifiersAndValues) {
646
646
  _.each(objectPosition, (properties, rule) => {
647
647
  _.each(modifiersAndValues, (value, modifier) => {
648
648
  if (typeof value === 'object') {
649
- _.each(value, (_value, _modifier) => {
650
- convertedStyles += `'.${removeLastDash(`${rule}-${setModifier2(modifier, rule)}${setModifier2(_modifier)}`)}': ` + _.replace(_.replace(properties, /{transparentValue}/g, `${addTransparencyToValue(parseValue(_value))}`), /{value}/g, parseValue(_value)) + '\n'
651
- })
649
+ const emitLeaf = (leafValue, _modifier, suffix) => {
650
+ convertedStyles += `'.${removeLastDash(`${rule}-${setModifier2(modifier, rule)}${setModifier2(_modifier)}${suffix}`)}': ` + _.replace(_.replace(properties, /{transparentValue}/g, `${addTransparencyToValue(parseValue(leafValue))}`), /{value}/g, parseValue(leafValue)) + '\n'
651
+ }
652
+ const walk = (val, _modifier, suffix) => {
653
+ if (val && typeof val === 'object') {
654
+ _.each(val, (childVal, childKey) => {
655
+ const newSuffix = defaultModifier(childKey) ? suffix : `${suffix}-${camelCaseToDash(String(childKey))}`
656
+ walk(childVal, _modifier, newSuffix)
657
+ })
658
+ } else {
659
+ emitLeaf(val, _modifier, suffix)
660
+ }
661
+ }
662
+ _.each(value, (_value, _modifier) => walk(_value, _modifier, ''))
652
663
  } else {
653
664
  convertedStyles += `'.${setModifier2(rule, modifier)}${setModifier2(modifier)}': ` + _.replace(_.replace(properties, /{value}/g, parseValue(value)), /{transparentValue}/g, `${addTransparencyToValue(parseValue(value))}`) + '\n'
654
665
  }
@@ -665,9 +676,20 @@ export function backgroundGradient(modifiersAndValues) {
665
676
  _.each(objectPosition, (properties, rule) => {
666
677
  _.each(modifiersAndValues, (value, modifier) => {
667
678
  if (typeof value === 'object') {
668
- _.each(value, (_value, _modifier) => {
669
- convertedStyles += `'.${removeLastDash(`${rule}-${setModifier2(modifier, rule)}${setModifier2(_modifier)}`)}': ` + _.replace(properties, /{value}/g, parseValue(_value)) + '\n'
670
- })
679
+ const emitLeaf = (leafValue, _modifier, suffix) => {
680
+ convertedStyles += `'.${removeLastDash(`${rule}-${setModifier2(modifier, rule)}${setModifier2(_modifier)}${suffix}`)}': ` + _.replace(properties, /{value}/g, parseValue(leafValue)) + '\n'
681
+ }
682
+ const walk = (val, _modifier, suffix) => {
683
+ if (val && typeof val === 'object') {
684
+ _.each(val, (childVal, childKey) => {
685
+ const newSuffix = defaultModifier(childKey) ? suffix : `${suffix}-${camelCaseToDash(String(childKey))}`
686
+ walk(childVal, _modifier, newSuffix)
687
+ })
688
+ } else {
689
+ emitLeaf(val, _modifier, suffix)
690
+ }
691
+ }
692
+ _.each(value, (_value, _modifier) => walk(_value, _modifier, ''))
671
693
  } else {
672
694
  convertedStyles += `'.${setModifier2(rule, modifier)}${setModifier2(modifier)}': ` + _.replace(properties, /{value}/g, parseValue(value)) + '\n'
673
695
  }
@@ -688,9 +710,20 @@ export function backgroundSelectedGradient(modifiersAndValues) {
688
710
  _.each(objectPosition, (properties, rule) => {
689
711
  _.each(modifiersAndValues, (value, modifier) => {
690
712
  if (typeof value === 'object') {
691
- _.each(value, (_value, _modifier) => {
692
- convertedStyles += `'.${removeLastDash(`${rule}-${setModifier2(modifier, rule)}${setModifier2(_modifier)}`)}': ` + _.replace(_.replace(properties, /{transparentValue}/g, `${addTransparencyToValue(parseValue(_value))}`), /{value}/g, parseValue(_value)) + '\n'
693
- })
713
+ const emitLeaf = (leafValue, _modifier, suffix) => {
714
+ convertedStyles += `'.${removeLastDash(`${rule}-${setModifier2(modifier, rule)}${setModifier2(_modifier)}${suffix}`)}': ` + _.replace(_.replace(properties, /{transparentValue}/g, `${addTransparencyToValue(parseValue(leafValue))}`), /{value}/g, parseValue(leafValue)) + '\n'
715
+ }
716
+ const walk = (val, _modifier, suffix) => {
717
+ if (val && typeof val === 'object') {
718
+ _.each(val, (childVal, childKey) => {
719
+ const newSuffix = defaultModifier(childKey) ? suffix : `${suffix}-${camelCaseToDash(String(childKey))}`
720
+ walk(childVal, _modifier, newSuffix)
721
+ })
722
+ } else {
723
+ emitLeaf(val, _modifier, suffix)
724
+ }
725
+ }
726
+ _.each(value, (_value, _modifier) => walk(_value, _modifier, ''))
694
727
  } else {
695
728
  convertedStyles += `'.${setModifier2(rule, modifier)}${setModifier2(modifier)}': ` + _.replace(_.replace(properties, /{value}/g, parseValue(value)), /{transparentValue}/g, `${addTransparencyToValue(parseValue(value))}`) + '\n'
696
729
  }
@@ -707,9 +740,20 @@ export function backgroundSelectedGradient(modifiersAndValues) {
707
740
  _.each(objectPosition, (properties, rule) => {
708
741
  _.each(modifiersAndValues, (value, modifier) => {
709
742
  if (typeof value === 'object') {
710
- _.each(value, (_value, _modifier) => {
711
- convertedStyles += `'.${removeLastDash(`${rule}-${setModifier2(modifier, rule)}${setModifier2(_modifier)}`)}': ` + _.replace(properties, /{value}/g, parseValue(_value)) + '\n'
712
- })
743
+ const emitLeaf = (leafValue, _modifier, suffix) => {
744
+ convertedStyles += `'.${removeLastDash(`${rule}-${setModifier2(modifier, rule)}${setModifier2(_modifier)}${suffix}`)}': ` + _.replace(properties, /{value}/g, parseValue(leafValue)) + '\n'
745
+ }
746
+ const walk = (val, _modifier, suffix) => {
747
+ if (val && typeof val === 'object') {
748
+ _.each(val, (childVal, childKey) => {
749
+ const newSuffix = defaultModifier(childKey) ? suffix : `${suffix}-${camelCaseToDash(String(childKey))}`
750
+ walk(childVal, _modifier, newSuffix)
751
+ })
752
+ } else {
753
+ emitLeaf(val, _modifier, suffix)
754
+ }
755
+ }
756
+ _.each(value, (_value, _modifier) => walk(_value, _modifier, ''))
713
757
  } else {
714
758
  convertedStyles += `'.${setModifier2(rule, modifier)}${setModifier2(modifier)}': ` + _.replace(properties, /{value}/g, parseValue(value)) + '\n'
715
759
  }
@@ -1,6 +1,13 @@
1
1
  import fs from 'fs'
2
2
  import _ from 'lodash'
3
3
  import { deriveAlphaKey } from '../semantic-helpers.js'
4
+ import {
5
+ projectsFA_TSS_File,
6
+ srcFontAwesomeTSSFile,
7
+ srcMaterialIconsTSSFile,
8
+ srcMaterialSymbolsTSSFile,
9
+ srcFramework7FontTSSFile
10
+ } from '../constants.js'
4
11
 
5
12
  // Internal variables and constants
6
13
  const _applyClasses = {}
@@ -30,14 +37,25 @@ export function processProperties(info, selectorAndDeclarationBlock, selectorsAn
30
37
  _.each(rulesAndValuesPair, (value, rule) => {
31
38
  if (debug) console.log('rule:', rule, 'value:', value)
32
39
  if (typeof value === 'object') {
33
- _.each(value, (_value, key) => {
34
- if (debug) console.log('key:', key, '_value:', _value)
35
- let processedProperties = _.replace(declarationBlock, /{value}/g, parseValue(_value, minusSigns))
40
+ const emitLeaf = (leafValue, key, suffix) => {
41
+ if (debug) console.log('key:', key, 'leafValue:', leafValue, 'suffix:', suffix)
42
+ let processedProperties = _.replace(declarationBlock, /{value}/g, parseValue(leafValue, minusSigns))
36
43
  if (declarationBlock.includes('double')) {
37
- processedProperties = _.replace(processedProperties, /{double}/g, parseValue(_value, minusSigns) * 2)
44
+ processedProperties = _.replace(processedProperties, /{double}/g, parseValue(leafValue, minusSigns) * 2)
38
45
  }
39
- convertedStyles += defaultModifier(key) ? `'.${setModifier2(mainSelector, rule)}${setModifier2(rule)}${setModifier2(selector)}': ${processedProperties}\n` : `'.${setModifier2(mainSelector, rule)}${setModifier2(rule, key)}${setModifier2(key)}${setModifier2(selector)}': ${processedProperties}\n`
40
- })
46
+ convertedStyles += defaultModifier(key) ? `'.${setModifier2(mainSelector, rule)}${setModifier2(rule)}${suffix}${setModifier2(selector)}': ${processedProperties}\n` : `'.${setModifier2(mainSelector, rule)}${setModifier2(rule, key)}${setModifier2(key)}${suffix}${setModifier2(selector)}': ${processedProperties}\n`
47
+ }
48
+ const walk = (val, key, suffix) => {
49
+ if (val && typeof val === 'object') {
50
+ _.each(val, (childVal, childKey) => {
51
+ const newSuffix = defaultModifier(childKey) ? suffix : `${suffix}-${camelCaseToDash(String(childKey))}`
52
+ walk(childVal, key, newSuffix)
53
+ })
54
+ } else {
55
+ emitLeaf(val, key, suffix)
56
+ }
57
+ }
58
+ _.each(value, (_value, key) => walk(_value, key, ''))
41
59
  } else {
42
60
  let processedProperties = _.replace(declarationBlock, /{value}/g, parseValue(value, minusSigns))
43
61
  if (declarationBlock.includes('double')) {
@@ -464,6 +482,18 @@ export function compileApplyDirectives(twClasses) {
464
482
  const twClassesArray = twClasses.split(/\r?\n/)
465
483
  const fontsClassesArray = (fs.existsSync(cwd + '/purgetss/styles/fonts.tss')) ? fs.readFileSync(cwd + '/purgetss/styles/fonts.tss', 'utf8').split(/\r?\n/) : null
466
484
 
485
+ // Default icon font sources (FontAwesome, Material Icons/Symbols, Framework7).
486
+ // Project-level fontawesome.tss (Pro/Beta) takes precedence over the bundled default,
487
+ // matching the precedence used by purgeFontAwesome().
488
+ const iconClassesArrays = [
489
+ fs.existsSync(projectsFA_TSS_File) ? projectsFA_TSS_File : srcFontAwesomeTSSFile,
490
+ srcMaterialIconsTSSFile,
491
+ srcMaterialSymbolsTSSFile,
492
+ srcFramework7FontTSSFile
493
+ ]
494
+ .filter(p => fs.existsSync(p))
495
+ .map(p => fs.readFileSync(p, 'utf8').split(/\r?\n/))
496
+
467
497
  _.each(_applyClasses, (values, className) => {
468
498
  const indexOfModifier = findIndexOfClassName(`'${className}':`, twClassesArray)
469
499
 
@@ -519,6 +549,14 @@ export function compileApplyDirectives(twClasses) {
519
549
  if (!foundClass && fontsClassesArray) {
520
550
  foundClass = fontsClassesArray[findIndexOfClassName(genericClassName, fontsClassesArray)]
521
551
  }
552
+ // Last resort: search default icon font sources (FontAwesome, Material Icons/Symbols, Framework7)
553
+ // so apply: directives can use fas, fa-*, mi-*, ms-*, f7-* without requiring fonts.tss
554
+ if (!foundClass) {
555
+ for (const arr of iconClassesArrays) {
556
+ const idx = findIndexOfClassName(genericClassName, arr)
557
+ if (idx !== -1) { foundClass = arr[idx]; break }
558
+ }
559
+ }
522
560
  }
523
561
 
524
562
  if (foundClass) compoundClasses.push(justProperties(foundClass))
@@ -607,8 +645,8 @@ function deduplicateLineProperties(line) {
607
645
  let depth = 0
608
646
  let current = ''
609
647
  for (const char of propsStr) {
610
- if (char === '{') depth++
611
- else if (char === '}') depth--
648
+ if (char === '{' || char === '[') depth++
649
+ else if (char === '}' || char === ']') depth--
612
650
  else if (char === ',' && depth === 0) {
613
651
  if (current.trim()) props.push(current.trim())
614
652
  current = ''
@@ -77,6 +77,14 @@ export const logger = {
77
77
  _emit(chalk.yellow(args.join(' ')) + ' file created!')
78
78
  },
79
79
 
80
+ /**
81
+ * Log success messages in green
82
+ * @param {...any} args - Arguments to log
83
+ */
84
+ success: function(...args) {
85
+ _emit(chalk.green(args.join(' ')))
86
+ },
87
+
80
88
  /**
81
89
  * Enable section mode. The next info/warn/error/file call becomes the
82
90
  * ::PurgeTSS:: header; subsequent calls print indented without prefix.
@@ -122,6 +130,10 @@ export const logger = {
122
130
  }
123
131
  }
124
132
 
133
+ // Aliases: long-form `warning` matches the semantic of `warn`. Multiple
134
+ // callsites across branding/images/svg flows expect this name.
135
+ logger.warning = logger.warn
136
+
125
137
  /**
126
138
  * Get current debug mode status
127
139
  * @returns {boolean} Current debug status
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Config validator for purgetss/config.cjs.
3
+ *
4
+ * Validates known fields in the user's config and, on type mismatch, throws an
5
+ * error formatted via the shared error-reporter so that File / Path / Line /
6
+ * Context / Issue / Fix are obvious instead of crashing downstream with
7
+ * cryptic messages like `rule.startsWith is not a function`.
8
+ *
9
+ * Currently validates:
10
+ * - theme.fontFamily.* and theme.extend.fontFamily.*
11
+ * Expected: string. Detects Tailwind-style arrays (`['Inter', 'sans-serif']`)
12
+ * and reports with a fix snippet.
13
+ *
14
+ * Extend by adding entries to FIELD_RULES below. Each rule names the JSON
15
+ * path, the expected JS type, and a tip explaining the fix.
16
+ */
17
+
18
+ import fs from 'fs'
19
+ import * as acorn from 'acorn'
20
+ import chalk from 'chalk'
21
+ import { throwSyntaxError } from '../error-reporter.js'
22
+
23
+ // ─── Field rules ────────────────────────────────────────────────────────────
24
+ const FIELD_RULES = [
25
+ {
26
+ parent: 'theme.fontFamily',
27
+ expected: 'string',
28
+ tipFor: (key, value) => buildFontFamilyTip(key, value)
29
+ },
30
+ {
31
+ parent: 'theme.extend.fontFamily',
32
+ expected: 'string',
33
+ tipFor: (key, value) => buildFontFamilyTip(key, value)
34
+ }
35
+ ]
36
+
37
+ function buildFontFamilyTip(key, value) {
38
+ if (Array.isArray(value)) {
39
+ const first = value.length > 0 ? value[0] : 'FontName'
40
+ return `Use a string instead of an array. Tailwind-style fallback fonts are not supported — Titanium accepts a single font family per element. Change to: ${chalk.green(`${quoteKey(key)}: '${first}'`)}`
41
+ }
42
+ return `Expected a string. Change to: ${chalk.green(`${quoteKey(key)}: 'FontName'`)}`
43
+ }
44
+
45
+ function quoteKey(key) {
46
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : `'${key}'`
47
+ }
48
+
49
+ // ─── Public API ─────────────────────────────────────────────────────────────
50
+
51
+ /**
52
+ * Validate the loaded config object against FIELD_RULES.
53
+ * Throws a formatted Error (via error-reporter) on the first mismatch found.
54
+ *
55
+ * @param {Object} configObject - The required()'d config object.
56
+ * @param {string} configPath - Absolute path to the config.cjs file.
57
+ */
58
+ export function validateConfig(configObject, configPath) {
59
+ for (const rule of FIELD_RULES) {
60
+ const parent = getByPath(configObject, rule.parent)
61
+ if (!parent || typeof parent !== 'object') continue
62
+
63
+ for (const key of Object.keys(parent)) {
64
+ const value = parent[key]
65
+ if (typeof value === rule.expected) continue
66
+
67
+ const jsonPath = `${rule.parent}.${key}`
68
+ const source = safeReadFile(configPath)
69
+ const contextLines = source ? source.split('\n') : null
70
+ const line = contextLines ? findPropertyLine(source, jsonPath) : null
71
+
72
+ throwSyntaxError({
73
+ type: 'Config',
74
+ file: configPath,
75
+ path: jsonPath,
76
+ line,
77
+ contextLines,
78
+ issue: `Expected ${rule.expected}, got ${describeType(value)} (${previewValue(value)})`,
79
+ fix: rule.tipFor(key, value)
80
+ })
81
+ }
82
+ }
83
+ }
84
+
85
+ // ─── AST scan to find the line where a dotted-path property is declared ─────
86
+
87
+ function findPropertyLine(source, dottedPath) {
88
+ let ast
89
+ try {
90
+ ast = acorn.parse(source, { ecmaVersion: 'latest', locations: true })
91
+ } catch (_e) {
92
+ return null
93
+ }
94
+
95
+ let found = null
96
+
97
+ function walk(node, currentPath) {
98
+ if (found || !node || typeof node !== 'object') return
99
+
100
+ if (node.type === 'ObjectExpression') {
101
+ for (const prop of node.properties) {
102
+ if (prop.type !== 'Property') continue
103
+ const keyName = prop.key.name || prop.key.value
104
+ if (keyName == null) continue
105
+ const nextPath = currentPath ? `${currentPath}.${keyName}` : keyName
106
+
107
+ if (nextPath === dottedPath && prop.loc) {
108
+ found = prop.loc.start.line
109
+ return
110
+ }
111
+ walk(prop.value, nextPath)
112
+ if (found) return
113
+ }
114
+ return
115
+ }
116
+
117
+ if (node.type === 'AssignmentExpression') {
118
+ walk(node.right, currentPath)
119
+ return
120
+ }
121
+
122
+ for (const key of Object.keys(node)) {
123
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'range') continue
124
+ const child = node[key]
125
+ if (Array.isArray(child)) {
126
+ for (const c of child) {
127
+ walk(c, currentPath)
128
+ if (found) return
129
+ }
130
+ } else if (child && typeof child === 'object') {
131
+ walk(child, currentPath)
132
+ }
133
+ }
134
+ }
135
+
136
+ walk(ast, '')
137
+ return found
138
+ }
139
+
140
+ // ─── Helpers ────────────────────────────────────────────────────────────────
141
+
142
+ function describeType(value) {
143
+ if (value === null) return 'null'
144
+ if (Array.isArray(value)) return 'Array'
145
+ return typeof value
146
+ }
147
+
148
+ function previewValue(value) {
149
+ try {
150
+ const json = JSON.stringify(value)
151
+ return json && json.length > 60 ? json.slice(0, 57) + '...' : json
152
+ } catch (_e) {
153
+ return String(value)
154
+ }
155
+ }
156
+
157
+ function getByPath(obj, dottedPath) {
158
+ return dottedPath.split('.').reduce((acc, key) => (acc == null ? acc : acc[key]), obj)
159
+ }
160
+
161
+ function safeReadFile(filePath) {
162
+ try {
163
+ return fs.readFileSync(filePath, 'utf8')
164
+ } catch (_e) {
165
+ return null
166
+ }
167
+ }