@nuasite/cms-marker 0.0.71 → 0.0.72

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.
@@ -12,6 +12,16 @@ import { generateSourceHash } from './utils'
12
12
  // File Parsing Cache - Avoid re-parsing the same files
13
13
  // ============================================================================
14
14
 
15
+ /** Import information from frontmatter */
16
+ interface ImportInfo {
17
+ /** Local name of the imported binding */
18
+ localName: string
19
+ /** Original exported name (or 'default' for default imports) */
20
+ importedName: string
21
+ /** The import source path (e.g., './config', '../data/nav') */
22
+ source: string
23
+ }
24
+
15
25
  interface CachedParsedFile {
16
26
  content: string
17
27
  lines: string[]
@@ -19,6 +29,11 @@ interface CachedParsedFile {
19
29
  frontmatterContent: string | null
20
30
  frontmatterStartLine: number
21
31
  variableDefinitions: VariableDefinition[]
32
+ /** Mapping of local variable names to prop names from Astro.props destructuring
33
+ * e.g., { navItems: 'items' } for `const { items: navItems } = Astro.props` */
34
+ propAliases: Map<string, string>
35
+ /** Import information from frontmatter */
36
+ imports: ImportInfo[]
22
37
  }
23
38
 
24
39
  /** Cache for parsed Astro files - cleared between builds */
@@ -162,6 +177,8 @@ async function getCachedParsedFile(filePath: string): Promise<CachedParsedFile |
162
177
  frontmatterContent: null,
163
178
  frontmatterStartLine: 0,
164
179
  variableDefinitions: [],
180
+ propAliases: new Map(),
181
+ imports: [],
165
182
  }
166
183
  parsedFileCache.set(filePath, entry)
167
184
  return entry
@@ -170,10 +187,14 @@ async function getCachedParsedFile(filePath: string): Promise<CachedParsedFile |
170
187
  const { ast, frontmatterContent, frontmatterStartLine } = await parseAstroFile(content)
171
188
 
172
189
  let variableDefinitions: VariableDefinition[] = []
190
+ let propAliases = new Map<string, string>()
191
+ let imports: ImportInfo[] = []
173
192
  if (frontmatterContent) {
174
193
  const frontmatterAst = parseFrontmatter(frontmatterContent, filePath)
175
194
  if (frontmatterAst) {
176
195
  variableDefinitions = extractVariableDefinitions(frontmatterAst, frontmatterStartLine)
196
+ propAliases = extractPropAliases(frontmatterAst)
197
+ imports = extractImports(frontmatterAst)
177
198
  }
178
199
  }
179
200
 
@@ -184,6 +205,8 @@ async function getCachedParsedFile(filePath: string): Promise<CachedParsedFile |
184
205
  frontmatterContent,
185
206
  frontmatterStartLine,
186
207
  variableDefinitions,
208
+ propAliases,
209
+ imports,
187
210
  }
188
211
 
189
212
  parsedFileCache.set(filePath, entry)
@@ -210,9 +233,15 @@ function indexFileContent(cached: CachedParsedFile, relFile: string): void {
210
233
  // Check for variable references
211
234
  const exprInfo = hasExpressionChild(elemNode)
212
235
  if (exprInfo.found && exprInfo.varNames.length > 0) {
213
- for (const varName of exprInfo.varNames) {
236
+ for (const exprPath of exprInfo.varNames) {
214
237
  for (const def of cached.variableDefinitions) {
215
- if (def.name === varName || (def.parentName && def.name === varName)) {
238
+ // Build the full definition path for comparison
239
+ // For array indices (numeric names), use bracket notation
240
+ const defPath = buildDefinitionPath(def)
241
+ // Check if the expression path matches the definition path
242
+ // e.g., 'config.nav.title' matches def with parentName='config.nav', name='title'
243
+ // or 'items[0]' matches def with parentName='items', name='0'
244
+ if (defPath === exprPath) {
216
245
  const normalizedDef = normalizeText(def.value)
217
246
  const completeSnippet = extractCompleteTagSnippet(cached.lines, line - 1, tag)
218
247
  const snippet = extractInnerHtmlFromSnippet(completeSnippet, tag) ?? completeSnippet
@@ -222,7 +251,7 @@ function indexFileContent(cached: CachedParsedFile, relFile: string): void {
222
251
  line: def.line,
223
252
  snippet: cached.lines[def.line - 1] || '',
224
253
  type: 'variable',
225
- variableName: def.parentName ? `${def.parentName}.${def.name}` : def.name,
254
+ variableName: defPath,
226
255
  definitionLine: def.line,
227
256
  normalizedText: normalizedDef,
228
257
  tag,
@@ -416,14 +445,45 @@ function getTextContent(node: AstroNode): string {
416
445
  return ''
417
446
  }
418
447
 
448
+ /**
449
+ * Parse an expression path and extract the full path for variable lookup.
450
+ * Handles patterns like: varName, obj.prop, items[0], config.nav.title, links[0].text
451
+ * @returns The full expression path or null if not a simple variable reference
452
+ */
453
+ function parseExpressionPath(exprText: string): string | null {
454
+ // Match patterns like: varName, obj.prop, items[0], config.nav.title, links[0].text
455
+ // Pattern breakdown: word characters, dots, and bracket notation with numbers
456
+ const match = exprText.match(/^\s*([\w]+(?:\.[\w]+|\[\d+\])*(?:\.[\w]+)?)\s*$/)
457
+ if (match) {
458
+ return match[1]!
459
+ }
460
+ return null
461
+ }
462
+
463
+ /**
464
+ * Build the full path for a variable definition.
465
+ * For array indices (numeric names), uses bracket notation: items[0]
466
+ * For object properties, uses dot notation: config.nav.title
467
+ */
468
+ function buildDefinitionPath(def: VariableDefinition): string {
469
+ if (!def.parentName) {
470
+ return def.name
471
+ }
472
+ // Check if the name is a numeric index (for arrays)
473
+ if (/^\d+$/.test(def.name)) {
474
+ return `${def.parentName}[${def.name}]`
475
+ }
476
+ return `${def.parentName}.${def.name}`
477
+ }
478
+
419
479
  // Helper for indexing - check for expression children
420
480
  function hasExpressionChild(node: AstroNode): { found: boolean; varNames: string[] } {
421
481
  const varNames: string[] = []
422
482
  if (node.type === 'expression') {
423
483
  const exprText = getTextContent(node)
424
- const match = exprText.match(/^\s*(\w+)(?:\.(\w+))?\s*$/)
425
- if (match) {
426
- varNames.push(match[2] ?? match[1]!)
484
+ const fullPath = parseExpressionPath(exprText)
485
+ if (fullPath) {
486
+ varNames.push(fullPath)
427
487
  }
428
488
  return { found: true, varNames }
429
489
  }
@@ -587,6 +647,105 @@ function extractVariableDefinitions(ast: BabelFile, frontmatterStartLine: number
587
647
  return (babelLine - 1) + frontmatterStartLine
588
648
  }
589
649
 
650
+ /**
651
+ * Recursively extract properties from an object expression
652
+ * @param objNode - The ObjectExpression node
653
+ * @param parentPath - The full path to this object (e.g., 'config' or 'config.nav')
654
+ */
655
+ function extractObjectProperties(objNode: BabelNode, parentPath: string): void {
656
+ const properties = objNode.properties as BabelNode[] | undefined
657
+ for (const prop of properties ?? []) {
658
+ if (prop.type !== 'ObjectProperty') continue
659
+ const key = prop.key as BabelNode | undefined
660
+ const value = prop.value as BabelNode | undefined
661
+ if (!key || key.type !== 'Identifier' || !value) continue
662
+
663
+ const propName = key.name as string
664
+ const fullPath = `${parentPath}.${propName}`
665
+ const propLoc = prop.loc as { start: { line: number } } | undefined
666
+ const propLine = babelLineToFileLine(propLoc?.start.line ?? 1)
667
+
668
+ const stringValue = getStringValue(value)
669
+ if (stringValue !== null) {
670
+ definitions.push({
671
+ name: propName,
672
+ value: stringValue,
673
+ line: propLine,
674
+ parentName: parentPath,
675
+ })
676
+ }
677
+
678
+ // Recurse for nested objects
679
+ if (value.type === 'ObjectExpression') {
680
+ extractObjectProperties(value, fullPath)
681
+ }
682
+
683
+ // Handle arrays within objects
684
+ if (value.type === 'ArrayExpression') {
685
+ extractArrayElements(value, fullPath, propLine)
686
+ }
687
+ }
688
+ }
689
+
690
+ /**
691
+ * Extract elements from an array expression
692
+ * @param arrNode - The ArrayExpression node
693
+ * @param parentPath - The full path to this array (e.g., 'items' or 'config.items')
694
+ * @param defaultLine - Fallback line if element has no location
695
+ */
696
+ function extractArrayElements(arrNode: BabelNode, parentPath: string, defaultLine: number): void {
697
+ const elements = arrNode.elements as BabelNode[] | undefined
698
+ for (let i = 0; i < (elements?.length ?? 0); i++) {
699
+ const elem = elements![i]
700
+ if (!elem) continue
701
+
702
+ const elemLoc = elem.loc as { start: { line: number } } | undefined
703
+ const elemLine = babelLineToFileLine(elemLoc?.start.line ?? defaultLine)
704
+ const indexPath = `${parentPath}[${i}]`
705
+
706
+ // Handle string values in array
707
+ const elemValue = getStringValue(elem)
708
+ if (elemValue !== null) {
709
+ definitions.push({
710
+ name: String(i),
711
+ value: elemValue,
712
+ line: elemLine,
713
+ parentName: parentPath,
714
+ })
715
+ }
716
+
717
+ // Handle array of objects: [{ text: 'Home' }]
718
+ if (elem.type === 'ObjectExpression') {
719
+ const objProperties = elem.properties as BabelNode[] | undefined
720
+ for (const prop of objProperties ?? []) {
721
+ if (prop.type !== 'ObjectProperty') continue
722
+ const key = prop.key as BabelNode | undefined
723
+ const value = prop.value as BabelNode | undefined
724
+ if (!key || key.type !== 'Identifier' || !value) continue
725
+
726
+ const propName = key.name as string
727
+ const propLoc = prop.loc as { start: { line: number } } | undefined
728
+ const propLine = babelLineToFileLine(propLoc?.start.line ?? elemLine)
729
+
730
+ const stringValue = getStringValue(value)
731
+ if (stringValue !== null) {
732
+ definitions.push({
733
+ name: propName,
734
+ value: stringValue,
735
+ line: propLine,
736
+ parentName: indexPath,
737
+ })
738
+ }
739
+
740
+ // Recurse for nested objects within array elements
741
+ if (value.type === 'ObjectExpression') {
742
+ extractObjectProperties(value, `${indexPath}.${propName}`)
743
+ }
744
+ }
745
+ }
746
+ }
747
+ }
748
+
590
749
  function visitNode(node: BabelNode) {
591
750
  if (node.type === 'VariableDeclaration') {
592
751
  const declarations = node.declarations as BabelNode[] | undefined
@@ -604,23 +763,100 @@ function extractVariableDefinitions(ast: BabelFile, frontmatterStartLine: number
604
763
  definitions.push({ name: varName, value: stringValue, line })
605
764
  }
606
765
 
607
- // Object expression - extract properties
766
+ // Object expression - extract properties recursively
608
767
  if (init.type === 'ObjectExpression') {
609
- const properties = init.properties as BabelNode[] | undefined
768
+ extractObjectProperties(init, varName)
769
+ }
770
+
771
+ // Array expression - extract elements
772
+ if (init.type === 'ArrayExpression') {
773
+ extractArrayElements(init, varName, line)
774
+ }
775
+ }
776
+ }
777
+ }
778
+
779
+ // Recursively visit child nodes
780
+ for (const key of Object.keys(node)) {
781
+ const value = node[key]
782
+ if (value && typeof value === 'object') {
783
+ if (Array.isArray(value)) {
784
+ for (const item of value) {
785
+ if (item && typeof item === 'object' && 'type' in item) {
786
+ visitNode(item as BabelNode)
787
+ }
788
+ }
789
+ } else if ('type' in value) {
790
+ visitNode(value as BabelNode)
791
+ }
792
+ }
793
+ }
794
+ }
795
+
796
+ visitNode(ast.program)
797
+ return definitions
798
+ }
799
+
800
+ /**
801
+ * Extract prop aliases from Astro.props destructuring patterns.
802
+ * Returns a Map of local variable name -> prop name.
803
+ * Examples:
804
+ * const { title } = Astro.props -> Map { 'title' => 'title' }
805
+ * const { items: navItems } = Astro.props -> Map { 'navItems' => 'items' }
806
+ */
807
+ function extractPropAliases(ast: BabelFile): Map<string, string> {
808
+ const propAliases = new Map<string, string>()
809
+
810
+ function visitNode(node: BabelNode) {
811
+ if (node.type === 'VariableDeclaration') {
812
+ const declarations = node.declarations as BabelNode[] | undefined
813
+ for (const decl of declarations ?? []) {
814
+ const id = decl.id as BabelNode | undefined
815
+ const init = decl.init as BabelNode | undefined
816
+
817
+ // Check for destructuring from Astro.props
818
+ // Pattern: const { x, y } = Astro.props;
819
+ if (id?.type === 'ObjectPattern' && init?.type === 'MemberExpression') {
820
+ const object = init.object as BabelNode | undefined
821
+ const property = init.property as BabelNode | undefined
822
+
823
+ if (
824
+ object?.type === 'Identifier'
825
+ && (object.name as string) === 'Astro'
826
+ && property?.type === 'Identifier'
827
+ && (property.name as string) === 'props'
828
+ ) {
829
+ // Extract property names from the destructuring pattern
830
+ const properties = id.properties as BabelNode[] | undefined
610
831
  for (const prop of properties ?? []) {
611
- const key = prop.key as BabelNode | undefined
612
- const value = prop.value as BabelNode | undefined
613
- if (prop.type === 'ObjectProperty' && key?.type === 'Identifier' && value) {
614
- const propValue = getStringValue(value)
615
- if (propValue !== null) {
616
- const propLoc = prop.loc as { start: { line: number } } | undefined
617
- const propLine = babelLineToFileLine(propLoc?.start.line ?? 1)
618
- definitions.push({
619
- name: key.name as string,
620
- value: propValue,
621
- line: propLine,
622
- parentName: varName,
623
- })
832
+ if (prop.type === 'ObjectProperty') {
833
+ const key = prop.key as BabelNode | undefined
834
+ const value = prop.value as BabelNode | undefined
835
+
836
+ if (key?.type === 'Identifier') {
837
+ const propName = key.name as string
838
+ // Check for renaming: { items: navItems }
839
+ // key is the prop name (items), value is the local name (navItems)
840
+ if (value?.type === 'Identifier') {
841
+ const localName = value.name as string
842
+ propAliases.set(localName, propName)
843
+ } else if (value?.type === 'AssignmentPattern') {
844
+ // Handle default values: { items: navItems = [] } or { items = [] }
845
+ const left = value.left as BabelNode | undefined
846
+ if (left?.type === 'Identifier') {
847
+ propAliases.set(left.name as string, propName)
848
+ }
849
+ } else {
850
+ // Simple case: { items } - key and value are the same
851
+ propAliases.set(propName, propName)
852
+ }
853
+ }
854
+ } else if (prop.type === 'RestElement') {
855
+ // Handle rest pattern: const { x, ...rest } = Astro.props;
856
+ const argument = prop.argument as BabelNode | undefined
857
+ if (argument?.type === 'Identifier') {
858
+ // Rest element captures all remaining props
859
+ propAliases.set(argument.name as string, '...')
624
860
  }
625
861
  }
626
862
  }
@@ -647,7 +883,281 @@ function extractVariableDefinitions(ast: BabelFile, frontmatterStartLine: number
647
883
  }
648
884
 
649
885
  visitNode(ast.program)
650
- return definitions
886
+ return propAliases
887
+ }
888
+
889
+ /**
890
+ * Extract import information from Babel AST.
891
+ * Handles:
892
+ * import { foo } from './file' -> { localName: 'foo', importedName: 'foo', source: './file' }
893
+ * import { foo as bar } from './file' -> { localName: 'bar', importedName: 'foo', source: './file' }
894
+ * import foo from './file' -> { localName: 'foo', importedName: 'default', source: './file' }
895
+ * import * as foo from './file' -> { localName: 'foo', importedName: '*', source: './file' }
896
+ */
897
+ function extractImports(ast: BabelFile): ImportInfo[] {
898
+ const imports: ImportInfo[] = []
899
+
900
+ for (const node of ast.program.body) {
901
+ if (node.type === 'ImportDeclaration') {
902
+ const source = (node.source as BabelNode)?.value as string
903
+ if (!source) continue
904
+
905
+ const specifiers = node.specifiers as BabelNode[] | undefined
906
+ for (const spec of specifiers ?? []) {
907
+ if (spec.type === 'ImportSpecifier') {
908
+ // Named import: import { foo } from './file' or import { foo as bar } from './file'
909
+ const imported = spec.imported as BabelNode | undefined
910
+ const local = spec.local as BabelNode | undefined
911
+ if (imported?.type === 'Identifier' && local?.type === 'Identifier') {
912
+ imports.push({
913
+ localName: local.name as string,
914
+ importedName: imported.name as string,
915
+ source,
916
+ })
917
+ }
918
+ } else if (spec.type === 'ImportDefaultSpecifier') {
919
+ // Default import: import foo from './file'
920
+ const local = spec.local as BabelNode | undefined
921
+ if (local?.type === 'Identifier') {
922
+ imports.push({
923
+ localName: local.name as string,
924
+ importedName: 'default',
925
+ source,
926
+ })
927
+ }
928
+ } else if (spec.type === 'ImportNamespaceSpecifier') {
929
+ // Namespace import: import * as foo from './file'
930
+ const local = spec.local as BabelNode | undefined
931
+ if (local?.type === 'Identifier') {
932
+ imports.push({
933
+ localName: local.name as string,
934
+ importedName: '*',
935
+ source,
936
+ })
937
+ }
938
+ }
939
+ }
940
+ }
941
+ }
942
+
943
+ return imports
944
+ }
945
+
946
+ /**
947
+ * Resolve an import source path to an absolute file path.
948
+ * Handles relative paths and tries common extensions.
949
+ */
950
+ async function resolveImportPath(source: string, fromFile: string): Promise<string | null> {
951
+ // Only handle relative imports
952
+ if (!source.startsWith('.')) {
953
+ return null
954
+ }
955
+
956
+ const fromDir = path.dirname(fromFile)
957
+ const basePath = path.resolve(fromDir, source)
958
+
959
+ // Try different extensions
960
+ const extensions = ['.ts', '.js', '.astro', '.tsx', '.jsx', '']
961
+ for (const ext of extensions) {
962
+ const fullPath = basePath + ext
963
+ try {
964
+ await fs.access(fullPath)
965
+ return fullPath
966
+ } catch {
967
+ // File doesn't exist with this extension
968
+ }
969
+ }
970
+
971
+ // Try index files
972
+ for (const ext of ['.ts', '.js', '.tsx', '.jsx']) {
973
+ const indexPath = path.join(basePath, `index${ext}`)
974
+ try {
975
+ await fs.access(indexPath)
976
+ return indexPath
977
+ } catch {
978
+ // File doesn't exist
979
+ }
980
+ }
981
+
982
+ return null
983
+ }
984
+
985
+ /**
986
+ * Parse a TypeScript/JavaScript file and extract exported variable definitions.
987
+ */
988
+ async function getExportedDefinitions(filePath: string): Promise<VariableDefinition[]> {
989
+ try {
990
+ const content = await fs.readFile(filePath, 'utf-8')
991
+ const ast = parseBabel(content, {
992
+ sourceType: 'module',
993
+ plugins: ['typescript'],
994
+ errorRecovery: true,
995
+ }) as unknown as BabelFile
996
+
997
+ const definitions: VariableDefinition[] = []
998
+ const lines = content.split('\n')
999
+
1000
+ function getStringValue(node: BabelNode): string | null {
1001
+ if (node.type === 'StringLiteral') {
1002
+ return node.value as string
1003
+ }
1004
+ if (node.type === 'TemplateLiteral') {
1005
+ const quasis = node.quasis as Array<{ value: { cooked: string | null } }> | undefined
1006
+ const expressions = node.expressions as unknown[] | undefined
1007
+ if (quasis?.length === 1 && expressions?.length === 0) {
1008
+ return quasis[0]?.value.cooked ?? null
1009
+ }
1010
+ }
1011
+ return null
1012
+ }
1013
+
1014
+ function extractObjectProperties(objNode: BabelNode, parentPath: string, line: number): void {
1015
+ const properties = objNode.properties as BabelNode[] | undefined
1016
+ for (const prop of properties ?? []) {
1017
+ if (prop.type !== 'ObjectProperty') continue
1018
+ const key = prop.key as BabelNode | undefined
1019
+ const value = prop.value as BabelNode | undefined
1020
+ if (!key || key.type !== 'Identifier' || !value) continue
1021
+
1022
+ const propName = key.name as string
1023
+ const fullPath = `${parentPath}.${propName}`
1024
+ const propLoc = prop.loc as { start: { line: number } } | undefined
1025
+ const propLine = propLoc?.start.line ?? line
1026
+
1027
+ const stringValue = getStringValue(value)
1028
+ if (stringValue !== null) {
1029
+ definitions.push({
1030
+ name: propName,
1031
+ value: stringValue,
1032
+ line: propLine,
1033
+ parentName: parentPath,
1034
+ })
1035
+ }
1036
+
1037
+ if (value.type === 'ObjectExpression') {
1038
+ extractObjectProperties(value, fullPath, propLine)
1039
+ }
1040
+
1041
+ if (value.type === 'ArrayExpression') {
1042
+ extractArrayElements(value, fullPath, propLine)
1043
+ }
1044
+ }
1045
+ }
1046
+
1047
+ function extractArrayElements(arrNode: BabelNode, parentPath: string, defaultLine: number): void {
1048
+ const elements = arrNode.elements as BabelNode[] | undefined
1049
+ for (let i = 0; i < (elements?.length ?? 0); i++) {
1050
+ const elem = elements![i]
1051
+ if (!elem) continue
1052
+
1053
+ const elemLoc = elem.loc as { start: { line: number } } | undefined
1054
+ const elemLine = elemLoc?.start.line ?? defaultLine
1055
+ const indexPath = `${parentPath}[${i}]`
1056
+
1057
+ const elemValue = getStringValue(elem)
1058
+ if (elemValue !== null) {
1059
+ definitions.push({
1060
+ name: String(i),
1061
+ value: elemValue,
1062
+ line: elemLine,
1063
+ parentName: parentPath,
1064
+ })
1065
+ }
1066
+
1067
+ if (elem.type === 'ObjectExpression') {
1068
+ const objProperties = elem.properties as BabelNode[] | undefined
1069
+ for (const prop of objProperties ?? []) {
1070
+ if (prop.type !== 'ObjectProperty') continue
1071
+ const key = prop.key as BabelNode | undefined
1072
+ const value = prop.value as BabelNode | undefined
1073
+ if (!key || key.type !== 'Identifier' || !value) continue
1074
+
1075
+ const propName = key.name as string
1076
+ const propLoc = prop.loc as { start: { line: number } } | undefined
1077
+ const propLine = propLoc?.start.line ?? elemLine
1078
+
1079
+ const stringValue = getStringValue(value)
1080
+ if (stringValue !== null) {
1081
+ definitions.push({
1082
+ name: propName,
1083
+ value: stringValue,
1084
+ line: propLine,
1085
+ parentName: indexPath,
1086
+ })
1087
+ }
1088
+
1089
+ if (value.type === 'ObjectExpression') {
1090
+ extractObjectProperties(value, `${indexPath}.${propName}`, propLine)
1091
+ }
1092
+ }
1093
+ }
1094
+ }
1095
+ }
1096
+
1097
+ for (const node of ast.program.body) {
1098
+ // Handle: export const foo = 'value'
1099
+ if (node.type === 'ExportNamedDeclaration') {
1100
+ const declaration = node.declaration as BabelNode | undefined
1101
+ if (declaration?.type === 'VariableDeclaration') {
1102
+ const declarations = declaration.declarations as BabelNode[] | undefined
1103
+ for (const decl of declarations ?? []) {
1104
+ const id = decl.id as BabelNode | undefined
1105
+ const init = decl.init as BabelNode | undefined
1106
+ if (id?.type === 'Identifier' && init) {
1107
+ const varName = id.name as string
1108
+ const loc = decl.loc as { start: { line: number } } | undefined
1109
+ const line = loc?.start.line ?? 1
1110
+
1111
+ const stringValue = getStringValue(init)
1112
+ if (stringValue !== null) {
1113
+ definitions.push({ name: varName, value: stringValue, line })
1114
+ }
1115
+
1116
+ if (init.type === 'ObjectExpression') {
1117
+ extractObjectProperties(init, varName, line)
1118
+ }
1119
+
1120
+ if (init.type === 'ArrayExpression') {
1121
+ extractArrayElements(init, varName, line)
1122
+ }
1123
+ }
1124
+ }
1125
+ }
1126
+ }
1127
+
1128
+ // Handle: const foo = 'value'; export { foo }
1129
+ // First collect all variable declarations
1130
+ if (node.type === 'VariableDeclaration') {
1131
+ const declarations = node.declarations as BabelNode[] | undefined
1132
+ for (const decl of declarations ?? []) {
1133
+ const id = decl.id as BabelNode | undefined
1134
+ const init = decl.init as BabelNode | undefined
1135
+ if (id?.type === 'Identifier' && init) {
1136
+ const varName = id.name as string
1137
+ const loc = decl.loc as { start: { line: number } } | undefined
1138
+ const line = loc?.start.line ?? 1
1139
+
1140
+ const stringValue = getStringValue(init)
1141
+ if (stringValue !== null) {
1142
+ definitions.push({ name: varName, value: stringValue, line })
1143
+ }
1144
+
1145
+ if (init.type === 'ObjectExpression') {
1146
+ extractObjectProperties(init, varName, line)
1147
+ }
1148
+
1149
+ if (init.type === 'ArrayExpression') {
1150
+ extractArrayElements(init, varName, line)
1151
+ }
1152
+ }
1153
+ }
1154
+ }
1155
+ }
1156
+
1157
+ return definitions
1158
+ } catch {
1159
+ return []
1160
+ }
651
1161
  }
652
1162
 
653
1163
  interface TemplateMatch {
@@ -656,21 +1166,49 @@ interface TemplateMatch {
656
1166
  variableName?: string
657
1167
  /** For variables, the definition line in frontmatter */
658
1168
  definitionLine?: number
1169
+ /** If true, the expression uses a variable from props that needs cross-file tracking */
1170
+ usesProp?: boolean
1171
+ /** The prop name if usesProp is true */
1172
+ propName?: string
1173
+ /** The full expression path if usesProp is true (e.g., 'items[0]') */
1174
+ expressionPath?: string
1175
+ /** If true, the expression uses a variable from an import */
1176
+ usesImport?: boolean
1177
+ /** The import info if usesImport is true */
1178
+ importInfo?: ImportInfo
1179
+ }
1180
+
1181
+ /** Result type for findElementWithText - returns best match and all prop/import candidates */
1182
+ interface FindElementResult {
1183
+ /** The best match found (local variables or static content) */
1184
+ bestMatch: TemplateMatch | null
1185
+ /** All prop-based matches for the tag (need cross-file verification) */
1186
+ propCandidates: TemplateMatch[]
1187
+ /** All import-based matches for the tag (need cross-file verification) */
1188
+ importCandidates: TemplateMatch[]
659
1189
  }
660
1190
 
661
1191
  /**
662
- * Walk the Astro AST to find elements matching a tag with specific text content
1192
+ * Walk the Astro AST to find elements matching a tag with specific text content.
1193
+ * Returns the best match (local variables or static content) AND all prop/import candidates
1194
+ * that need cross-file verification for multiple same-tag elements.
1195
+ * @param propAliases - Map of local variable names to prop names from Astro.props (for cross-file tracking)
1196
+ * @param imports - Import information from frontmatter (for cross-file tracking)
663
1197
  */
664
1198
  function findElementWithText(
665
1199
  ast: AstroNode,
666
1200
  tag: string,
667
1201
  searchText: string,
668
1202
  variableDefinitions: VariableDefinition[],
669
- ): TemplateMatch | null {
1203
+ propAliases: Map<string, string> = new Map(),
1204
+ imports: ImportInfo[] = [],
1205
+ ): FindElementResult {
670
1206
  const normalizedSearch = normalizeText(searchText)
671
1207
  const tagLower = tag.toLowerCase()
672
1208
  let bestMatch: TemplateMatch | null = null
673
1209
  let bestScore = 0
1210
+ const propCandidates: TemplateMatch[] = []
1211
+ const importCandidates: TemplateMatch[] = []
674
1212
 
675
1213
  function getTextContent(node: AstroNode): string {
676
1214
  if (node.type === 'text') {
@@ -688,10 +1226,10 @@ function findElementWithText(
688
1226
  // Try to extract variable name from expression
689
1227
  // The expression node children contain the text representation
690
1228
  const exprText = getTextContent(node)
691
- // Extract variable names like {foo} or {foo.bar}
692
- const match = exprText.match(/^\s*(\w+)(?:\.(\w+))?\s*$/)
693
- if (match) {
694
- varNames.push(match[2] ?? match[1]!)
1229
+ // Extract variable paths like {foo}, {foo.bar}, {items[0]}, {config.nav.title}, {links[0].text}
1230
+ const fullPath = parseExpressionPath(exprText)
1231
+ if (fullPath) {
1232
+ varNames.push(fullPath)
695
1233
  }
696
1234
  return { found: true, varNames }
697
1235
  }
@@ -706,6 +1244,15 @@ function findElementWithText(
706
1244
  return { found: varNames.length > 0, varNames }
707
1245
  }
708
1246
 
1247
+ /**
1248
+ * Extract the base variable name from an expression path.
1249
+ * e.g., 'items[0]' -> 'items', 'config.nav.title' -> 'config'
1250
+ */
1251
+ function getBaseVarName(exprPath: string): string {
1252
+ const match = exprPath.match(/^(\w+)/)
1253
+ return match?.[1] ?? exprPath
1254
+ }
1255
+
709
1256
  function visit(node: AstroNode) {
710
1257
  // Check if this is an element or component matching our tag
711
1258
  if ((node.type === 'element' || node.type === 'component') && node.name.toLowerCase() === tagLower) {
@@ -718,9 +1265,15 @@ function findElementWithText(
718
1265
  const exprInfo = hasExpressionChild(elemNode)
719
1266
  if (exprInfo.found && exprInfo.varNames.length > 0) {
720
1267
  // Look for matching variable definition
721
- for (const varName of exprInfo.varNames) {
1268
+ for (const exprPath of exprInfo.varNames) {
1269
+ let foundInLocal = false
1270
+
722
1271
  for (const def of variableDefinitions) {
723
- if (def.name === varName || (def.parentName && def.name === varName)) {
1272
+ // Build the full definition path for comparison
1273
+ const defPath = buildDefinitionPath(def)
1274
+ // Check if the expression path matches the definition path
1275
+ if (defPath === exprPath) {
1276
+ foundInLocal = true
724
1277
  const normalizedDef = normalizeText(def.value)
725
1278
  if (normalizedDef === normalizedSearch) {
726
1279
  // Found a variable match - this is highest priority
@@ -729,7 +1282,7 @@ function findElementWithText(
729
1282
  bestMatch = {
730
1283
  line,
731
1284
  type: 'variable',
732
- variableName: def.parentName ? `${def.parentName}.${def.name}` : def.name,
1285
+ variableName: defPath,
733
1286
  definitionLine: def.line,
734
1287
  }
735
1288
  }
@@ -737,6 +1290,38 @@ function findElementWithText(
737
1290
  }
738
1291
  }
739
1292
  }
1293
+
1294
+ // If not found in local definitions, check if it's from props or imports
1295
+ if (!foundInLocal) {
1296
+ const baseVar = getBaseVarName(exprPath)
1297
+
1298
+ // Check props first
1299
+ const actualPropName = propAliases.get(baseVar)
1300
+ if (actualPropName) {
1301
+ // This expression uses a prop - collect as candidate for cross-file verification
1302
+ // (don't set bestMatch yet - we need to verify each candidate)
1303
+ propCandidates.push({
1304
+ line,
1305
+ type: 'variable',
1306
+ usesProp: true,
1307
+ propName: actualPropName, // Use the actual prop name, not the local alias
1308
+ expressionPath: exprPath,
1309
+ })
1310
+ } else {
1311
+ // Check if it's from an import
1312
+ const importInfo = imports.find((imp) => imp.localName === baseVar)
1313
+ if (importInfo) {
1314
+ // This expression uses an import - collect as candidate for cross-file verification
1315
+ importCandidates.push({
1316
+ line,
1317
+ type: 'variable',
1318
+ usesImport: true,
1319
+ importInfo,
1320
+ expressionPath: exprPath,
1321
+ })
1322
+ }
1323
+ }
1324
+ }
740
1325
  }
741
1326
  }
742
1327
 
@@ -814,7 +1399,7 @@ function findElementWithText(
814
1399
  }
815
1400
 
816
1401
  visit(ast)
817
- return bestMatch
1402
+ return { bestMatch, propCandidates, importCandidates }
818
1403
  }
819
1404
 
820
1405
  interface ComponentPropMatch {
@@ -864,6 +1449,297 @@ function findComponentProp(
864
1449
  return visit(ast)
865
1450
  }
866
1451
 
1452
+ interface ExpressionPropMatch {
1453
+ componentName: string
1454
+ propName: string
1455
+ /** The expression text (e.g., 'navItems' from items={navItems}) */
1456
+ expressionText: string
1457
+ line: number
1458
+ }
1459
+
1460
+ interface SpreadPropMatch {
1461
+ componentName: string
1462
+ /** The variable name being spread (e.g., 'cardProps' from {...cardProps}) */
1463
+ spreadVarName: string
1464
+ line: number
1465
+ }
1466
+
1467
+ /**
1468
+ * Walk the Astro AST to find component usages with expression props.
1469
+ * Looks for patterns like: <Nav items={navItems} />
1470
+ * @param ast - The Astro AST
1471
+ * @param componentName - The component name to search for (e.g., 'Nav')
1472
+ * @param propName - The prop name to find (e.g., 'items')
1473
+ */
1474
+ function findExpressionProp(
1475
+ ast: AstroNode,
1476
+ componentName: string,
1477
+ propName: string,
1478
+ ): ExpressionPropMatch | null {
1479
+ function visit(node: AstroNode): ExpressionPropMatch | null {
1480
+ // Check component nodes matching the name
1481
+ if (node.type === 'component') {
1482
+ const compNode = node as ComponentNode
1483
+ if (compNode.name === componentName) {
1484
+ for (const attr of compNode.attributes) {
1485
+ // Check for expression attributes: items={navItems}
1486
+ if (attr.type === 'attribute' && attr.name === propName && attr.kind === 'expression') {
1487
+ // The value contains the expression text
1488
+ const exprText = attr.value?.trim() || ''
1489
+ if (exprText) {
1490
+ return {
1491
+ componentName,
1492
+ propName,
1493
+ expressionText: exprText,
1494
+ line: attr.position?.start.line ?? compNode.position?.start.line ?? 0,
1495
+ }
1496
+ }
1497
+ }
1498
+ }
1499
+ }
1500
+ }
1501
+
1502
+ // Recursively visit children
1503
+ if ('children' in node && Array.isArray(node.children)) {
1504
+ for (const child of node.children) {
1505
+ const result = visit(child)
1506
+ if (result) return result
1507
+ }
1508
+ }
1509
+
1510
+ return null
1511
+ }
1512
+
1513
+ return visit(ast)
1514
+ }
1515
+
1516
+ /**
1517
+ * Walk the Astro AST to find component usages with spread props.
1518
+ * Looks for patterns like: <Card {...cardProps} />
1519
+ * @param ast - The Astro AST
1520
+ * @param componentName - The component name to search for (e.g., 'Card')
1521
+ */
1522
+ function findSpreadProp(
1523
+ ast: AstroNode,
1524
+ componentName: string,
1525
+ ): SpreadPropMatch | null {
1526
+ function visit(node: AstroNode): SpreadPropMatch | null {
1527
+ // Check component nodes matching the name
1528
+ if (node.type === 'component') {
1529
+ const compNode = node as ComponentNode
1530
+ if (compNode.name === componentName) {
1531
+ for (const attr of compNode.attributes) {
1532
+ // Check for spread attributes: {...cardProps}
1533
+ // In Astro AST: type='attribute', kind='spread', name=variable name
1534
+ if (attr.type === 'attribute' && attr.kind === 'spread' && attr.name) {
1535
+ return {
1536
+ componentName,
1537
+ spreadVarName: attr.name,
1538
+ line: attr.position?.start.line ?? compNode.position?.start.line ?? 0,
1539
+ }
1540
+ }
1541
+ }
1542
+ }
1543
+ }
1544
+
1545
+ // Recursively visit children
1546
+ if ('children' in node && Array.isArray(node.children)) {
1547
+ for (const child of node.children) {
1548
+ const result = visit(child)
1549
+ if (result) return result
1550
+ }
1551
+ }
1552
+
1553
+ return null
1554
+ }
1555
+
1556
+ return visit(ast)
1557
+ }
1558
+
1559
+ /**
1560
+ * Search for a component usage with an expression prop across all files.
1561
+ * When we find an expression like {items[0]} in a component where items comes from props,
1562
+ * we search for where that component is used and track the expression prop back.
1563
+ * Supports multi-level prop drilling with a depth limit.
1564
+ *
1565
+ * @param componentFileName - The file name of the component (e.g., 'Nav.astro')
1566
+ * @param propName - The prop name we're looking for (e.g., 'items')
1567
+ * @param expressionPath - The full expression path (e.g., 'items[0]')
1568
+ * @param searchText - The text content we're searching for
1569
+ * @param depth - Current recursion depth (default 0, max 5)
1570
+ * @returns Source location if found
1571
+ */
1572
+ async function searchForExpressionProp(
1573
+ componentFileName: string,
1574
+ propName: string,
1575
+ expressionPath: string,
1576
+ searchText: string,
1577
+ depth: number = 0,
1578
+ ): Promise<SourceLocation | undefined> {
1579
+ // Limit recursion depth to prevent infinite loops
1580
+ if (depth > 5) return undefined
1581
+
1582
+ const srcDir = path.join(getProjectRoot(), 'src')
1583
+ const searchDirs = [
1584
+ path.join(srcDir, 'pages'),
1585
+ path.join(srcDir, 'components'),
1586
+ path.join(srcDir, 'layouts'),
1587
+ ]
1588
+
1589
+ // Extract the component name from file name (e.g., 'Nav.astro' -> 'Nav')
1590
+ const componentName = path.basename(componentFileName, '.astro')
1591
+ const normalizedSearch = normalizeText(searchText)
1592
+
1593
+ for (const dir of searchDirs) {
1594
+ try {
1595
+ const result = await searchDirForExpressionProp(
1596
+ dir,
1597
+ componentName,
1598
+ propName,
1599
+ expressionPath,
1600
+ normalizedSearch,
1601
+ searchText,
1602
+ depth,
1603
+ )
1604
+ if (result) return result
1605
+ } catch {
1606
+ // Directory doesn't exist, continue
1607
+ }
1608
+ }
1609
+
1610
+ return undefined
1611
+ }
1612
+
1613
+ async function searchDirForExpressionProp(
1614
+ dir: string,
1615
+ componentName: string,
1616
+ propName: string,
1617
+ expressionPath: string,
1618
+ normalizedSearch: string,
1619
+ searchText: string,
1620
+ depth: number,
1621
+ ): Promise<SourceLocation | undefined> {
1622
+ try {
1623
+ const entries = await fs.readdir(dir, { withFileTypes: true })
1624
+
1625
+ for (const entry of entries) {
1626
+ const fullPath = path.join(dir, entry.name)
1627
+
1628
+ if (entry.isDirectory()) {
1629
+ const result = await searchDirForExpressionProp(
1630
+ fullPath,
1631
+ componentName,
1632
+ propName,
1633
+ expressionPath,
1634
+ normalizedSearch,
1635
+ searchText,
1636
+ depth,
1637
+ )
1638
+ if (result) return result
1639
+ } else if (entry.isFile() && entry.name.endsWith('.astro')) {
1640
+ const cached = await getCachedParsedFile(fullPath)
1641
+ if (!cached) continue
1642
+
1643
+ // First, try to find expression prop usage: <Nav items={navItems} />
1644
+ const exprPropMatch = findExpressionProp(cached.ast, componentName, propName)
1645
+
1646
+ if (exprPropMatch) {
1647
+ // The expression text might be a simple variable like 'navItems'
1648
+ const exprText = exprPropMatch.expressionText
1649
+
1650
+ // Build the corresponding path in the parent's variable definitions
1651
+ // e.g., if expressionPath is 'items[0]' and exprText is 'navItems',
1652
+ // we look for 'navItems[0]' in the parent's definitions
1653
+ const parentPath = expressionPath.replace(/^[^.[]+/, exprText)
1654
+
1655
+ // Check if the value is in local variable definitions
1656
+ for (const def of cached.variableDefinitions) {
1657
+ const defPath = buildDefinitionPath(def)
1658
+ if (defPath === parentPath) {
1659
+ const normalizedDef = normalizeText(def.value)
1660
+ if (normalizedDef === normalizedSearch) {
1661
+ return {
1662
+ file: path.relative(getProjectRoot(), fullPath),
1663
+ line: def.line,
1664
+ snippet: cached.lines[def.line - 1] || '',
1665
+ type: 'variable',
1666
+ variableName: defPath,
1667
+ definitionLine: def.line,
1668
+ }
1669
+ }
1670
+ }
1671
+ }
1672
+
1673
+ // Check if exprText is itself from props (multi-level prop drilling)
1674
+ const baseVar = exprText.match(/^(\w+)/)?.[1]
1675
+ if (baseVar && cached.propAliases.has(baseVar)) {
1676
+ const actualPropName = cached.propAliases.get(baseVar)!
1677
+ // Recursively search for where this component is used
1678
+ const result = await searchForExpressionProp(
1679
+ entry.name,
1680
+ actualPropName,
1681
+ parentPath, // Use the path with the parent's variable name
1682
+ searchText,
1683
+ depth + 1,
1684
+ )
1685
+ if (result) return result
1686
+ }
1687
+
1688
+ continue
1689
+ }
1690
+
1691
+ // Second, try to find spread prop usage: <Card {...cardProps} />
1692
+ const spreadMatch = findSpreadProp(cached.ast, componentName)
1693
+
1694
+ if (spreadMatch) {
1695
+ // Find the spread variable's definition
1696
+ const spreadVarName = spreadMatch.spreadVarName
1697
+
1698
+ // The propName we're looking for should be a property of the spread object
1699
+ // e.g., if propName is 'title' and spread is {...cardProps},
1700
+ // we look for cardProps.title in the definitions
1701
+ const spreadPropPath = `${spreadVarName}.${propName}`
1702
+
1703
+ for (const def of cached.variableDefinitions) {
1704
+ const defPath = buildDefinitionPath(def)
1705
+ if (defPath === spreadPropPath) {
1706
+ const normalizedDef = normalizeText(def.value)
1707
+ if (normalizedDef === normalizedSearch) {
1708
+ return {
1709
+ file: path.relative(getProjectRoot(), fullPath),
1710
+ line: def.line,
1711
+ snippet: cached.lines[def.line - 1] || '',
1712
+ type: 'variable',
1713
+ variableName: defPath,
1714
+ definitionLine: def.line,
1715
+ }
1716
+ }
1717
+ }
1718
+ }
1719
+
1720
+ // Check if the spread variable itself comes from props
1721
+ if (cached.propAliases.has(spreadVarName)) {
1722
+ const actualPropName = cached.propAliases.get(spreadVarName)!
1723
+ // For spread from props, we need to search for the full path
1724
+ const result = await searchForExpressionProp(
1725
+ entry.name,
1726
+ actualPropName,
1727
+ expressionPath,
1728
+ searchText,
1729
+ depth + 1,
1730
+ )
1731
+ if (result) return result
1732
+ }
1733
+ }
1734
+ }
1735
+ }
1736
+ } catch {
1737
+ // Error reading directory
1738
+ }
1739
+
1740
+ return undefined
1741
+ }
1742
+
867
1743
  interface ImageMatch {
868
1744
  line: number
869
1745
  src: string
@@ -1160,20 +2036,28 @@ async function searchAstroFile(
1160
2036
  const cached = await getCachedParsedFile(filePath)
1161
2037
  if (!cached) return undefined
1162
2038
 
1163
- const { lines, ast, variableDefinitions } = cached
2039
+ const { lines, ast, variableDefinitions, propAliases, imports } = cached
1164
2040
 
1165
2041
  // Find matching element in template AST
1166
- const match = findElementWithText(ast, tag, textContent, variableDefinitions)
2042
+ const { bestMatch, propCandidates, importCandidates } = findElementWithText(
2043
+ ast,
2044
+ tag,
2045
+ textContent,
2046
+ variableDefinitions,
2047
+ propAliases,
2048
+ imports,
2049
+ )
1167
2050
 
1168
- if (match) {
2051
+ // First, check if we have a direct match (local variable or static content)
2052
+ if (bestMatch && !bestMatch.usesProp && !bestMatch.usesImport) {
1169
2053
  // Determine the editable line (definition for variables, usage for static)
1170
- const editableLine = match.type === 'variable' && match.definitionLine
1171
- ? match.definitionLine
1172
- : match.line
2054
+ const editableLine = bestMatch.type === 'variable' && bestMatch.definitionLine
2055
+ ? bestMatch.definitionLine
2056
+ : bestMatch.line
1173
2057
 
1174
2058
  // Get the source snippet - innerHTML for static content, definition line for variables
1175
2059
  let snippet: string
1176
- if (match.type === 'static') {
2060
+ if (bestMatch.type === 'static') {
1177
2061
  // For static content, extract only the innerHTML (not the wrapper element)
1178
2062
  const completeSnippet = extractCompleteTagSnippet(lines, editableLine - 1, tag)
1179
2063
  snippet = extractInnerHtmlFromSnippet(completeSnippet, tag) ?? completeSnippet
@@ -1186,9 +2070,42 @@ async function searchAstroFile(
1186
2070
  file: path.relative(getProjectRoot(), filePath),
1187
2071
  line: editableLine,
1188
2072
  snippet,
1189
- type: match.type,
1190
- variableName: match.variableName,
1191
- definitionLine: match.type === 'variable' ? match.definitionLine : undefined,
2073
+ type: bestMatch.type,
2074
+ variableName: bestMatch.variableName,
2075
+ definitionLine: bestMatch.type === 'variable' ? bestMatch.definitionLine : undefined,
2076
+ }
2077
+ }
2078
+
2079
+ // Try all prop candidates - verify each one to find the correct match
2080
+ // (handles multiple same-tag elements with different prop values)
2081
+ for (const propCandidate of propCandidates) {
2082
+ if (propCandidate.propName && propCandidate.expressionPath) {
2083
+ const componentFileName = path.basename(filePath)
2084
+ const exprPropResult = await searchForExpressionProp(
2085
+ componentFileName,
2086
+ propCandidate.propName,
2087
+ propCandidate.expressionPath,
2088
+ textContent,
2089
+ )
2090
+ if (exprPropResult) {
2091
+ return exprPropResult
2092
+ }
2093
+ }
2094
+ }
2095
+
2096
+ // Try all import candidates - verify each one to find the correct match
2097
+ // (handles multiple same-tag elements with different imported values)
2098
+ for (const importCandidate of importCandidates) {
2099
+ if (importCandidate.importInfo && importCandidate.expressionPath) {
2100
+ const importResult = await searchForImportedValue(
2101
+ filePath,
2102
+ importCandidate.importInfo,
2103
+ importCandidate.expressionPath,
2104
+ textContent,
2105
+ )
2106
+ if (importResult) {
2107
+ return importResult
2108
+ }
1192
2109
  }
1193
2110
  }
1194
2111
  } catch {
@@ -1198,6 +2115,70 @@ async function searchAstroFile(
1198
2115
  return undefined
1199
2116
  }
1200
2117
 
2118
+ /**
2119
+ * Search for a value in an imported file.
2120
+ * @param fromFile - The file that contains the import
2121
+ * @param importInfo - Information about the import
2122
+ * @param expressionPath - The full expression path (e.g., 'config.title' or 'navItems[0]')
2123
+ * @param searchText - The text content we're searching for
2124
+ */
2125
+ async function searchForImportedValue(
2126
+ fromFile: string,
2127
+ importInfo: ImportInfo,
2128
+ expressionPath: string,
2129
+ searchText: string,
2130
+ ): Promise<SourceLocation | undefined> {
2131
+ // Resolve the import path to an absolute file path
2132
+ const importedFilePath = await resolveImportPath(importInfo.source, fromFile)
2133
+ if (!importedFilePath) return undefined
2134
+
2135
+ // Get exported definitions from the imported file
2136
+ const exportedDefs = await getExportedDefinitions(importedFilePath)
2137
+ if (exportedDefs.length === 0) return undefined
2138
+
2139
+ const normalizedSearch = normalizeText(searchText)
2140
+
2141
+ // Build the path we're looking for in the imported file
2142
+ // e.g., if expressionPath is 'config.title' and localName is 'config',
2143
+ // and importedName is 'siteConfig', we look for 'siteConfig.title'
2144
+ let targetPath: string
2145
+ if (importInfo.importedName === 'default' || importInfo.importedName === importInfo.localName) {
2146
+ // Direct import: import { config } from './file' or import config from './file'
2147
+ // The expression path uses the local name, which matches the exported name
2148
+ targetPath = expressionPath
2149
+ } else {
2150
+ // Renamed import: import { config as siteConfig } from './file'
2151
+ // Replace the local name with the original exported name
2152
+ targetPath = expressionPath.replace(
2153
+ new RegExp(`^${importInfo.localName}`),
2154
+ importInfo.importedName,
2155
+ )
2156
+ }
2157
+
2158
+ // Search for the target path in the exported definitions
2159
+ for (const def of exportedDefs) {
2160
+ const defPath = buildDefinitionPath(def)
2161
+ if (defPath === targetPath) {
2162
+ const normalizedDef = normalizeText(def.value)
2163
+ if (normalizedDef === normalizedSearch) {
2164
+ const importedFileContent = await fs.readFile(importedFilePath, 'utf-8')
2165
+ const importedLines = importedFileContent.split('\n')
2166
+
2167
+ return {
2168
+ file: path.relative(getProjectRoot(), importedFilePath),
2169
+ line: def.line,
2170
+ snippet: importedLines[def.line - 1] || '',
2171
+ type: 'variable',
2172
+ variableName: defPath,
2173
+ definitionLine: def.line,
2174
+ }
2175
+ }
2176
+ }
2177
+ }
2178
+
2179
+ return undefined
2180
+ }
2181
+
1201
2182
  /**
1202
2183
  * Search for prop values passed to components using AST parsing.
1203
2184
  * Uses caching for better performance.