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.
- package/README.md +21 -1
- package/bin/purgetss +13 -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 +1 -1
- package/src/cli/commands/images.js +41 -2
- package/src/cli/commands/purge.js +15 -2
- package/src/cli/utils/cli-helpers.js +15 -5
- package/src/cli/utils/unsupported-class-reporter.js +3 -3
- 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/src/core/builders/auto-utilities-builder.js +20 -15
- package/src/core/images/ensure-images-section.js +6 -4
- package/src/core/images/gen-scales.js +82 -17
- package/src/core/images/index.js +117 -12
- package/src/core/purger/icon-purger.js +7 -3
- package/src/core/purger/tailwind-purger.js +3 -1
- 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 +18 -0
- 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/utils.js +46 -8
- package/src/shared/logger.js +12 -0
- 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
|
-
|
|
650
|
-
convertedStyles += `'.${removeLastDash(`${rule}-${setModifier2(modifier, rule)}${setModifier2(_modifier)}`)}': ` + _.replace(_.replace(properties, /{transparentValue}/g, `${addTransparencyToValue(parseValue(
|
|
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
|
-
|
|
669
|
-
convertedStyles += `'.${removeLastDash(`${rule}-${setModifier2(modifier, rule)}${setModifier2(_modifier)}`)}': ` + _.replace(properties, /{value}/g, parseValue(
|
|
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
|
-
|
|
692
|
-
convertedStyles += `'.${removeLastDash(`${rule}-${setModifier2(modifier, rule)}${setModifier2(_modifier)}`)}': ` + _.replace(_.replace(properties, /{transparentValue}/g, `${addTransparencyToValue(parseValue(
|
|
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
|
-
|
|
711
|
-
convertedStyles += `'.${removeLastDash(`${rule}-${setModifier2(modifier, rule)}${setModifier2(_modifier)}`)}': ` + _.replace(properties, /{value}/g, parseValue(
|
|
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
|
-
|
|
34
|
-
if (debug) console.log('key:', key, '
|
|
35
|
-
let processedProperties = _.replace(declarationBlock, /{value}/g, parseValue(
|
|
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(
|
|
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 = ''
|
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,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
|
+
}
|