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.
- package/README.md +28 -0
- package/bin/purgetss +23 -0
- package/dist/purgetss.ui.js +1 -1
- package/lib/templates/create/index.xml +1 -1
- package/lib/templates/purgetss.config.js.cjs +3 -1
- package/package.json +2 -2
- package/src/cli/commands/build.js +9 -4
- package/src/cli/commands/images.js +49 -2
- package/src/cli/commands/purge.js +31 -4
- package/src/cli/commands/shades.js +2 -2
- package/src/cli/utils/cli-helpers.js +15 -5
- package/src/cli/utils/unsupported-class-reporter.js +209 -0
- package/src/core/analyzers/class-extractor.js +54 -0
- package/src/core/analyzers/controller-svg-refs.js +154 -0
- package/src/core/branding/brand-config.js +7 -0
- package/src/core/branding/ensure-brand-section.js +4 -3
- package/src/core/branding/gen-feature-graphic.js +57 -0
- package/src/core/branding/index.js +28 -4
- package/src/core/branding/post-gen-notes.js +2 -2
- package/{experimental/completions2.js → src/core/builders/auto-utilities-builder.js} +74 -40
- package/src/core/builders/tailwind-builder.js +2 -2
- package/src/core/builders/tailwind-helpers.js +0 -444
- package/src/core/images/ensure-images-section.js +6 -4
- package/src/core/images/gen-scales.js +96 -13
- package/src/core/images/index.js +121 -9
- package/src/core/purger/icon-purger.js +7 -3
- package/src/core/purger/tailwind-purger.js +43 -5
- package/src/core/svg/cache.js +96 -0
- package/src/core/svg/derive-dimensions.js +120 -0
- package/src/core/svg/index.js +215 -0
- package/src/core/svg/resolve-classes.js +46 -0
- package/src/core/svg/sync-images.js +278 -0
- package/src/core/svg/tss-reader.js +134 -0
- package/src/dev/builders/tailwind-builder.js +3 -11
- package/src/shared/config-manager.js +72 -3
- package/src/shared/error-reporter.js +117 -0
- package/src/shared/helpers/colors.js +57 -13
- package/src/shared/helpers/core.js +0 -19
- package/src/shared/helpers/utils.js +146 -36
- package/src/shared/logger.js +12 -0
- package/src/shared/semantic-helpers.js +143 -0
- package/src/shared/validation/config-validator.js +167 -0
package/src/shared/logger.js
CHANGED
|
@@ -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
|
+
}
|