purgetss 7.7.1 → 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 (42) hide show
  1. package/README.md +28 -0
  2. package/bin/purgetss +23 -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 +2 -2
  7. package/src/cli/commands/build.js +9 -4
  8. package/src/cli/commands/images.js +49 -2
  9. package/src/cli/commands/purge.js +31 -4
  10. package/src/cli/commands/shades.js +2 -2
  11. package/src/cli/utils/cli-helpers.js +15 -5
  12. package/src/cli/utils/unsupported-class-reporter.js +209 -0
  13. package/src/core/analyzers/class-extractor.js +54 -0
  14. package/src/core/analyzers/controller-svg-refs.js +154 -0
  15. package/src/core/branding/brand-config.js +7 -0
  16. package/src/core/branding/ensure-brand-section.js +4 -3
  17. package/src/core/branding/gen-feature-graphic.js +57 -0
  18. package/src/core/branding/index.js +28 -4
  19. package/src/core/branding/post-gen-notes.js +2 -2
  20. package/{experimental/completions2.js → src/core/builders/auto-utilities-builder.js} +74 -40
  21. package/src/core/builders/tailwind-builder.js +2 -2
  22. package/src/core/builders/tailwind-helpers.js +0 -444
  23. package/src/core/images/ensure-images-section.js +6 -4
  24. package/src/core/images/gen-scales.js +96 -13
  25. package/src/core/images/index.js +121 -9
  26. package/src/core/purger/icon-purger.js +7 -3
  27. package/src/core/purger/tailwind-purger.js +43 -5
  28. package/src/core/svg/cache.js +96 -0
  29. package/src/core/svg/derive-dimensions.js +120 -0
  30. package/src/core/svg/index.js +215 -0
  31. package/src/core/svg/resolve-classes.js +46 -0
  32. package/src/core/svg/sync-images.js +278 -0
  33. package/src/core/svg/tss-reader.js +134 -0
  34. package/src/dev/builders/tailwind-builder.js +3 -11
  35. package/src/shared/config-manager.js +72 -3
  36. package/src/shared/error-reporter.js +117 -0
  37. package/src/shared/helpers/colors.js +57 -13
  38. package/src/shared/helpers/core.js +0 -19
  39. package/src/shared/helpers/utils.js +146 -36
  40. package/src/shared/logger.js +12 -0
  41. package/src/shared/semantic-helpers.js +143 -0
  42. package/src/shared/validation/config-validator.js +167 -0
@@ -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,143 @@
1
+ /**
2
+ * PurgeTSS - Semantic Colors Helpers
3
+ *
4
+ * Auto-derivation of semantic color keys with applied alpha for opacity
5
+ * modifiers (e.g. `bg-surface/65`). Mutates `semantic.colors.json` idempotently
6
+ * by adding `<originalKey>_<alphaPercent>` entries with `{ color, alpha }` for
7
+ * `light` and `dark`.
8
+ *
9
+ * Lifecycle:
10
+ * - Names of semantic colors are tracked via `registerSemanticName()` while
11
+ * the utilities builder expands `theme.extend.colors`.
12
+ * - Hooks in `tailwind-purger` and `compileApplyDirectives` call
13
+ * `deriveAlphaKey()` before falling back to warn/throw on missing hex.
14
+ * - CLI commands flush the cached JSON to disk via `flushSemanticColors()`
15
+ * at the end of their run.
16
+ *
17
+ * @fileoverview Semantic color auto-derivation for opacity modifiers
18
+ * @since 7.9.0
19
+ */
20
+
21
+ import fs from 'fs'
22
+ import { getSemanticColorsPath } from '../cli/utils/project-detection.js'
23
+
24
+ const _semanticNames = new Set()
25
+ let _cache = null
26
+ let _dirty = false
27
+ let _lastMtime = 0
28
+ let _lastPath = null
29
+
30
+ /**
31
+ * Reset all in-memory state. Intended for tests; not part of the public API.
32
+ */
33
+ export function _resetSemanticHelpersState() {
34
+ _semanticNames.clear()
35
+ _cache = null
36
+ _dirty = false
37
+ _lastMtime = 0
38
+ _lastPath = null
39
+ }
40
+
41
+ /**
42
+ * Track a name that resolves to a semantic color (string, non-hex). Called by
43
+ * the utilities builder while expanding `theme.extend.colors`.
44
+ *
45
+ * @param {string} name
46
+ */
47
+ export function registerSemanticName(name) {
48
+ if (typeof name === 'string' && name.length > 0) _semanticNames.add(name)
49
+ }
50
+
51
+ /**
52
+ * @param {string} name
53
+ * @returns {boolean}
54
+ */
55
+ export function isSemanticColorName(name) {
56
+ return _semanticNames.has(name)
57
+ }
58
+
59
+ /**
60
+ * Load `semantic.colors.json` for the current project. Cached by mtime so
61
+ * repeated calls in the same build are cheap. The cache is invalidated when
62
+ * the file mtime changes on disk (relevant for watch-mode cycles where the
63
+ * JSON gets rewritten between runs).
64
+ *
65
+ * @returns {Object} parsed JSON, or `{}` when the file doesn't exist
66
+ */
67
+ export function loadSemanticColors() {
68
+ const p = getSemanticColorsPath()
69
+ if (!fs.existsSync(p)) {
70
+ _cache = {}
71
+ _lastMtime = 0
72
+ _lastPath = p
73
+ return _cache
74
+ }
75
+ const mtime = fs.statSync(p).mtimeMs
76
+ if (_cache && _lastPath === p && mtime === _lastMtime) return _cache
77
+ _cache = JSON.parse(fs.readFileSync(p, 'utf8'))
78
+ _lastMtime = mtime
79
+ _lastPath = p
80
+ return _cache
81
+ }
82
+
83
+ /**
84
+ * Derive a semantic key with the requested alpha applied. Idempotent: if the
85
+ * derived key already exists with matching values it's reused; if it exists
86
+ * with conflicting values an Error is thrown so manual edits aren't silently
87
+ * overwritten.
88
+ *
89
+ * @param {string} baseKey - The original semantic key (e.g. `surfaceColor`).
90
+ * @param {number|string} alphaPercent - Integer 0..100 (e.g. 65).
91
+ * @returns {string|null} The derived key, or `null` if `baseKey` isn't in the JSON.
92
+ */
93
+ export function deriveAlphaKey(baseKey, alphaPercent) {
94
+ const semantic = loadSemanticColors()
95
+ const base = semantic[baseKey]
96
+ if (!base) return null
97
+
98
+ const alphaStr = String(alphaPercent)
99
+ const newKey = `${baseKey}_${alphaStr}`
100
+
101
+ const lightHex = typeof base.light === 'string' ? base.light : base.light?.color
102
+ const darkHex = typeof base.dark === 'string' ? base.dark : base.dark?.color
103
+
104
+ if (semantic[newKey]) {
105
+ const existing = semantic[newKey]
106
+ const existingLightHex = typeof existing.light === 'string' ? existing.light : existing.light?.color
107
+ const existingDarkHex = typeof existing.dark === 'string' ? existing.dark : existing.dark?.color
108
+ const existingLightAlpha = typeof existing.light === 'string' ? null : existing.light?.alpha
109
+ const existingDarkAlpha = typeof existing.dark === 'string' ? null : existing.dark?.alpha
110
+
111
+ if (
112
+ existingLightHex !== lightHex ||
113
+ existingDarkHex !== darkHex ||
114
+ existingLightAlpha !== alphaStr ||
115
+ existingDarkAlpha !== alphaStr
116
+ ) {
117
+ throw new Error(
118
+ `Conflict: "${newKey}" already exists in semantic.colors.json with different values. Manual edit detected — resolve before continuing.`
119
+ )
120
+ }
121
+ return newKey
122
+ }
123
+
124
+ semantic[newKey] = {
125
+ light: { color: lightHex, alpha: alphaStr },
126
+ dark: { color: darkHex, alpha: alphaStr }
127
+ }
128
+ _dirty = true
129
+ return newKey
130
+ }
131
+
132
+ /**
133
+ * Persist any pending derivations back to `semantic.colors.json`. No-op when
134
+ * nothing changed. Writes a trailing newline for clean git diffs.
135
+ */
136
+ export function flushSemanticColors() {
137
+ if (!_dirty) return
138
+ const p = getSemanticColorsPath()
139
+ fs.writeFileSync(p, JSON.stringify(_cache, null, 2) + '\n')
140
+ _dirty = false
141
+ _lastMtime = fs.statSync(p).mtimeMs
142
+ _lastPath = p
143
+ }
@@ -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
+ }