@nuasite/cms 0.37.0 → 0.38.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/dist/editor.js +6119 -6090
- package/package.json +1 -1
- package/src/editor/components/editable-highlights.tsx +18 -56
- package/src/editor/dom.ts +37 -0
- package/src/editor/editor.ts +42 -1
- package/src/source-finder/cross-file-tracker.ts +16 -6
- package/src/source-finder/element-finder.ts +135 -3
- package/src/source-finder/search-index.ts +319 -98
- package/src/source-finder/snippet-utils.ts +45 -42
- package/src/source-finder/source-lookup.ts +5 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'preact/hooks'
|
|
2
2
|
import { Z_INDEX } from '../constants'
|
|
3
|
-
import { getOutlineColor } from '../dom'
|
|
3
|
+
import { getOutlineColor, isElementVisible } from '../dom'
|
|
4
4
|
import * as signals from '../signals'
|
|
5
5
|
|
|
6
6
|
export interface EditableHighlightsProps {
|
|
@@ -164,72 +164,34 @@ function collectEditableElements(): HighlightRect[] {
|
|
|
164
164
|
const highlights: HighlightRect[] = []
|
|
165
165
|
const manifest = signals.manifest.value
|
|
166
166
|
|
|
167
|
-
|
|
168
|
-
const textElements = document.querySelectorAll('[data-cms-id]')
|
|
169
|
-
const componentElements = document.querySelectorAll('[data-cms-component-id]')
|
|
170
|
-
const imageElements = document.querySelectorAll('img[data-cms-img]')
|
|
171
|
-
const bgImageElements = document.querySelectorAll('[data-cms-bg-img]')
|
|
172
|
-
|
|
173
|
-
// Process text elements
|
|
174
|
-
textElements.forEach((el) => {
|
|
175
|
-
const cmsId = el.getAttribute('data-cms-id')
|
|
167
|
+
const tryPush = (el: Element, cmsId: string | null, type: HighlightRect['type']) => {
|
|
176
168
|
if (!cmsId) return
|
|
177
|
-
|
|
178
|
-
//
|
|
179
|
-
if (el.hasAttribute('data-cms-component-id') || el.tagName === 'IMG') return
|
|
180
|
-
|
|
181
|
-
// Skip if not in manifest (invalid element)
|
|
182
|
-
if (!manifest.entries[cmsId]) return
|
|
183
|
-
|
|
169
|
+
// Cheap rect gate first — skips most off-screen / collapsed elements without
|
|
170
|
+
// touching computed style. Only survivors pay the visibility check cost.
|
|
184
171
|
const rect = el.getBoundingClientRect()
|
|
185
|
-
// Skip elements not in viewport or too small
|
|
186
172
|
if (rect.width < 10 || rect.height < 10) return
|
|
187
173
|
if (rect.bottom < 0 || rect.top > window.innerHeight) return
|
|
188
174
|
if (rect.right < 0 || rect.left > window.innerWidth) return
|
|
175
|
+
if (!isElementVisible(el)) return
|
|
176
|
+
highlights.push({ cmsId, type, rect })
|
|
177
|
+
}
|
|
189
178
|
|
|
190
|
-
|
|
179
|
+
document.querySelectorAll('[data-cms-id]').forEach((el) => {
|
|
180
|
+
// Routed to the component/image branches below.
|
|
181
|
+
if (el.hasAttribute('data-cms-component-id') || el.tagName === 'IMG') return
|
|
182
|
+
const cmsId = el.getAttribute('data-cms-id')
|
|
183
|
+
if (cmsId && !manifest.entries[cmsId]) return
|
|
184
|
+
tryPush(el, cmsId, el.hasAttribute('data-cms-bg-img') ? 'image' : 'text')
|
|
191
185
|
})
|
|
192
186
|
|
|
193
|
-
|
|
194
|
-
componentElements.forEach((el) => {
|
|
187
|
+
document.querySelectorAll('[data-cms-component-id]').forEach((el) => {
|
|
195
188
|
const componentId = el.getAttribute('data-cms-component-id')
|
|
196
|
-
if (!componentId) return
|
|
197
|
-
|
|
198
|
-
// Skip if not in manifest
|
|
199
|
-
if (!manifest.components[componentId]) return
|
|
200
|
-
|
|
201
|
-
const rect = el.getBoundingClientRect()
|
|
202
|
-
if (rect.width < 10 || rect.height < 10) return
|
|
203
|
-
if (rect.bottom < 0 || rect.top > window.innerHeight) return
|
|
204
|
-
if (rect.right < 0 || rect.left > window.innerWidth) return
|
|
205
|
-
|
|
206
|
-
highlights.push({ cmsId: componentId, type: 'component', rect })
|
|
189
|
+
if (componentId && !manifest.components[componentId]) return
|
|
190
|
+
tryPush(el, componentId, 'component')
|
|
207
191
|
})
|
|
208
192
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const cmsId = el.getAttribute('data-cms-img')
|
|
212
|
-
if (!cmsId) return
|
|
213
|
-
|
|
214
|
-
const rect = el.getBoundingClientRect()
|
|
215
|
-
if (rect.width < 10 || rect.height < 10) return
|
|
216
|
-
if (rect.bottom < 0 || rect.top > window.innerHeight) return
|
|
217
|
-
if (rect.right < 0 || rect.left > window.innerWidth) return
|
|
218
|
-
|
|
219
|
-
highlights.push({ cmsId, type: 'image', rect })
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
// Process background image elements
|
|
223
|
-
bgImageElements.forEach((el) => {
|
|
224
|
-
const cmsId = el.getAttribute('data-cms-id')
|
|
225
|
-
if (!cmsId) return
|
|
226
|
-
|
|
227
|
-
const rect = el.getBoundingClientRect()
|
|
228
|
-
if (rect.width < 10 || rect.height < 10) return
|
|
229
|
-
if (rect.bottom < 0 || rect.top > window.innerHeight) return
|
|
230
|
-
if (rect.right < 0 || rect.left > window.innerWidth) return
|
|
231
|
-
|
|
232
|
-
highlights.push({ cmsId, type: 'image', rect })
|
|
193
|
+
document.querySelectorAll('img[data-cms-img]').forEach((el) => {
|
|
194
|
+
tryPush(el, el.getAttribute('data-cms-img'), 'image')
|
|
233
195
|
})
|
|
234
196
|
|
|
235
197
|
return highlights
|
package/src/editor/dom.ts
CHANGED
|
@@ -51,6 +51,43 @@ export function getOutlineColor(): string {
|
|
|
51
51
|
return isPageDark() ? '#FFFFFF' : '#1A1A1A'
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Check if an element is actually visible to the user. Catches `display:none`,
|
|
56
|
+
* `visibility:hidden|collapse`, `opacity:0`, and `content-visibility` on the
|
|
57
|
+
* element or any ancestor — `getBoundingClientRect` alone reports a non-zero
|
|
58
|
+
* box for `opacity:0`/`visibility:hidden`, so highlight overlays would otherwise
|
|
59
|
+
* draw over hidden mobile menus and modal panels.
|
|
60
|
+
*/
|
|
61
|
+
export function isElementVisible(el: Element): boolean {
|
|
62
|
+
const anyEl = el as Element & {
|
|
63
|
+
checkVisibility?: (opts?: {
|
|
64
|
+
checkOpacity?: boolean
|
|
65
|
+
checkVisibilityCSS?: boolean
|
|
66
|
+
contentVisibilityAuto?: boolean
|
|
67
|
+
opacityProperty?: boolean
|
|
68
|
+
visibilityProperty?: boolean
|
|
69
|
+
}) => boolean
|
|
70
|
+
}
|
|
71
|
+
if (typeof anyEl.checkVisibility === 'function') {
|
|
72
|
+
return anyEl.checkVisibility({
|
|
73
|
+
checkOpacity: true,
|
|
74
|
+
checkVisibilityCSS: true,
|
|
75
|
+
contentVisibilityAuto: true,
|
|
76
|
+
opacityProperty: true,
|
|
77
|
+
visibilityProperty: true,
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
let cur: Element | null = el
|
|
81
|
+
while (cur && cur !== document.documentElement) {
|
|
82
|
+
const cs = getComputedStyle(cur)
|
|
83
|
+
if (cs.display === 'none') return false
|
|
84
|
+
if (cs.visibility === 'hidden' || cs.visibility === 'collapse') return false
|
|
85
|
+
if (parseFloat(cs.opacity) === 0) return false
|
|
86
|
+
cur = cur.parentElement
|
|
87
|
+
}
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
|
|
54
91
|
/** Style element for contenteditable focus styles injected into the host page */
|
|
55
92
|
let focusStyleElement: HTMLStyleElement | null = null
|
|
56
93
|
|
package/src/editor/editor.ts
CHANGED
|
@@ -100,6 +100,47 @@ export function notifyLockedElement(): void {
|
|
|
100
100
|
signals.showToast("This text can't be edited here — no source file is linked to it", 'info')
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Manifest is built in two phases (fast HTML marking, then background source
|
|
105
|
+
* resolution). Entering edit mode before phase 2 completes can pre-lock elements
|
|
106
|
+
* that later gain a valid sourcePath, leaving stale locks on the DOM.
|
|
107
|
+
*
|
|
108
|
+
* On click, re-check the local snapshot, then re-fetch from the server in case
|
|
109
|
+
* phase 2 finished since the editor took its snapshot. Toggle edit mode once
|
|
110
|
+
* afterwards to make the now-unlocked element editable.
|
|
111
|
+
*/
|
|
112
|
+
let inFlightLockedFetch: Promise<unknown> | null = null
|
|
113
|
+
function handleLockedClick(event: Event): void {
|
|
114
|
+
const target = event.currentTarget as HTMLElement | null
|
|
115
|
+
const id = target?.getAttribute(CSS.ID_ATTRIBUTE)
|
|
116
|
+
if (!target || !id) {
|
|
117
|
+
notifyLockedElement()
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (signals.manifest.value.entries[id]?.sourcePath) {
|
|
122
|
+
target.removeAttribute(CSS.LOCKED_ATTRIBUTE)
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// In-memory signal can lag phase-2 writes. Coalesce concurrent clicks into
|
|
127
|
+
// one fetch so the dev server doesn't get hammered.
|
|
128
|
+
if (!inFlightLockedFetch) {
|
|
129
|
+
inFlightLockedFetch = fetchManifest()
|
|
130
|
+
.then((fresh) => signals.setManifest(fresh))
|
|
131
|
+
.catch(() => {})
|
|
132
|
+
.finally(() => {
|
|
133
|
+
inFlightLockedFetch = null
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
inFlightLockedFetch.then(() => {
|
|
137
|
+
if (signals.manifest.value.entries[id]?.sourcePath) {
|
|
138
|
+
target.removeAttribute(CSS.LOCKED_ATTRIBUTE)
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
notifyLockedElement()
|
|
142
|
+
}
|
|
143
|
+
|
|
103
144
|
/** Test-only: reset toast throttle state between test cases. */
|
|
104
145
|
export function _resetToastThrottles(): void {
|
|
105
146
|
lastFormattingBlockedToastAt = 0
|
|
@@ -246,7 +287,7 @@ export async function startEditMode(
|
|
|
246
287
|
logDebug(config.debug, 'Skipping element without source path:', cmsId)
|
|
247
288
|
makeElementNonEditable(el)
|
|
248
289
|
el.setAttribute(CSS.LOCKED_ATTRIBUTE, 'true')
|
|
249
|
-
el.addEventListener('click',
|
|
290
|
+
el.addEventListener('click', handleLockedClick, { signal: editModeSignal })
|
|
250
291
|
return
|
|
251
292
|
}
|
|
252
293
|
|
|
@@ -6,10 +6,19 @@ import { escapeRegex, resolveSourcePath } from '../utils'
|
|
|
6
6
|
import { buildDefinitionPath, parseExpressionPath } from './ast-extractors'
|
|
7
7
|
import { getCachedParsedFile } from './ast-parser'
|
|
8
8
|
import { findComponentProp, findExpressionProp, findSpreadProp } from './element-finder'
|
|
9
|
+
import { wildcardPathToRegexBody } from './search-index'
|
|
9
10
|
import { normalizeText } from './snippet-utils'
|
|
10
11
|
import type { ImportInfo, SourceLocation, VariableDefinition } from './types'
|
|
11
12
|
import { getExportedDefinitions, resolveImportPath } from './variable-extraction'
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Compile a wildcard path like `navItems[*].label` into a regex matching
|
|
16
|
+
* concrete definition paths (`navItems[0].label`, `navItems[3].label`, …).
|
|
17
|
+
*/
|
|
18
|
+
function buildWildcardPathRegex(wildcardPath: string): RegExp {
|
|
19
|
+
return new RegExp('^' + wildcardPathToRegexBody(wildcardPath) + '$')
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
// ============================================================================
|
|
14
23
|
// Expression Prop Search
|
|
15
24
|
// ============================================================================
|
|
@@ -104,16 +113,17 @@ async function searchDirForExpressionProp(
|
|
|
104
113
|
if (exprPropMatch) {
|
|
105
114
|
// The expression text might be a simple variable like 'navItems'
|
|
106
115
|
const exprText = exprPropMatch.expressionText
|
|
107
|
-
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
116
|
+
// Substitute the parent's local name for the child's:
|
|
117
|
+
// child `items[0]` + parent `navItems` → `navItems[0]`
|
|
118
|
+
// May contain `[*]` wildcards when the child resolved a `.map()` callback
|
|
119
|
+
// param — compare via regex to match concrete indices in the parent.
|
|
111
120
|
const parentPath = expressionPath.replace(/^[^.[]+/, exprText)
|
|
121
|
+
const parentPathRegex = parentPath.includes('[*]') ? buildWildcardPathRegex(parentPath) : null
|
|
112
122
|
|
|
113
|
-
// Check if the value is in local variable definitions
|
|
114
123
|
for (const def of cached.variableDefinitions) {
|
|
115
124
|
const defPath = buildDefinitionPath(def)
|
|
116
|
-
|
|
125
|
+
const matches = parentPathRegex ? parentPathRegex.test(defPath) : defPath === parentPath
|
|
126
|
+
if (matches) {
|
|
117
127
|
const normalizedDef = normalizeText(def.value)
|
|
118
128
|
if (normalizedDef === normalizedSearch) {
|
|
119
129
|
return {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ComponentNode, ElementNode, Node as AstroNode, TextNode } from '@astrojs/compiler/types'
|
|
2
2
|
|
|
3
3
|
import { buildDefinitionPath, parseExpressionPath } from './ast-extractors'
|
|
4
|
+
import { makeLeafPathRegex, resolveMapChain } from './search-index'
|
|
4
5
|
import { normalizeText } from './snippet-utils'
|
|
5
6
|
import type {
|
|
6
7
|
ComponentPropMatch,
|
|
@@ -100,7 +101,9 @@ export function findElementWithText(
|
|
|
100
101
|
return match?.[1] ?? exprPath
|
|
101
102
|
}
|
|
102
103
|
|
|
103
|
-
function visit(node: AstroNode) {
|
|
104
|
+
function visit(node: AstroNode, parentExpression: AstroNode | null) {
|
|
105
|
+
const currentExpr = node.type === 'expression' ? node : parentExpression
|
|
106
|
+
|
|
104
107
|
// Check if this is an element or component matching our tag
|
|
105
108
|
if ((node.type === 'element' || node.type === 'component') && node.name.toLowerCase() === tagLower) {
|
|
106
109
|
const elemNode = node as ElementNode | ComponentNode
|
|
@@ -166,6 +169,43 @@ export function findElementWithText(
|
|
|
166
169
|
importInfo,
|
|
167
170
|
expressionPath: exprPath,
|
|
168
171
|
})
|
|
172
|
+
} else if (currentExpr) {
|
|
173
|
+
// `baseVar` may be a `.map()` callback parameter or destructured
|
|
174
|
+
// property. Resolve it to the source array; if the array is local
|
|
175
|
+
// take the leaf, otherwise surface as a cross-file prop candidate.
|
|
176
|
+
const mapMatch = resolveMapForLocal(
|
|
177
|
+
exprPath,
|
|
178
|
+
currentExpr,
|
|
179
|
+
variableDefinitions,
|
|
180
|
+
normalizedSearch,
|
|
181
|
+
)
|
|
182
|
+
if (mapMatch && bestScore < 100) {
|
|
183
|
+
bestScore = 100
|
|
184
|
+
bestMatch = {
|
|
185
|
+
line,
|
|
186
|
+
type: 'variable',
|
|
187
|
+
variableName: mapMatch.variableName,
|
|
188
|
+
definitionLine: mapMatch.definitionLine,
|
|
189
|
+
}
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!mapMatch) {
|
|
194
|
+
const mapPropCandidate = resolveMapForProp(
|
|
195
|
+
exprPath,
|
|
196
|
+
currentExpr,
|
|
197
|
+
propAliases,
|
|
198
|
+
)
|
|
199
|
+
if (mapPropCandidate) {
|
|
200
|
+
propCandidates.push({
|
|
201
|
+
line,
|
|
202
|
+
type: 'variable',
|
|
203
|
+
usesProp: true,
|
|
204
|
+
propName: mapPropCandidate.propName,
|
|
205
|
+
expressionPath: mapPropCandidate.expressionPath,
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
}
|
|
169
209
|
}
|
|
170
210
|
}
|
|
171
211
|
}
|
|
@@ -224,7 +264,7 @@ export function findElementWithText(
|
|
|
224
264
|
// Recursively visit children
|
|
225
265
|
if ('children' in node && Array.isArray(node.children)) {
|
|
226
266
|
for (const child of node.children) {
|
|
227
|
-
visit(child)
|
|
267
|
+
visit(child, currentExpr)
|
|
228
268
|
}
|
|
229
269
|
}
|
|
230
270
|
}
|
|
@@ -245,10 +285,102 @@ export function findElementWithText(
|
|
|
245
285
|
return null
|
|
246
286
|
}
|
|
247
287
|
|
|
248
|
-
visit(ast)
|
|
288
|
+
visit(ast, null)
|
|
249
289
|
return { bestMatch, propCandidates, importCandidates }
|
|
250
290
|
}
|
|
251
291
|
|
|
292
|
+
/**
|
|
293
|
+
* Collect the joined text of every direct text child of an expression node.
|
|
294
|
+
* Used to feed `resolveMapChain` with the surrounding `.map(...)` source.
|
|
295
|
+
*/
|
|
296
|
+
function collectExpressionText(parentExpression: AstroNode): string[] {
|
|
297
|
+
const exprTexts: string[] = []
|
|
298
|
+
if ('children' in parentExpression && Array.isArray(parentExpression.children)) {
|
|
299
|
+
for (const child of parentExpression.children) {
|
|
300
|
+
if (child.type === 'text' && (child as TextNode).value) {
|
|
301
|
+
exprTexts.push((child as TextNode).value)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return exprTexts
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Resolve a `.map()` loop variable reference (`item.label` or destructured `label`)
|
|
310
|
+
* against local variable definitions, returning the concrete element whose value
|
|
311
|
+
* matches `normalizedSearch`. Returns null when the chain doesn't resolve or no
|
|
312
|
+
* leaf value matches.
|
|
313
|
+
*/
|
|
314
|
+
function resolveMapForLocal(
|
|
315
|
+
exprPath: string,
|
|
316
|
+
parentExpression: AstroNode,
|
|
317
|
+
variableDefinitions: VariableDefinition[],
|
|
318
|
+
normalizedSearch: string,
|
|
319
|
+
): { variableName: string; definitionLine: number } | null {
|
|
320
|
+
const baseMatch = exprPath.match(/^(\w+)(.*)$/)
|
|
321
|
+
if (!baseMatch) return null
|
|
322
|
+
const baseVar = baseMatch[1]!
|
|
323
|
+
const suffix = baseMatch[2] ?? ''
|
|
324
|
+
|
|
325
|
+
const exprTexts = collectExpressionText(parentExpression)
|
|
326
|
+
if (exprTexts.length === 0) return null
|
|
327
|
+
|
|
328
|
+
const resolved = resolveMapChain(exprTexts, baseVar)
|
|
329
|
+
if (!resolved) return null
|
|
330
|
+
|
|
331
|
+
const leafRegex = makeLeafPathRegex({
|
|
332
|
+
arrayPath: resolved.arrayPath,
|
|
333
|
+
leafSuffix: resolved.leafSuffix + suffix,
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
for (const def of variableDefinitions) {
|
|
337
|
+
const defPath = buildDefinitionPath(def)
|
|
338
|
+
if (!leafRegex.test(defPath)) continue
|
|
339
|
+
if (normalizeText(def.value) === normalizedSearch) {
|
|
340
|
+
return { variableName: defPath, definitionLine: def.line }
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return null
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Resolve a `.map()` loop variable reference against prop aliases, returning a
|
|
348
|
+
* cross-file candidate when the source array comes from props. The expression
|
|
349
|
+
* path encodes the wildcard chain (e.g. `items[*].label`) so cross-file lookups
|
|
350
|
+
* can match concrete parent definitions.
|
|
351
|
+
*/
|
|
352
|
+
function resolveMapForProp(
|
|
353
|
+
exprPath: string,
|
|
354
|
+
parentExpression: AstroNode,
|
|
355
|
+
propAliases: Map<string, string>,
|
|
356
|
+
): { propName: string; expressionPath: string } | null {
|
|
357
|
+
const baseMatch = exprPath.match(/^(\w+)(.*)$/)
|
|
358
|
+
if (!baseMatch) return null
|
|
359
|
+
const baseVar = baseMatch[1]!
|
|
360
|
+
const suffix = baseMatch[2] ?? ''
|
|
361
|
+
|
|
362
|
+
const exprTexts = collectExpressionText(parentExpression)
|
|
363
|
+
if (exprTexts.length === 0) return null
|
|
364
|
+
|
|
365
|
+
const resolved = resolveMapChain(exprTexts, baseVar)
|
|
366
|
+
if (!resolved) return null
|
|
367
|
+
|
|
368
|
+
// The head of the resolved arrayPath is the local name of the array; if that
|
|
369
|
+
// local name is a prop alias, we have a cross-file candidate.
|
|
370
|
+
const headMatch = resolved.arrayPath.match(/^(\w+)/)
|
|
371
|
+
if (!headMatch) return null
|
|
372
|
+
const arrayHead = headMatch[1]!
|
|
373
|
+
const actualPropName = propAliases.get(arrayHead)
|
|
374
|
+
if (!actualPropName) return null
|
|
375
|
+
|
|
376
|
+
// Build the expression path the parent should match: e.g. `items[*].label`.
|
|
377
|
+
// Cross-file lookup combines this with the parent's local binding.
|
|
378
|
+
return {
|
|
379
|
+
propName: actualPropName,
|
|
380
|
+
expressionPath: resolved.arrayPath + '[*]' + resolved.leafSuffix + suffix,
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
252
384
|
// ============================================================================
|
|
253
385
|
// Component Prop Finding
|
|
254
386
|
// ============================================================================
|