@nuasite/cms 0.37.0 → 0.39.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.
@@ -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
- // Build the corresponding path in the parent's variable definitions
109
- // e.g., if expressionPath is 'items[0]' and exprText is 'navItems',
110
- // we look for 'navItems[0]' in the parent's definitions
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
- if (defPath === parentPath) {
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
  // ============================================================================