purgetss 7.7.1 → 7.9.0
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 +8 -0
- package/bin/purgetss +10 -0
- package/dist/purgetss.ui.js +1 -1
- package/package.json +2 -2
- package/src/cli/commands/build.js +9 -4
- package/src/cli/commands/images.js +8 -0
- package/src/cli/commands/purge.js +17 -3
- package/src/cli/commands/shades.js +2 -2
- package/src/cli/utils/unsupported-class-reporter.js +209 -0
- package/{experimental/completions2.js → src/core/builders/auto-utilities-builder.js} +56 -27
- package/src/core/builders/tailwind-builder.js +2 -2
- package/src/core/builders/tailwind-helpers.js +0 -444
- package/src/core/images/gen-scales.js +24 -6
- package/src/core/images/index.js +9 -2
- package/src/core/purger/tailwind-purger.js +40 -4
- package/src/shared/helpers/core.js +0 -19
- package/src/shared/helpers/utils.js +100 -28
- package/src/shared/semantic-helpers.js +143 -0
- package/src/dev/builders/tailwind-builder.js +0 -26
package/README.md
CHANGED
|
@@ -377,6 +377,14 @@ Button: {
|
|
|
377
377
|
|
|
378
378
|
## Recent changes
|
|
379
379
|
|
|
380
|
+
### v7.8.0
|
|
381
|
+
|
|
382
|
+
- **`--width <n>` flag for `images`.** Pin Android `mdpi` / iPhone `@1x` to a specific width in pixels (e.g. `purgetss images logo.svg --width 256`); larger scales derive at ×1.5, ×2, ×3, ×4 with height staying proportional. Recommended when the source is an SVG export from Affinity / Illustrator with a disproportionate viewBox — the legacy 4× master convention produces unpredictable sizes there, and the new flag pins the result exactly. When you pass an SVG without `--width`, `images` now prints a one-time hint suggesting the flag, then falls back to the legacy behavior.
|
|
383
|
+
- **Class syntax pre-validation.** `purgetss` now halts with a structured `Class Syntax Error` block (file + line + suggested fix) when it detects known authoring mistakes in your class names: inverted negative sign (`top-(-10)` → `-top-(10)`), Tailwind-style brackets (`top-[10px]` → `top-(10px)`), empty parentheses (`wh-()`), whitespace inside parentheses (`wh-( 200 )`), and redundant `px` unit (`top-(10px)` → `top-(10)`).
|
|
384
|
+
- All offenders are reported in a single run, so you can fix them in one pass.
|
|
385
|
+
- Generic unknown classes (typos, vendor utilities not enabled, custom classes not yet declared) are NOT flagged by the validator — they continue to flow silently into the `// Unused or unsupported classes` block in `app.tss`, exactly as in previous versions.
|
|
386
|
+
- **Arbitrary-value parser no longer crashes on negative values inside parentheses.** Classes like `top-(-10)`, `mt-(-5)`, or `origin-(-10,-20)` used to trigger a `Cannot read properties of null (reading 'pop')` exception. The parser was rewritten to extract the `(...)` portion first, so a `-` inside the value never breaks the split — and the new pre-validator catches the inverted-sign form before it gets that far.
|
|
387
|
+
|
|
380
388
|
### v7.7.0
|
|
381
389
|
|
|
382
390
|
- `brand` now uses grouped config sections: `brand.logos`, `brand.padding`, `brand.android`, `brand.ios`, and `brand.colors`.
|
package/bin/purgetss
CHANGED
|
@@ -353,6 +353,13 @@ Writes directly to the project (auto-detects Alloy vs Classic):
|
|
|
353
353
|
Defaults come from the ${chalk.cyan('images:')} section in ${chalk.cyan('purgetss/config.cjs')}.
|
|
354
354
|
CLI flags always win over config values.
|
|
355
355
|
|
|
356
|
+
Sizing:
|
|
357
|
+
By default, sources are treated as 4× masters (xxxhdpi/@4x); all other
|
|
358
|
+
scales derive at 1/4, 1.5/4, 2/4, 3/4 of the source's natural pixels.
|
|
359
|
+
Pass ${chalk.cyan('--width <n>')} to pin Android mdpi (= iPhone @1x) to a specific width;
|
|
360
|
+
larger scales derive as ×1.5, ×2, ×3, ×4 (height stays proportional).
|
|
361
|
+
Recommended for SVGs from vector editors with oversized viewBoxes.
|
|
362
|
+
|
|
356
363
|
Examples:
|
|
357
364
|
${chalk.cyan('purgetss images')} # uses purgetss/images/ + config
|
|
358
365
|
${chalk.cyan('purgetss images')} ./docs/screenshots # scope to one folder
|
|
@@ -361,12 +368,15 @@ Examples:
|
|
|
361
368
|
${chalk.cyan('purgetss images')} --ios # iPhone scales only
|
|
362
369
|
${chalk.cyan('purgetss images')} --format webp # convert all outputs to WebP
|
|
363
370
|
${chalk.cyan('purgetss images')} --format png --quality 95
|
|
371
|
+
${chalk.cyan('purgetss images')} logo.svg --width 256 # pin @1x/mdpi to 256 px wide
|
|
372
|
+
${chalk.cyan('purgetss images')} banner.svg --width 512 --format webp
|
|
364
373
|
${chalk.cyan('purgetss images')} --dry-run
|
|
365
374
|
`)
|
|
366
375
|
.option('--android', 'Generate only Android density variants (skip iPhone)')
|
|
367
376
|
.option('--ios', 'Generate only iPhone scale variants (skip Android)')
|
|
368
377
|
.option('--format <ext>', 'Convert all outputs to this format: webp|jpeg|png|avif|gif|tiff')
|
|
369
378
|
.option('--quality <n>', 'JPEG/WebP/AVIF quality 0-100 (default: 85)', (v) => parseInt(v, 10))
|
|
379
|
+
.option('--width <n>', 'Target width in px for @1x/mdpi (other scales derive: ×1.5, ×2, ×3, ×4)', (v) => parseInt(v, 10))
|
|
370
380
|
.option('--project <path>', 'Project root (default: cwd)')
|
|
371
381
|
.option('--dry-run', 'Preview without writing any files')
|
|
372
382
|
.option('-y, --yes', 'Skip the overwrite confirmation prompt')
|
package/dist/purgetss.ui.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "purgetss",
|
|
4
|
-
"version": "7.
|
|
4
|
+
"version": "7.9.0",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"purgetss": "bin/purgetss"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"dist/*.tss",
|
|
18
18
|
"dist/configs/",
|
|
19
19
|
"assets/fonts/",
|
|
20
|
-
"
|
|
20
|
+
"src/core/builders/auto-utilities-builder.js"
|
|
21
21
|
],
|
|
22
22
|
"publishConfig": {
|
|
23
23
|
"access": "public"
|
|
@@ -14,6 +14,7 @@ import { alloyProject } from '../../shared/utils.js'
|
|
|
14
14
|
import { ensureConfig } from '../../shared/config-manager.js'
|
|
15
15
|
import { buildTailwindBasedOnConfigOptions } from '../../core/builders/tailwind-builder.js'
|
|
16
16
|
import { createDefinitionsFile } from './init.js'
|
|
17
|
+
import { flushSemanticColors } from '../../shared/semantic-helpers.js'
|
|
17
18
|
|
|
18
19
|
// Import FontAwesome functions from their new modular location
|
|
19
20
|
import { buildFontAwesome, buildFontAwesomeJS } from '../../dev/builders/fontawesome-builder.js'
|
|
@@ -28,10 +29,14 @@ import { buildFontAwesome, buildFontAwesomeJS } from '../../dev/builders/fontawe
|
|
|
28
29
|
export function build(options) {
|
|
29
30
|
if (alloyProject()) {
|
|
30
31
|
ensureConfig()
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
try {
|
|
33
|
+
buildTailwindBasedOnConfigOptions(options)
|
|
34
|
+
buildFontAwesome()
|
|
35
|
+
buildFontAwesomeJS()
|
|
36
|
+
createDefinitionsFile()
|
|
37
|
+
} finally {
|
|
38
|
+
flushSemanticColors()
|
|
39
|
+
}
|
|
35
40
|
return true
|
|
36
41
|
}
|
|
37
42
|
return false
|
|
@@ -37,6 +37,13 @@ export async function images(cliSource, options = {}) {
|
|
|
37
37
|
process.exit(1)
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
if (options.width !== undefined) {
|
|
41
|
+
if (!Number.isFinite(options.width) || !Number.isInteger(options.width) || options.width < 1 || options.width > 8192) {
|
|
42
|
+
logger.error(`Invalid --width '${options.width}'. Must be an integer between 1 and 8192.`)
|
|
43
|
+
process.exit(1)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
40
47
|
const format = options.format ?? cfg.format ?? null
|
|
41
48
|
if (format && !VALID_FORMATS.has(format.toLowerCase())) {
|
|
42
49
|
logger.error(`Invalid --format '${format}'. Valid: ${[...VALID_FORMATS].join(', ')}`)
|
|
@@ -57,6 +64,7 @@ export async function images(cliSource, options = {}) {
|
|
|
57
64
|
iphoneOnly: Boolean(options.ios),
|
|
58
65
|
format: format ? format.toLowerCase() : null,
|
|
59
66
|
quality: options.quality ?? cfg.quality ?? 85,
|
|
67
|
+
baseWidth: options.width ?? null,
|
|
60
68
|
dryRun: Boolean(options.dryRun),
|
|
61
69
|
yes: Boolean(options.yes),
|
|
62
70
|
confirmOverwrites: cfg.confirmOverwrites !== false
|
|
@@ -33,6 +33,7 @@ import { getConfigOptions, getConfigFile, ensureConfig } from '../../shared/conf
|
|
|
33
33
|
// Import purger functions from core modules
|
|
34
34
|
import { processControllers } from '../../core/analyzers/class-extractor.js'
|
|
35
35
|
import { purgeTailwind } from '../../core/purger/tailwind-purger.js'
|
|
36
|
+
import { flushSemanticColors } from '../../shared/semantic-helpers.js'
|
|
36
37
|
import {
|
|
37
38
|
purgeFontAwesome,
|
|
38
39
|
purgeMaterialIcons,
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
purgeFramework7
|
|
41
42
|
} from '../../core/purger/icon-purger.js'
|
|
42
43
|
import { purgeFonts } from '../../core/purger/fonts-purger.js'
|
|
44
|
+
import { validateClassSyntax } from '../utils/unsupported-class-reporter.js'
|
|
43
45
|
|
|
44
46
|
// Global variables (EXACT copies from original src/index.js)
|
|
45
47
|
let configOptions = {}
|
|
@@ -693,10 +695,21 @@ export function purgeClasses(options) {
|
|
|
693
695
|
|
|
694
696
|
try {
|
|
695
697
|
uniqueClasses = getUniqueClasses()
|
|
698
|
+
|
|
699
|
+
// Pre-validate class syntax. Halts on inverted negatives, Tailwind
|
|
700
|
+
// brackets, empty parens, etc. — but NOT on generic unknown classes
|
|
701
|
+
// (those fall through silently to "// Unused or unsupported classes").
|
|
702
|
+
validateClassSyntax({
|
|
703
|
+
classes: uniqueClasses,
|
|
704
|
+
viewPaths: getViewPaths(),
|
|
705
|
+
controllerPaths: getControllerPaths()
|
|
706
|
+
})
|
|
696
707
|
} catch (error) {
|
|
697
|
-
// Handle pre-validation errors (XML syntax errors detected before
|
|
698
|
-
|
|
699
|
-
|
|
708
|
+
// Handle pre-validation errors (XML syntax errors detected before
|
|
709
|
+
// parsing) and class-syntax errors (authoring mistakes detected
|
|
710
|
+
// before purging). In both cases the error block was already
|
|
711
|
+
// printed; just exit cleanly.
|
|
712
|
+
if (error.isPreValidationError || error.isClassSyntaxError) {
|
|
700
713
|
// eslint-disable-next-line n/no-process-exit
|
|
701
714
|
process.exit(1)
|
|
702
715
|
}
|
|
@@ -728,6 +741,7 @@ export function purgeClasses(options) {
|
|
|
728
741
|
|
|
729
742
|
finish()
|
|
730
743
|
} finally {
|
|
744
|
+
flushSemanticColors()
|
|
731
745
|
logger.endSection()
|
|
732
746
|
}
|
|
733
747
|
|
|
@@ -329,8 +329,8 @@ export function buildSemanticPalette(family, kebabName, alpha) {
|
|
|
329
329
|
const mirror = sorted[sorted.length - 1 - i]
|
|
330
330
|
const key = `${camelName}${shade.number}`
|
|
331
331
|
semanticEntries[key] = {
|
|
332
|
-
light: wrapValue(
|
|
333
|
-
dark: wrapValue(
|
|
332
|
+
light: wrapValue(shade.hexcode, alpha),
|
|
333
|
+
dark: wrapValue(mirror.hexcode, alpha)
|
|
334
334
|
}
|
|
335
335
|
configMapping[shade.number] = key
|
|
336
336
|
})
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - Class Syntax Validator
|
|
3
|
+
*
|
|
4
|
+
* Pre-validation helper that runs BEFORE the purge starts. Scans every unique
|
|
5
|
+
* class name pulled from XML views (and JS controllers) for well-known
|
|
6
|
+
* authoring mistakes:
|
|
7
|
+
*
|
|
8
|
+
* - Inverted negative sign: top-(-10) → -top-(10)
|
|
9
|
+
* - Tailwind-style brackets: top-[10px] → top-(10px)
|
|
10
|
+
* - Empty parentheses: wh-() → wh-(<value>)
|
|
11
|
+
* - Whitespace in parens: wh-( 200 ) → wh-(200)
|
|
12
|
+
* - Redundant px units: top-(10px) → top-(10)
|
|
13
|
+
*
|
|
14
|
+
* On any match the validator collects every offender, prints one block per
|
|
15
|
+
* offender (file + line + fix, mirroring throwPreValidationError) and throws
|
|
16
|
+
* an `isClassSyntaxError` so the CLI exits cleanly without running the purge.
|
|
17
|
+
*
|
|
18
|
+
* Generic unknown classes (typos, vendor utilities not enabled, custom
|
|
19
|
+
* classes not yet defined) are intentionally left alone — they are still
|
|
20
|
+
* captured silently in the "// Unused or unsupported classes" section of
|
|
21
|
+
* app.tss, but never trigger warnings or halts. That's the only way to keep
|
|
22
|
+
* the workflow usable on projects with dozens of in-progress class names.
|
|
23
|
+
*
|
|
24
|
+
* @author César Estrada
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import fs from 'fs'
|
|
28
|
+
import chalk from 'chalk'
|
|
29
|
+
import { logger } from '../../shared/logger.js'
|
|
30
|
+
|
|
31
|
+
const cwd = process.cwd()
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Pattern detectors. Each detector receives the className and returns
|
|
35
|
+
* { issue, suggestion } when it recognises the problem, or null otherwise.
|
|
36
|
+
* Order matters: more specific detectors first.
|
|
37
|
+
*/
|
|
38
|
+
const detectors = [
|
|
39
|
+
// top-(-10) → -top-(10)
|
|
40
|
+
function detectInvertedNegative(className) {
|
|
41
|
+
const m = className.match(/^([a-zA-Z][\w-]*?)-\(-([^()]+)\)$/)
|
|
42
|
+
if (!m) return null
|
|
43
|
+
return {
|
|
44
|
+
issue: 'Negative sign is inside the parentheses',
|
|
45
|
+
suggestion: `Use ${chalk.green(`"-${m[1]}-(${m[2]})"`)} — PurgeTSS expects the "-" prefix BEFORE the rule, not inside the value`
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
// top-[10px] → top-(10)
|
|
50
|
+
function detectTailwindBrackets(className) {
|
|
51
|
+
if (!className.includes('[') && !className.includes(']')) return null
|
|
52
|
+
const fixed = className.replace(/\[/g, '(').replace(/\]/g, ')')
|
|
53
|
+
return {
|
|
54
|
+
issue: 'Tailwind-style brackets "[ ]" are not supported',
|
|
55
|
+
suggestion: `Use parentheses instead: ${chalk.green(`"${fixed}"`)}`
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
// wh-() → "Add a value"
|
|
60
|
+
function detectEmptyParens(className) {
|
|
61
|
+
const m = className.match(/^([\w-]+)-\(\s*\)$/)
|
|
62
|
+
if (!m) return null
|
|
63
|
+
return {
|
|
64
|
+
issue: 'Empty value inside parentheses',
|
|
65
|
+
suggestion: `Add a value, e.g. ${chalk.green(`"${m[1]}-(10)"`)}`
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
// wh-( 200 ) → wh-(200)
|
|
70
|
+
function detectSpacesInParens(className) {
|
|
71
|
+
const m = className.match(/^([\w-]+)-\(\s+([^()]*?)\s*\)$|^([\w-]+)-\(([^()]*?)\s+\)$/)
|
|
72
|
+
if (!m) return null
|
|
73
|
+
const rule = m[1] || m[3]
|
|
74
|
+
const value = (m[2] || m[4] || '').trim()
|
|
75
|
+
if (!value) return null
|
|
76
|
+
return {
|
|
77
|
+
issue: 'Whitespace inside parentheses',
|
|
78
|
+
suggestion: `Remove the spaces: ${chalk.green(`"${rule}-(${value})"`)}`
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
// top-(10px) — units inside parens that PurgeTSS would strip anyway.
|
|
83
|
+
// Only flag when value is purely numeric+unit; never blanket-flag because
|
|
84
|
+
// some properties (durations, percentages) accept units.
|
|
85
|
+
function detectRedundantUnits(className) {
|
|
86
|
+
const m = className.match(/^([\w-]+)-\((-?\d+(?:\.\d+)?)px\)$/)
|
|
87
|
+
if (!m) return null
|
|
88
|
+
return {
|
|
89
|
+
issue: 'Explicit "px" unit is redundant',
|
|
90
|
+
suggestion: `PurgeTSS treats unit-less values as pixels: ${chalk.green(`"${m[1]}-(${m[2]})"`)}`
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
function detectIssue(className) {
|
|
96
|
+
for (const detector of detectors) {
|
|
97
|
+
const result = detector(className)
|
|
98
|
+
if (result) return result
|
|
99
|
+
}
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Find every (file, line) where `className` appears as a whole token inside a
|
|
105
|
+
* class="..." attribute. Uses an attribute-then-split strategy (rather than
|
|
106
|
+
* matching the class name directly) so arbitrary-value class names like
|
|
107
|
+
* "top-(-10)" — which contain regex-special characters — do not need escaping
|
|
108
|
+
* and cannot false-match a partial substring.
|
|
109
|
+
*/
|
|
110
|
+
function findLocations(className, paths) {
|
|
111
|
+
const locations = []
|
|
112
|
+
const attrRe = /class\s*=\s*["']([^"']*)["']/g
|
|
113
|
+
|
|
114
|
+
for (const filePath of paths) {
|
|
115
|
+
let content
|
|
116
|
+
try {
|
|
117
|
+
content = fs.readFileSync(filePath, 'utf8')
|
|
118
|
+
} catch {
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
const lines = content.split(/\r?\n/)
|
|
122
|
+
for (let i = 0; i < lines.length; i++) {
|
|
123
|
+
attrRe.lastIndex = 0
|
|
124
|
+
let match
|
|
125
|
+
while ((match = attrRe.exec(lines[i])) !== null) {
|
|
126
|
+
const tokens = match[1].split(/\s+/).filter(Boolean)
|
|
127
|
+
if (tokens.includes(className)) {
|
|
128
|
+
locations.push({
|
|
129
|
+
filePath,
|
|
130
|
+
lineNumber: i + 1,
|
|
131
|
+
lineContent: lines[i].trim()
|
|
132
|
+
})
|
|
133
|
+
break
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return locations
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function relativePath(filePath) {
|
|
142
|
+
return filePath.startsWith(cwd + '/') ? filePath.slice(cwd.length + 1) : filePath
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Pre-validate every unique class against the syntax detectors. Collects all
|
|
147
|
+
* offenders (so the dev sees the full list in a single run instead of
|
|
148
|
+
* one-fix-per-attempt), prints a block per offender, then throws.
|
|
149
|
+
*
|
|
150
|
+
* Generic unknown classes are NOT flagged — they fall through silently to
|
|
151
|
+
* the "// Unused or unsupported classes" comment block in app.tss.
|
|
152
|
+
*
|
|
153
|
+
* @param {Object} args
|
|
154
|
+
* @param {string[]} args.classes Unique classes pulled from views/controllers.
|
|
155
|
+
* @param {string[]} args.viewPaths Absolute paths to XML view files.
|
|
156
|
+
* @param {string[]} [args.controllerPaths] Absolute paths to JS controller files.
|
|
157
|
+
* @throws {Error} with `isClassSyntaxError === true` if any class matches a detector.
|
|
158
|
+
*/
|
|
159
|
+
export function validateClassSyntax({ classes, viewPaths, controllerPaths = [] }) {
|
|
160
|
+
if (!classes || classes.length === 0) return
|
|
161
|
+
|
|
162
|
+
const allPaths = [...viewPaths, ...controllerPaths]
|
|
163
|
+
const offenders = []
|
|
164
|
+
|
|
165
|
+
for (const className of classes) {
|
|
166
|
+
const issue = detectIssue(className)
|
|
167
|
+
if (!issue) continue
|
|
168
|
+
offenders.push({
|
|
169
|
+
className,
|
|
170
|
+
issue,
|
|
171
|
+
locations: findLocations(className, allPaths)
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (offenders.length === 0) return
|
|
176
|
+
|
|
177
|
+
for (const offender of offenders) {
|
|
178
|
+
const lines = []
|
|
179
|
+
lines.push(`Class: ${chalk.yellow(`"${offender.className}"`)}`)
|
|
180
|
+
|
|
181
|
+
if (offender.locations.length > 0) {
|
|
182
|
+
const first = offender.locations[0]
|
|
183
|
+
lines.push(`File: ${chalk.yellow(`"${relativePath(first.filePath)}"`)}`)
|
|
184
|
+
lines.push(`Line: ${chalk.yellow(first.lineNumber)}`)
|
|
185
|
+
lines.push(`Content: ${chalk.gray(first.lineContent)}`)
|
|
186
|
+
if (offender.locations.length > 1) {
|
|
187
|
+
const more = offender.locations.length - 1
|
|
188
|
+
lines.push(chalk.gray(`(also used in ${more} other location${more === 1 ? '' : 's'})`))
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
lines.push(chalk.gray('Location: not found in views — likely from a controller or safelist'))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
lines.push('')
|
|
195
|
+
lines.push(chalk.red(`Issue: ${offender.issue.issue}`))
|
|
196
|
+
lines.push(`${chalk.green('Fix:')} ${offender.issue.suggestion}`)
|
|
197
|
+
|
|
198
|
+
logger.block(chalk.red('Class Syntax Error'), ...lines)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const word = offenders.length === 1 ? 'class syntax error' : 'class syntax errors'
|
|
202
|
+
logger.block(
|
|
203
|
+
chalk.red(`Found ${offenders.length} ${word} — fix the ${offenders.length === 1 ? 'class' : 'classes'} above and re-run purgetss`)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
const error = new Error('Class syntax errors detected')
|
|
207
|
+
error.isClassSyntaxError = true
|
|
208
|
+
throw error
|
|
209
|
+
}
|
|
@@ -7,28 +7,21 @@ import path from 'path'
|
|
|
7
7
|
import { fileURLToPath } from 'url'
|
|
8
8
|
import { createRequire } from 'module'
|
|
9
9
|
import _ from 'lodash'
|
|
10
|
-
import chalk from 'chalk'
|
|
11
10
|
let saveGlossary = false
|
|
12
11
|
|
|
13
12
|
const __filename = fileURLToPath(import.meta.url)
|
|
14
13
|
const __dirname = path.dirname(__filename)
|
|
15
14
|
const require = createRequire(import.meta.url)
|
|
16
15
|
const cwd = process.cwd()
|
|
17
|
-
import { colores } from '
|
|
16
|
+
import { colores } from '../../shared/brand-colors.js'
|
|
18
17
|
export { colores }
|
|
19
|
-
const purgeLabel = colores.purgeLabel
|
|
20
18
|
|
|
21
|
-
import * as helpers from '
|
|
22
|
-
import { getConfigFile } from '
|
|
23
|
-
import { projectsConfigJS } from '
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
info: (...args) => console.log(purgeLabel, args.join(' ')),
|
|
28
|
-
warn: (...args) => console.log(purgeLabel, chalk.yellow(args.join(' '))),
|
|
29
|
-
error: (...args) => console.log(purgeLabel, chalk.red(args.join(' '))),
|
|
30
|
-
file: (...args) => console.log(purgeLabel, chalk.yellow(args.join(' ')), 'file created!')
|
|
31
|
-
}
|
|
19
|
+
import * as helpers from '../../shared/helpers.js'
|
|
20
|
+
import { getConfigFile } from '../../shared/config-manager.js'
|
|
21
|
+
import { projectsConfigJS } from '../../shared/constants.js'
|
|
22
|
+
import { logger } from '../../shared/logger.js'
|
|
23
|
+
import { registerSemanticName } from '../../shared/semantic-helpers.js'
|
|
24
|
+
const tiCompletionsFile = require('../../../lib/completions/titanium/completions-v3.json')
|
|
32
25
|
|
|
33
26
|
// Keys whose numeric values are interpreted with `ti.ui.defaultunit` from tiapp.xml.
|
|
34
27
|
// The glossary .md files for these keys receive an inline "// Unit: ..." note
|
|
@@ -174,7 +167,7 @@ function buildSubfolderIndex(folder) {
|
|
|
174
167
|
}
|
|
175
168
|
|
|
176
169
|
function glossaryBaseFolder() {
|
|
177
|
-
if (!fs.existsSync(projectsConfigJS)) return path.resolve(__dirname, '
|
|
170
|
+
if (!fs.existsSync(projectsConfigJS)) return path.resolve(__dirname, '../../../dist/glossary/')
|
|
178
171
|
if (saveGlossary) return cwd + '/purgetss/glossary/'
|
|
179
172
|
return ''
|
|
180
173
|
}
|
|
@@ -236,7 +229,7 @@ function autoBuildUtilitiesTSS(options = {}) {
|
|
|
236
229
|
|
|
237
230
|
saveGlossary = options.glossary ?? false
|
|
238
231
|
scaffoldGlossary()
|
|
239
|
-
let tailwindStyles = fs.readFileSync(path.resolve(__dirname, '
|
|
232
|
+
let tailwindStyles = fs.readFileSync(path.resolve(__dirname, '../../../lib/templates/tailwind/custom-template.tss'), 'utf8')
|
|
240
233
|
tailwindStyles += (fs.existsSync(projectsConfigJS)) ? `// config.js file updated on: ${getFileUpdatedDate(projectsConfigJS)}\n` : '// default config.js file\n'
|
|
241
234
|
|
|
242
235
|
const baseValues = combineDefaultThemeWithConfigFile()
|
|
@@ -255,7 +248,7 @@ function autoBuildUtilitiesTSS(options = {}) {
|
|
|
255
248
|
saveFile(cwd + '/purgetss/styles/utilities.tss', tailwindStyles)
|
|
256
249
|
logger.file('./purgetss/styles/utilities.tss')
|
|
257
250
|
} else {
|
|
258
|
-
saveFile(path.resolve(__dirname, '
|
|
251
|
+
saveFile(path.resolve(__dirname, '../../../dist/utilities.tss'), tailwindStyles)
|
|
259
252
|
logger.file('./dist/utilities.tss')
|
|
260
253
|
}
|
|
261
254
|
}
|
|
@@ -311,7 +304,7 @@ function processCompletionsClasses(_completionsWithBaseValues) {
|
|
|
311
304
|
|
|
312
305
|
function generateGlossary(_key, _theClasses, _keyName = null) {
|
|
313
306
|
let baseDestinationFolder = ''
|
|
314
|
-
if (!fs.existsSync(projectsConfigJS)) baseDestinationFolder = path.resolve(__dirname, '
|
|
307
|
+
if (!fs.existsSync(projectsConfigJS)) baseDestinationFolder = path.resolve(__dirname, '../../../dist/glossary/')
|
|
315
308
|
else if (saveGlossary) baseDestinationFolder = cwd + '/purgetss/glossary/'
|
|
316
309
|
|
|
317
310
|
if (baseDestinationFolder !== '') {
|
|
@@ -378,7 +371,7 @@ function getTiUIComponents(_base) {
|
|
|
378
371
|
|
|
379
372
|
function processCompoundClasses({ ..._base }) {
|
|
380
373
|
let compoundClasses = ''
|
|
381
|
-
const compoundTemplate = require('
|
|
374
|
+
const compoundTemplate = require('../../../lib/templates/tailwind/compoundTemplate.json')
|
|
382
375
|
|
|
383
376
|
_.each(compoundTemplate, (value, key) => {
|
|
384
377
|
compoundClasses += generateGlossary(key, helpers.processProperties(value.description, value.template, value.base ?? { default: _base[key] }))
|
|
@@ -534,6 +527,11 @@ function combineDefaultThemeWithConfigFile() {
|
|
|
534
527
|
}
|
|
535
528
|
|
|
536
529
|
_.merge(base.colors, themeOrDefaultValues.colors, configFile.theme.extend.colors)
|
|
530
|
+
// Track semantic color names so opacity modifiers (bg-X/65) can later
|
|
531
|
+
// auto-derive an alpha-applied entry in semantic.colors.json. A value is
|
|
532
|
+
// "semantic" when it's a string that isn't a hex literal or a Ti reserved
|
|
533
|
+
// keyword.
|
|
534
|
+
_.each(base.colors, value => collectSemanticReferences(value))
|
|
537
535
|
_.merge(base.size, themeOrDefaultValues.spacing, configFile.theme.extend.spacing)
|
|
538
536
|
_.merge(base.spacing, themeOrDefaultValues.spacing, configFile.theme.extend.spacing)
|
|
539
537
|
|
|
@@ -572,8 +570,16 @@ function combineDefaultThemeWithConfigFile() {
|
|
|
572
570
|
delete base.zIndex.auto
|
|
573
571
|
|
|
574
572
|
// ! Process custom Window, View and ImageView
|
|
575
|
-
//
|
|
573
|
+
// Track whether the user defined each Ti Element at the theme.X (replace) level
|
|
574
|
+
// BEFORE merging extend into theme. This mirrors the Tailwind convention:
|
|
575
|
+
// theme.X → REPLACE the framework's defaults entirely
|
|
576
|
+
// theme.extend.X → MERGE with the framework's defaults
|
|
577
|
+
// Without this distinction, presets (like Window's backgroundColor: '#FFFFFF')
|
|
578
|
+
// leak into a strict-replace config and surface as ghost properties in app.tss.
|
|
579
|
+
const userReplaced = {}
|
|
576
580
|
_.each(['Window', 'View', 'ImageView'], comp => {
|
|
581
|
+
userReplaced[comp] = !!configFile.theme[comp] && !configFile.theme.extend[comp]
|
|
582
|
+
|
|
577
583
|
if (configFile.theme.extend[comp]) {
|
|
578
584
|
configFile.theme[comp] = _.merge({}, configFile.theme[comp], configFile.theme.extend[comp])
|
|
579
585
|
delete configFile.theme.extend[comp]
|
|
@@ -584,11 +590,18 @@ function combineDefaultThemeWithConfigFile() {
|
|
|
584
590
|
}
|
|
585
591
|
})
|
|
586
592
|
|
|
587
|
-
//
|
|
588
|
-
//
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
593
|
+
// Apply framework defaults only when NOT in strict-replace mode. In replace
|
|
594
|
+
// mode the user's config is the single source of truth — presets must not be
|
|
595
|
+
// silently re-injected.
|
|
596
|
+
if (!userReplaced.Window) {
|
|
597
|
+
configFile.theme.Window = _.merge({ default: { backgroundColor: '#FFFFFF' } }, configFile.theme.Window)
|
|
598
|
+
}
|
|
599
|
+
if (!userReplaced.ImageView) {
|
|
600
|
+
configFile.theme.ImageView = _.merge({ ios: { hires: true } }, configFile.theme.ImageView)
|
|
601
|
+
}
|
|
602
|
+
if (!userReplaced.View) {
|
|
603
|
+
configFile.theme.View = _.merge({ default: { width: 'Ti.UI.SIZE', height: 'Ti.UI.SIZE' } }, configFile.theme.View)
|
|
604
|
+
}
|
|
592
605
|
|
|
593
606
|
base.Window = configFile.theme.Window
|
|
594
607
|
base.ImageView = configFile.theme.ImageView
|
|
@@ -610,6 +623,22 @@ function checkDeletePlugins() {
|
|
|
610
623
|
return Array.isArray(deletePlugins) ? deletePlugins : Object.keys(deletePlugins).map(key => key)
|
|
611
624
|
}
|
|
612
625
|
|
|
626
|
+
// Walk a color config value (possibly nested object of shades) and register
|
|
627
|
+
// any leaf string that points to a semantic color name in
|
|
628
|
+
// `semantic.colors.json` (e.g. `surface: 'surfaceColor'`,
|
|
629
|
+
// `brand: { DEFAULT: 'brandColor' }`).
|
|
630
|
+
const _semanticReservedValues = new Set(['transparent', 'currentColor', 'inherit'])
|
|
631
|
+
function collectSemanticReferences(value) {
|
|
632
|
+
if (typeof value === 'string') {
|
|
633
|
+
if (value.startsWith('#') || _semanticReservedValues.has(value)) return
|
|
634
|
+
registerSemanticName(value)
|
|
635
|
+
return
|
|
636
|
+
}
|
|
637
|
+
if (value && typeof value === 'object') {
|
|
638
|
+
_.each(value, v => collectSemanticReferences(v))
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
613
642
|
// ! Helper Functions
|
|
614
643
|
function removeDeprecatedColors(theObject) {
|
|
615
644
|
delete theObject.blueGray
|
|
@@ -830,8 +859,8 @@ function generateCombinedClasses(key, data) {
|
|
|
830
859
|
|
|
831
860
|
function saveAutoTSS(key, classes) {
|
|
832
861
|
if (fs.existsSync(projectsConfigJS) && saveGlossary) {
|
|
833
|
-
makeSureFolderExists(cwd + '/purgetss/
|
|
834
|
-
saveFile(cwd + `/purgetss/
|
|
862
|
+
makeSureFolderExists(cwd + '/purgetss/glossary/tailwind-classes/')
|
|
863
|
+
saveFile(cwd + `/purgetss/glossary/tailwind-classes/${key}.tss`, classes)
|
|
835
864
|
}
|
|
836
865
|
}
|
|
837
866
|
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
// Import functions from their new modular locations
|
|
10
10
|
import * as helpers from '../../shared/helpers.js'
|
|
11
|
-
import { autoBuildUtilitiesTSS } from '
|
|
11
|
+
import { autoBuildUtilitiesTSS } from './auto-utilities-builder.js'
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Build Tailwind (
|
|
14
|
+
* Build Tailwind (Auto-builds utilities.tss from config.cjs (active production path))
|
|
15
15
|
* @param {Object} options - Build options
|
|
16
16
|
*/
|
|
17
17
|
export function buildTailwind(options) {
|