@nuasite/cms 0.1.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.
Files changed (269) hide show
  1. package/README.md +237 -0
  2. package/dist/src/build-processor.d.ts +20 -0
  3. package/dist/src/build-processor.d.ts.map +1 -0
  4. package/dist/src/collection-scanner.d.ts +6 -0
  5. package/dist/src/collection-scanner.d.ts.map +1 -0
  6. package/dist/src/component-registry.d.ts +63 -0
  7. package/dist/src/component-registry.d.ts.map +1 -0
  8. package/dist/src/config.d.ts +24 -0
  9. package/dist/src/config.d.ts.map +1 -0
  10. package/dist/src/dev-middleware.d.ts +20 -0
  11. package/dist/src/dev-middleware.d.ts.map +1 -0
  12. package/dist/src/editor/ai.d.ts +60 -0
  13. package/dist/src/editor/ai.d.ts.map +1 -0
  14. package/dist/src/editor/api.d.ts +140 -0
  15. package/dist/src/editor/api.d.ts.map +1 -0
  16. package/dist/src/editor/color-utils.d.ts +106 -0
  17. package/dist/src/editor/color-utils.d.ts.map +1 -0
  18. package/dist/src/editor/components/ai-chat.d.ts +11 -0
  19. package/dist/src/editor/components/ai-chat.d.ts.map +1 -0
  20. package/dist/src/editor/components/ai-tooltip.d.ts +12 -0
  21. package/dist/src/editor/components/ai-tooltip.d.ts.map +1 -0
  22. package/dist/src/editor/components/attribute-editor.d.ts +5 -0
  23. package/dist/src/editor/components/attribute-editor.d.ts.map +1 -0
  24. package/dist/src/editor/components/block-editor.d.ts +12 -0
  25. package/dist/src/editor/components/block-editor.d.ts.map +1 -0
  26. package/dist/src/editor/components/collections-browser.d.ts +2 -0
  27. package/dist/src/editor/components/collections-browser.d.ts.map +1 -0
  28. package/dist/src/editor/components/color-toolbar.d.ts +12 -0
  29. package/dist/src/editor/components/color-toolbar.d.ts.map +1 -0
  30. package/dist/src/editor/components/confirm-dialog.d.ts +2 -0
  31. package/dist/src/editor/components/confirm-dialog.d.ts.map +1 -0
  32. package/dist/src/editor/components/create-page-modal.d.ts +2 -0
  33. package/dist/src/editor/components/create-page-modal.d.ts.map +1 -0
  34. package/dist/src/editor/components/editable-highlights.d.ts +9 -0
  35. package/dist/src/editor/components/editable-highlights.d.ts.map +1 -0
  36. package/dist/src/editor/components/error-boundary.d.ts +32 -0
  37. package/dist/src/editor/components/error-boundary.d.ts.map +1 -0
  38. package/dist/src/editor/components/fields.d.ts +75 -0
  39. package/dist/src/editor/components/fields.d.ts.map +1 -0
  40. package/dist/src/editor/components/frontmatter-fields.d.ts +29 -0
  41. package/dist/src/editor/components/frontmatter-fields.d.ts.map +1 -0
  42. package/dist/src/editor/components/highlight-overlay.d.ts +64 -0
  43. package/dist/src/editor/components/highlight-overlay.d.ts.map +1 -0
  44. package/dist/src/editor/components/image-overlay.d.ts +12 -0
  45. package/dist/src/editor/components/image-overlay.d.ts.map +1 -0
  46. package/dist/src/editor/components/markdown-editor-overlay.d.ts +6 -0
  47. package/dist/src/editor/components/markdown-editor-overlay.d.ts.map +1 -0
  48. package/dist/src/editor/components/markdown-inline-editor.d.ts +10 -0
  49. package/dist/src/editor/components/markdown-inline-editor.d.ts.map +1 -0
  50. package/dist/src/editor/components/media-library.d.ts +2 -0
  51. package/dist/src/editor/components/media-library.d.ts.map +1 -0
  52. package/dist/src/editor/components/outline.d.ts +21 -0
  53. package/dist/src/editor/components/outline.d.ts.map +1 -0
  54. package/dist/src/editor/components/redirect-countdown.d.ts +2 -0
  55. package/dist/src/editor/components/redirect-countdown.d.ts.map +1 -0
  56. package/dist/src/editor/components/seo-editor.d.ts +2 -0
  57. package/dist/src/editor/components/seo-editor.d.ts.map +1 -0
  58. package/dist/src/editor/components/text-style-toolbar.d.ts +8 -0
  59. package/dist/src/editor/components/text-style-toolbar.d.ts.map +1 -0
  60. package/dist/src/editor/components/toast/toast-container.d.ts +7 -0
  61. package/dist/src/editor/components/toast/toast-container.d.ts.map +1 -0
  62. package/dist/src/editor/components/toast/toast.d.ts +7 -0
  63. package/dist/src/editor/components/toast/toast.d.ts.map +1 -0
  64. package/dist/src/editor/components/toast/types.d.ts +7 -0
  65. package/dist/src/editor/components/toast/types.d.ts.map +1 -0
  66. package/dist/src/editor/components/toolbar.d.ts +21 -0
  67. package/dist/src/editor/components/toolbar.d.ts.map +1 -0
  68. package/dist/src/editor/config.d.ts +4 -0
  69. package/dist/src/editor/config.d.ts.map +1 -0
  70. package/dist/src/editor/constants.d.ts +101 -0
  71. package/dist/src/editor/constants.d.ts.map +1 -0
  72. package/dist/src/editor/context.d.ts +14 -0
  73. package/dist/src/editor/context.d.ts.map +1 -0
  74. package/dist/src/editor/dom.d.ts +77 -0
  75. package/dist/src/editor/dom.d.ts.map +1 -0
  76. package/dist/src/editor/editor.d.ts +64 -0
  77. package/dist/src/editor/editor.d.ts.map +1 -0
  78. package/dist/src/editor/history.d.ts +20 -0
  79. package/dist/src/editor/history.d.ts.map +1 -0
  80. package/dist/src/editor/hooks/index.d.ts +14 -0
  81. package/dist/src/editor/hooks/index.d.ts.map +1 -0
  82. package/dist/src/editor/hooks/useAIHandlers.d.ts +22 -0
  83. package/dist/src/editor/hooks/useAIHandlers.d.ts.map +1 -0
  84. package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts +18 -0
  85. package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts.map +1 -0
  86. package/dist/src/editor/hooks/useElementDetection.d.ts +26 -0
  87. package/dist/src/editor/hooks/useElementDetection.d.ts.map +1 -0
  88. package/dist/src/editor/hooks/useImageHoverDetection.d.ts +12 -0
  89. package/dist/src/editor/hooks/useImageHoverDetection.d.ts.map +1 -0
  90. package/dist/src/editor/hooks/useTextSelection.d.ts +23 -0
  91. package/dist/src/editor/hooks/useTextSelection.d.ts.map +1 -0
  92. package/dist/src/editor/hooks/useTooltipState.d.ts +19 -0
  93. package/dist/src/editor/hooks/useTooltipState.d.ts.map +1 -0
  94. package/dist/src/editor/hooks/utils.d.ts +32 -0
  95. package/dist/src/editor/hooks/utils.d.ts.map +1 -0
  96. package/dist/src/editor/index.d.ts +12 -0
  97. package/dist/src/editor/index.d.ts.map +1 -0
  98. package/dist/src/editor/lib/cn.d.ts +3 -0
  99. package/dist/src/editor/lib/cn.d.ts.map +1 -0
  100. package/dist/src/editor/manifest.d.ts +19 -0
  101. package/dist/src/editor/manifest.d.ts.map +1 -0
  102. package/dist/src/editor/markdown-api.d.ts +36 -0
  103. package/dist/src/editor/markdown-api.d.ts.map +1 -0
  104. package/dist/src/editor/signals.d.ts +242 -0
  105. package/dist/src/editor/signals.d.ts.map +1 -0
  106. package/dist/src/editor/storage.d.ts +27 -0
  107. package/dist/src/editor/storage.d.ts.map +1 -0
  108. package/dist/src/editor/text-styling.d.ts +350 -0
  109. package/dist/src/editor/text-styling.d.ts.map +1 -0
  110. package/dist/src/editor/themes.d.ts +38 -0
  111. package/dist/src/editor/themes.d.ts.map +1 -0
  112. package/dist/src/editor/types.d.ts +454 -0
  113. package/dist/src/editor/types.d.ts.map +1 -0
  114. package/dist/src/error-collector.d.ts +56 -0
  115. package/dist/src/error-collector.d.ts.map +1 -0
  116. package/dist/src/handlers/component-ops.d.ts +34 -0
  117. package/dist/src/handlers/component-ops.d.ts.map +1 -0
  118. package/dist/src/handlers/markdown-ops.d.ts +41 -0
  119. package/dist/src/handlers/markdown-ops.d.ts.map +1 -0
  120. package/dist/src/handlers/request-utils.d.ts +20 -0
  121. package/dist/src/handlers/request-utils.d.ts.map +1 -0
  122. package/dist/src/handlers/source-writer.d.ts +51 -0
  123. package/dist/src/handlers/source-writer.d.ts.map +1 -0
  124. package/dist/src/html-processor.d.ts +63 -0
  125. package/dist/src/html-processor.d.ts.map +1 -0
  126. package/dist/src/index.d.ts +41 -0
  127. package/dist/src/index.d.ts.map +1 -0
  128. package/dist/src/manifest-writer.d.ts +111 -0
  129. package/dist/src/manifest-writer.d.ts.map +1 -0
  130. package/dist/src/media/contember.d.ts +15 -0
  131. package/dist/src/media/contember.d.ts.map +1 -0
  132. package/dist/src/media/local.d.ts +9 -0
  133. package/dist/src/media/local.d.ts.map +1 -0
  134. package/dist/src/media/s3.d.ts +12 -0
  135. package/dist/src/media/s3.d.ts.map +1 -0
  136. package/dist/src/media/types.d.ts +40 -0
  137. package/dist/src/media/types.d.ts.map +1 -0
  138. package/dist/src/preview-generator.d.ts +19 -0
  139. package/dist/src/preview-generator.d.ts.map +1 -0
  140. package/dist/src/seo-processor.d.ts +23 -0
  141. package/dist/src/seo-processor.d.ts.map +1 -0
  142. package/dist/src/source-finder/ast-extractors.d.ts +35 -0
  143. package/dist/src/source-finder/ast-extractors.d.ts.map +1 -0
  144. package/dist/src/source-finder/ast-parser.d.ts +16 -0
  145. package/dist/src/source-finder/ast-parser.d.ts.map +1 -0
  146. package/dist/src/source-finder/cache.d.ts +18 -0
  147. package/dist/src/source-finder/cache.d.ts.map +1 -0
  148. package/dist/src/source-finder/collection-finder.d.ts +29 -0
  149. package/dist/src/source-finder/collection-finder.d.ts.map +1 -0
  150. package/dist/src/source-finder/cross-file-tracker.d.ts +39 -0
  151. package/dist/src/source-finder/cross-file-tracker.d.ts.map +1 -0
  152. package/dist/src/source-finder/element-finder.d.ts +42 -0
  153. package/dist/src/source-finder/element-finder.d.ts.map +1 -0
  154. package/dist/src/source-finder/image-finder.d.ts +24 -0
  155. package/dist/src/source-finder/image-finder.d.ts.map +1 -0
  156. package/dist/src/source-finder/index.d.ts +9 -0
  157. package/dist/src/source-finder/index.d.ts.map +1 -0
  158. package/dist/src/source-finder/search-index.d.ts +27 -0
  159. package/dist/src/source-finder/search-index.d.ts.map +1 -0
  160. package/dist/src/source-finder/snippet-utils.d.ts +90 -0
  161. package/dist/src/source-finder/snippet-utils.d.ts.map +1 -0
  162. package/dist/src/source-finder/source-lookup.d.ts +16 -0
  163. package/dist/src/source-finder/source-lookup.d.ts.map +1 -0
  164. package/dist/src/source-finder/types.d.ts +167 -0
  165. package/dist/src/source-finder/types.d.ts.map +1 -0
  166. package/dist/src/source-finder/variable-extraction.d.ts +37 -0
  167. package/dist/src/source-finder/variable-extraction.d.ts.map +1 -0
  168. package/dist/src/tailwind-colors.d.ts +54 -0
  169. package/dist/src/tailwind-colors.d.ts.map +1 -0
  170. package/dist/src/tsconfig.tsbuildinfo +1 -0
  171. package/dist/src/types.d.ts +367 -0
  172. package/dist/src/types.d.ts.map +1 -0
  173. package/dist/src/utils.d.ts +61 -0
  174. package/dist/src/utils.d.ts.map +1 -0
  175. package/dist/src/vite-plugin.d.ts +14 -0
  176. package/dist/src/vite-plugin.d.ts.map +1 -0
  177. package/dist/types/tsconfig.tsbuildinfo +1 -0
  178. package/package.json +80 -0
  179. package/src/build-processor.ts +784 -0
  180. package/src/collection-scanner.ts +304 -0
  181. package/src/component-registry.ts +393 -0
  182. package/src/config.ts +74 -0
  183. package/src/dev-middleware.ts +525 -0
  184. package/src/dist/src/tsconfig.tsbuildinfo +1 -0
  185. package/src/editor/ai.ts +185 -0
  186. package/src/editor/api.ts +513 -0
  187. package/src/editor/color-utils.ts +556 -0
  188. package/src/editor/components/ai-chat.tsx +632 -0
  189. package/src/editor/components/ai-tooltip.tsx +179 -0
  190. package/src/editor/components/attribute-editor.tsx +596 -0
  191. package/src/editor/components/block-editor.tsx +546 -0
  192. package/src/editor/components/collections-browser.tsx +248 -0
  193. package/src/editor/components/color-toolbar.tsx +314 -0
  194. package/src/editor/components/confirm-dialog.tsx +69 -0
  195. package/src/editor/components/create-page-modal.tsx +163 -0
  196. package/src/editor/components/editable-highlights.tsx +260 -0
  197. package/src/editor/components/error-boundary.tsx +87 -0
  198. package/src/editor/components/fields.tsx +387 -0
  199. package/src/editor/components/frontmatter-fields.tsx +469 -0
  200. package/src/editor/components/highlight-overlay.ts +229 -0
  201. package/src/editor/components/image-overlay.tsx +230 -0
  202. package/src/editor/components/markdown-editor-overlay.tsx +505 -0
  203. package/src/editor/components/markdown-inline-editor.tsx +780 -0
  204. package/src/editor/components/media-library.tsx +297 -0
  205. package/src/editor/components/outline.tsx +402 -0
  206. package/src/editor/components/redirect-countdown.tsx +45 -0
  207. package/src/editor/components/seo-editor.tsx +498 -0
  208. package/src/editor/components/text-style-toolbar.tsx +362 -0
  209. package/src/editor/components/toast/toast-container.tsx +15 -0
  210. package/src/editor/components/toast/toast.tsx +49 -0
  211. package/src/editor/components/toast/types.ts +7 -0
  212. package/src/editor/components/toolbar.tsx +366 -0
  213. package/src/editor/config.ts +12 -0
  214. package/src/editor/constants.ts +106 -0
  215. package/src/editor/context.tsx +38 -0
  216. package/src/editor/dom.ts +357 -0
  217. package/src/editor/editor.ts +1510 -0
  218. package/src/editor/env.d.ts +4 -0
  219. package/src/editor/history.ts +355 -0
  220. package/src/editor/hooks/index.ts +19 -0
  221. package/src/editor/hooks/useAIHandlers.ts +345 -0
  222. package/src/editor/hooks/useBlockEditorHandlers.ts +206 -0
  223. package/src/editor/hooks/useElementDetection.ts +284 -0
  224. package/src/editor/hooks/useImageHoverDetection.ts +102 -0
  225. package/src/editor/hooks/useTextSelection.ts +187 -0
  226. package/src/editor/hooks/useTooltipState.ts +126 -0
  227. package/src/editor/hooks/utils.ts +101 -0
  228. package/src/editor/index.tsx +481 -0
  229. package/src/editor/lib/cn.ts +4 -0
  230. package/src/editor/manifest.ts +25 -0
  231. package/src/editor/markdown-api.ts +209 -0
  232. package/src/editor/signals.ts +1351 -0
  233. package/src/editor/storage.ts +266 -0
  234. package/src/editor/styles.css +465 -0
  235. package/src/editor/text-styling.ts +773 -0
  236. package/src/editor/themes.ts +210 -0
  237. package/src/editor/types.ts +591 -0
  238. package/src/error-collector.ts +106 -0
  239. package/src/handlers/component-ops.ts +463 -0
  240. package/src/handlers/markdown-ops.ts +202 -0
  241. package/src/handlers/request-utils.ts +151 -0
  242. package/src/handlers/source-writer.ts +649 -0
  243. package/src/html-processor.ts +1108 -0
  244. package/src/index.ts +284 -0
  245. package/src/manifest-writer.ts +371 -0
  246. package/src/media/contember.ts +84 -0
  247. package/src/media/local.ts +114 -0
  248. package/src/media/s3.ts +133 -0
  249. package/src/media/types.ts +33 -0
  250. package/src/preview-generator.ts +293 -0
  251. package/src/seo-processor.ts +567 -0
  252. package/src/source-finder/ast-extractors.ts +185 -0
  253. package/src/source-finder/ast-parser.ts +150 -0
  254. package/src/source-finder/cache.ts +76 -0
  255. package/src/source-finder/collection-finder.ts +335 -0
  256. package/src/source-finder/cross-file-tracker.ts +741 -0
  257. package/src/source-finder/element-finder.ts +387 -0
  258. package/src/source-finder/image-finder.ts +283 -0
  259. package/src/source-finder/index.ts +37 -0
  260. package/src/source-finder/search-index.ts +525 -0
  261. package/src/source-finder/snippet-utils.ts +668 -0
  262. package/src/source-finder/source-lookup.ts +200 -0
  263. package/src/source-finder/types.ts +210 -0
  264. package/src/source-finder/variable-extraction.ts +406 -0
  265. package/src/tailwind-colors.ts +874 -0
  266. package/src/tsconfig.json +25 -0
  267. package/src/types.ts +406 -0
  268. package/src/utils.ts +186 -0
  269. package/src/vite-plugin.ts +42 -0
@@ -0,0 +1,741 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+
4
+ import { getProjectRoot } from '../config'
5
+ import { buildDefinitionPath, parseExpressionPath } from './ast-extractors'
6
+ import { getCachedParsedFile } from './ast-parser'
7
+ import { findComponentProp, findExpressionProp, findSpreadProp } from './element-finder'
8
+ import { normalizeText } from './snippet-utils'
9
+ import type { ImportInfo, SourceLocation, VariableDefinition } from './types'
10
+ import { escapeRegex } from '../utils'
11
+ import { getExportedDefinitions, resolveImportPath } from './variable-extraction'
12
+
13
+ // ============================================================================
14
+ // Expression Prop Search
15
+ // ============================================================================
16
+
17
+ /**
18
+ * Search for a component usage with an expression prop across all files.
19
+ * When we find an expression like {items[0]} in a component where items comes from props,
20
+ * we search for where that component is used and track the expression prop back.
21
+ * Supports multi-level prop drilling with a depth limit.
22
+ *
23
+ * @param componentFileName - The file name of the component (e.g., 'Nav.astro')
24
+ * @param propName - The prop name we're looking for (e.g., 'items')
25
+ * @param expressionPath - The full expression path (e.g., 'items[0]')
26
+ * @param searchText - The text content we're searching for
27
+ * @param depth - Current recursion depth (default 0, max 5)
28
+ * @returns Source location if found
29
+ */
30
+ export async function searchForExpressionProp(
31
+ componentFileName: string,
32
+ propName: string,
33
+ expressionPath: string,
34
+ searchText: string,
35
+ depth: number = 0,
36
+ ): Promise<SourceLocation | undefined> {
37
+ // Limit recursion depth to prevent infinite loops
38
+ if (depth > 5) return undefined
39
+
40
+ const srcDir = path.join(getProjectRoot(), 'src')
41
+ const searchDirs = [
42
+ path.join(srcDir, 'pages'),
43
+ path.join(srcDir, 'components'),
44
+ path.join(srcDir, 'layouts'),
45
+ ]
46
+
47
+ // Extract the component name from file name (e.g., 'Nav.astro' -> 'Nav')
48
+ const componentName = path.basename(componentFileName, '.astro')
49
+ const normalizedSearch = normalizeText(searchText)
50
+
51
+ for (const dir of searchDirs) {
52
+ try {
53
+ const result = await searchDirForExpressionProp(
54
+ dir,
55
+ componentName,
56
+ propName,
57
+ expressionPath,
58
+ normalizedSearch,
59
+ searchText,
60
+ depth,
61
+ )
62
+ if (result) return result
63
+ } catch {
64
+ // Directory doesn't exist, continue
65
+ }
66
+ }
67
+
68
+ return undefined
69
+ }
70
+
71
+ async function searchDirForExpressionProp(
72
+ dir: string,
73
+ componentName: string,
74
+ propName: string,
75
+ expressionPath: string,
76
+ normalizedSearch: string,
77
+ searchText: string,
78
+ depth: number,
79
+ ): Promise<SourceLocation | undefined> {
80
+ try {
81
+ const entries = await fs.readdir(dir, { withFileTypes: true })
82
+
83
+ for (const entry of entries) {
84
+ const fullPath = path.join(dir, entry.name)
85
+
86
+ if (entry.isDirectory()) {
87
+ const result = await searchDirForExpressionProp(
88
+ fullPath,
89
+ componentName,
90
+ propName,
91
+ expressionPath,
92
+ normalizedSearch,
93
+ searchText,
94
+ depth,
95
+ )
96
+ if (result) return result
97
+ } else if (entry.isFile() && entry.name.endsWith('.astro')) {
98
+ const cached = await getCachedParsedFile(fullPath)
99
+ if (!cached) continue
100
+
101
+ // First, try to find expression prop usage: <Nav items={navItems} />
102
+ const exprPropMatch = findExpressionProp(cached.ast, componentName, propName)
103
+
104
+ if (exprPropMatch) {
105
+ // The expression text might be a simple variable like 'navItems'
106
+ 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
111
+ const parentPath = expressionPath.replace(/^[^.[]+/, exprText)
112
+
113
+ // Check if the value is in local variable definitions
114
+ for (const def of cached.variableDefinitions) {
115
+ const defPath = buildDefinitionPath(def)
116
+ if (defPath === parentPath) {
117
+ const normalizedDef = normalizeText(def.value)
118
+ if (normalizedDef === normalizedSearch) {
119
+ return {
120
+ file: path.relative(getProjectRoot(), fullPath),
121
+ line: def.line,
122
+ snippet: cached.lines[def.line - 1] || '',
123
+ type: 'variable',
124
+ variableName: defPath,
125
+ definitionLine: def.line,
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ // Check if exprText is itself from props (multi-level prop drilling)
132
+ const baseVar = exprText.match(/^(\w+)/)?.[1]
133
+ if (baseVar && cached.propAliases.has(baseVar)) {
134
+ const actualPropName = cached.propAliases.get(baseVar)!
135
+ // Recursively search for where this component is used
136
+ const result = await searchForExpressionProp(
137
+ entry.name,
138
+ actualPropName,
139
+ parentPath, // Use the path with the parent's variable name
140
+ searchText,
141
+ depth + 1,
142
+ )
143
+ if (result) return result
144
+ }
145
+
146
+ continue
147
+ }
148
+
149
+ // Second, try to find spread prop usage: <Card {...cardProps} />
150
+ const spreadMatch = findSpreadProp(cached.ast, componentName)
151
+
152
+ if (spreadMatch) {
153
+ // Find the spread variable's definition
154
+ const spreadVarName = spreadMatch.spreadVarName
155
+
156
+ // The propName we're looking for should be a property of the spread object
157
+ // e.g., if propName is 'title' and spread is {...cardProps},
158
+ // we look for cardProps.title in the definitions
159
+ const spreadPropPath = `${spreadVarName}.${propName}`
160
+
161
+ for (const def of cached.variableDefinitions) {
162
+ const defPath = buildDefinitionPath(def)
163
+ if (defPath === spreadPropPath) {
164
+ const normalizedDef = normalizeText(def.value)
165
+ if (normalizedDef === normalizedSearch) {
166
+ return {
167
+ file: path.relative(getProjectRoot(), fullPath),
168
+ line: def.line,
169
+ snippet: cached.lines[def.line - 1] || '',
170
+ type: 'variable',
171
+ variableName: defPath,
172
+ definitionLine: def.line,
173
+ }
174
+ }
175
+ }
176
+ }
177
+
178
+ // Check if the spread variable itself comes from props
179
+ if (cached.propAliases.has(spreadVarName)) {
180
+ const actualPropName = cached.propAliases.get(spreadVarName)!
181
+ // For spread from props, we need to search for the full path
182
+ const result = await searchForExpressionProp(
183
+ entry.name,
184
+ actualPropName,
185
+ expressionPath,
186
+ searchText,
187
+ depth + 1,
188
+ )
189
+ if (result) return result
190
+ }
191
+ }
192
+ }
193
+ }
194
+ } catch {
195
+ // Error reading directory
196
+ }
197
+
198
+ return undefined
199
+ }
200
+
201
+ // ============================================================================
202
+ // Imported Value Search
203
+ // ============================================================================
204
+
205
+ /**
206
+ * Search for a value in an imported file.
207
+ * @param fromFile - The file that contains the import
208
+ * @param importInfo - Information about the import
209
+ * @param expressionPath - The full expression path (e.g., 'config.title' or 'navItems[0]')
210
+ * @param searchText - The text content we're searching for
211
+ */
212
+ export async function searchForImportedValue(
213
+ fromFile: string,
214
+ importInfo: ImportInfo,
215
+ expressionPath: string,
216
+ searchText: string,
217
+ ): Promise<SourceLocation | undefined> {
218
+ // Resolve the import path to an absolute file path
219
+ const importedFilePath = await resolveImportPath(importInfo.source, fromFile)
220
+ if (!importedFilePath) return undefined
221
+
222
+ // Get exported definitions from the imported file
223
+ const exportedDefs = await getExportedDefinitions(importedFilePath)
224
+ if (exportedDefs.length === 0) return undefined
225
+
226
+ const normalizedSearch = normalizeText(searchText)
227
+
228
+ // Build the path we're looking for in the imported file
229
+ // e.g., if expressionPath is 'config.title' and localName is 'config',
230
+ // and importedName is 'siteConfig', we look for 'siteConfig.title'
231
+ let targetPath: string
232
+ if (importInfo.importedName === 'default' || importInfo.importedName === importInfo.localName) {
233
+ // Direct import: import { config } from './file' or import config from './file'
234
+ // The expression path uses the local name, which matches the exported name
235
+ targetPath = expressionPath
236
+ } else {
237
+ // Renamed import: import { config as siteConfig } from './file'
238
+ // Replace the local name with the original exported name
239
+ targetPath = expressionPath.replace(
240
+ new RegExp(`^${escapeRegex(importInfo.localName)}`),
241
+ importInfo.importedName,
242
+ )
243
+ }
244
+
245
+ // Search for the target path in the exported definitions
246
+ for (const def of exportedDefs) {
247
+ const defPath = buildDefinitionPath(def)
248
+ if (defPath === targetPath) {
249
+ const normalizedDef = normalizeText(def.value)
250
+ if (normalizedDef === normalizedSearch) {
251
+ const importedFileContent = await fs.readFile(importedFilePath, 'utf-8')
252
+ const importedLines = importedFileContent.split('\n')
253
+
254
+ return {
255
+ file: path.relative(getProjectRoot(), importedFilePath),
256
+ line: def.line,
257
+ snippet: importedLines[def.line - 1] || '',
258
+ type: 'variable',
259
+ variableName: defPath,
260
+ definitionLine: def.line,
261
+ }
262
+ }
263
+ }
264
+ }
265
+
266
+ return undefined
267
+ }
268
+
269
+ // ============================================================================
270
+ // Prop in Parents Search
271
+ // ============================================================================
272
+
273
+ /**
274
+ * Search for prop values passed to components using AST parsing.
275
+ * Uses caching for better performance.
276
+ */
277
+ export async function searchForPropInParents(dir: string, textContent: string): Promise<SourceLocation | undefined> {
278
+ const entries = await fs.readdir(dir, { withFileTypes: true })
279
+
280
+ for (const entry of entries) {
281
+ const fullPath = path.join(dir, entry.name)
282
+
283
+ if (entry.isDirectory()) {
284
+ const result = await searchForPropInParents(fullPath, textContent)
285
+ if (result) return result
286
+ } else if (entry.isFile() && entry.name.endsWith('.astro')) {
287
+ try {
288
+ // Use cached parsed file
289
+ const cached = await getCachedParsedFile(fullPath)
290
+ if (!cached) continue
291
+
292
+ const { lines, ast } = cached
293
+
294
+ // Find component props matching our text
295
+ const propMatch = findComponentProp(ast, textContent)
296
+
297
+ if (propMatch) {
298
+ // Extract component snippet for context
299
+ const componentStart = propMatch.line - 1
300
+ const snippetLines: string[] = []
301
+ let depth = 0
302
+
303
+ for (let i = componentStart; i < Math.min(componentStart + 10, lines.length); i++) {
304
+ const line = lines[i]
305
+ if (!line) continue
306
+ snippetLines.push(line)
307
+
308
+ // Check for self-closing or end of opening tag
309
+ if (line.includes('/>')) {
310
+ break
311
+ }
312
+ if (line.includes('>') && !line.includes('/>')) {
313
+ // Count opening tags
314
+ const opens = (line.match(/<[A-Z]/g) || []).length
315
+ const closes = (line.match(/\/>/g) || []).length
316
+ depth += opens - closes
317
+ if (depth <= 0 || (i > componentStart && line.includes('>'))) {
318
+ break
319
+ }
320
+ }
321
+ }
322
+
323
+ return {
324
+ file: path.relative(getProjectRoot(), fullPath),
325
+ line: propMatch.line,
326
+ snippet: snippetLines.join('\n'),
327
+ type: 'prop',
328
+ variableName: propMatch.propName,
329
+ }
330
+ }
331
+ } catch {
332
+ // Error parsing file, continue
333
+ }
334
+ }
335
+ }
336
+
337
+ return undefined
338
+ }
339
+
340
+ // ============================================================================
341
+ // Attribute Source Location Finding
342
+ // ============================================================================
343
+
344
+ /**
345
+ * Find the actual source location for a dynamic attribute value.
346
+ * Uses the resolved VALUE to search for where it's defined (handles loop variables, etc.)
347
+ *
348
+ * @param expression - The source expression (e.g., "component.githubUrl")
349
+ * @param resolvedValue - The actual resolved value from the rendered HTML
350
+ * @param sourceFilePath - The source file path where the attribute is used (relative to project root)
351
+ * @returns Source location with file, line, and snippet for the actual value definition
352
+ */
353
+ export async function findAttributeSourceLocation(
354
+ expression: string,
355
+ resolvedValue: string,
356
+ sourceFilePath: string,
357
+ ): Promise<SourceLocation | undefined> {
358
+ // Parse the expression to get property name (e.g., "githubUrl" from "component.githubUrl")
359
+ const exprPath = parseExpressionPath(expression)
360
+ if (!exprPath) return undefined
361
+
362
+ // Get the property name (last part of the expression)
363
+ const propName = exprPath.includes('.') ? exprPath.split('.').pop()! : exprPath
364
+
365
+ const filePath = path.isAbsolute(sourceFilePath)
366
+ ? sourceFilePath
367
+ : path.join(getProjectRoot(), sourceFilePath)
368
+
369
+ const cached = await getCachedParsedFile(filePath)
370
+ if (!cached) return undefined
371
+
372
+ // 1. Search local variable definitions by VALUE (handles loop variables)
373
+ // Look for definitions where: the property name matches AND the value matches
374
+ for (const def of cached.variableDefinitions) {
375
+ if (def.name === propName && def.value === resolvedValue) {
376
+ return {
377
+ file: path.relative(getProjectRoot(), filePath),
378
+ line: def.line,
379
+ snippet: cached.lines[def.line - 1] || '',
380
+ type: 'variable',
381
+ variableName: buildDefinitionPath(def),
382
+ definitionLine: def.line,
383
+ }
384
+ }
385
+ }
386
+
387
+ // 2. Search by exact expression path match
388
+ const baseVar = exprPath.match(/^(\w+)/)?.[1]
389
+ if (baseVar) {
390
+ for (const def of cached.variableDefinitions) {
391
+ const defPath = buildDefinitionPath(def)
392
+ if (defPath === exprPath && def.value === resolvedValue) {
393
+ return {
394
+ file: path.relative(getProjectRoot(), filePath),
395
+ line: def.line,
396
+ snippet: cached.lines[def.line - 1] || '',
397
+ type: 'variable',
398
+ variableName: defPath,
399
+ definitionLine: def.line,
400
+ }
401
+ }
402
+ }
403
+
404
+ // 3. Check if the base variable comes from props
405
+ const actualPropName = cached.propAliases.get(baseVar)
406
+ if (actualPropName) {
407
+ const componentFileName = path.basename(filePath)
408
+ const result = await searchForExpressionPropAttributeByValue(
409
+ componentFileName,
410
+ propName,
411
+ resolvedValue,
412
+ )
413
+ if (result) return result
414
+ }
415
+
416
+ // 4. Check if the base variable comes from an import
417
+ const importInfo = cached.imports.find((imp) => imp.localName === baseVar)
418
+ if (importInfo) {
419
+ const result = await searchForImportedAttributeByValue(
420
+ filePath,
421
+ importInfo,
422
+ propName,
423
+ resolvedValue,
424
+ )
425
+ if (result) return result
426
+ }
427
+ }
428
+
429
+ // 5. Fallback: search all variable definitions by value only
430
+ for (const def of cached.variableDefinitions) {
431
+ if (def.value === resolvedValue) {
432
+ return {
433
+ file: path.relative(getProjectRoot(), filePath),
434
+ line: def.line,
435
+ snippet: cached.lines[def.line - 1] || '',
436
+ type: 'variable',
437
+ variableName: buildDefinitionPath(def),
438
+ definitionLine: def.line,
439
+ }
440
+ }
441
+ }
442
+
443
+ return undefined
444
+ }
445
+
446
+ /**
447
+ * Search for attribute value in parent components by matching the resolved value.
448
+ */
449
+ async function searchForExpressionPropAttributeByValue(
450
+ componentFileName: string,
451
+ propName: string,
452
+ resolvedValue: string,
453
+ depth: number = 0,
454
+ ): Promise<SourceLocation | undefined> {
455
+ if (depth > 5) return undefined
456
+
457
+ const srcDir = path.join(getProjectRoot(), 'src')
458
+ const searchDirs = [
459
+ path.join(srcDir, 'pages'),
460
+ path.join(srcDir, 'components'),
461
+ path.join(srcDir, 'layouts'),
462
+ ]
463
+
464
+ for (const dir of searchDirs) {
465
+ try {
466
+ const result = await searchDirForAttributeByValue(dir, propName, resolvedValue, depth)
467
+ if (result) return result
468
+ } catch {
469
+ // Directory doesn't exist
470
+ }
471
+ }
472
+
473
+ return undefined
474
+ }
475
+
476
+ async function searchDirForAttributeByValue(
477
+ dir: string,
478
+ propName: string,
479
+ resolvedValue: string,
480
+ depth: number,
481
+ ): Promise<SourceLocation | undefined> {
482
+ try {
483
+ const entries = await fs.readdir(dir, { withFileTypes: true })
484
+
485
+ for (const entry of entries) {
486
+ const fullPath = path.join(dir, entry.name)
487
+
488
+ if (entry.isDirectory()) {
489
+ const result = await searchDirForAttributeByValue(fullPath, propName, resolvedValue, depth)
490
+ if (result) return result
491
+ } else if (entry.isFile() && entry.name.endsWith('.astro')) {
492
+ const cached = await getCachedParsedFile(fullPath)
493
+ if (!cached) continue
494
+
495
+ // Search for variable definitions matching propName and value
496
+ for (const def of cached.variableDefinitions) {
497
+ if (def.name === propName && def.value === resolvedValue) {
498
+ return {
499
+ file: path.relative(getProjectRoot(), fullPath),
500
+ line: def.line,
501
+ snippet: cached.lines[def.line - 1] || '',
502
+ type: 'variable',
503
+ variableName: buildDefinitionPath(def),
504
+ definitionLine: def.line,
505
+ }
506
+ }
507
+ }
508
+ }
509
+ }
510
+ } catch {
511
+ // Error reading directory
512
+ }
513
+
514
+ return undefined
515
+ }
516
+
517
+ /**
518
+ * Search for attribute value in imported files by matching the resolved value.
519
+ */
520
+ async function searchForImportedAttributeByValue(
521
+ fromFile: string,
522
+ importInfo: ImportInfo,
523
+ propName: string,
524
+ resolvedValue: string,
525
+ ): Promise<SourceLocation | undefined> {
526
+ const importedFilePath = await resolveImportPath(importInfo.source, fromFile)
527
+ if (!importedFilePath) return undefined
528
+
529
+ const exportedDefs = await getExportedDefinitions(importedFilePath)
530
+ if (exportedDefs.length === 0) return undefined
531
+
532
+ // Search for definitions matching propName and value
533
+ for (const def of exportedDefs) {
534
+ if (def.name === propName && def.value === resolvedValue) {
535
+ const importedFileContent = await fs.readFile(importedFilePath, 'utf-8')
536
+ const importedLines = importedFileContent.split('\n')
537
+
538
+ return {
539
+ file: path.relative(getProjectRoot(), importedFilePath),
540
+ line: def.line,
541
+ snippet: importedLines[def.line - 1] || '',
542
+ type: 'variable',
543
+ variableName: buildDefinitionPath(def),
544
+ definitionLine: def.line,
545
+ }
546
+ }
547
+ }
548
+
549
+ // Also try matching by value only as fallback
550
+ for (const def of exportedDefs) {
551
+ if (def.value === resolvedValue) {
552
+ const importedFileContent = await fs.readFile(importedFilePath, 'utf-8')
553
+ const importedLines = importedFileContent.split('\n')
554
+
555
+ return {
556
+ file: path.relative(getProjectRoot(), importedFilePath),
557
+ line: def.line,
558
+ snippet: importedLines[def.line - 1] || '',
559
+ type: 'variable',
560
+ variableName: buildDefinitionPath(def),
561
+ definitionLine: def.line,
562
+ }
563
+ }
564
+ }
565
+
566
+ return undefined
567
+ }
568
+
569
+ /**
570
+ * Search for attribute value in parent components via expression props.
571
+ * @deprecated Use searchForExpressionPropAttributeByValue instead
572
+ */
573
+ async function searchForExpressionPropAttribute(
574
+ componentFileName: string,
575
+ propName: string,
576
+ expressionPath: string,
577
+ depth: number = 0,
578
+ ): Promise<SourceLocation | undefined> {
579
+ if (depth > 5) return undefined
580
+
581
+ const srcDir = path.join(getProjectRoot(), 'src')
582
+ const searchDirs = [
583
+ path.join(srcDir, 'pages'),
584
+ path.join(srcDir, 'components'),
585
+ path.join(srcDir, 'layouts'),
586
+ ]
587
+
588
+ const componentName = path.basename(componentFileName, '.astro')
589
+
590
+ for (const dir of searchDirs) {
591
+ try {
592
+ const result = await searchDirForAttributeProp(
593
+ dir,
594
+ componentName,
595
+ propName,
596
+ expressionPath,
597
+ depth,
598
+ )
599
+ if (result) return result
600
+ } catch {
601
+ // Directory doesn't exist
602
+ }
603
+ }
604
+
605
+ return undefined
606
+ }
607
+
608
+ async function searchDirForAttributeProp(
609
+ dir: string,
610
+ componentName: string,
611
+ propName: string,
612
+ expressionPath: string,
613
+ depth: number,
614
+ ): Promise<SourceLocation | undefined> {
615
+ try {
616
+ const entries = await fs.readdir(dir, { withFileTypes: true })
617
+
618
+ for (const entry of entries) {
619
+ const fullPath = path.join(dir, entry.name)
620
+
621
+ if (entry.isDirectory()) {
622
+ const result = await searchDirForAttributeProp(
623
+ fullPath,
624
+ componentName,
625
+ propName,
626
+ expressionPath,
627
+ depth,
628
+ )
629
+ if (result) return result
630
+ } else if (entry.isFile() && entry.name.endsWith('.astro')) {
631
+ const cached = await getCachedParsedFile(fullPath)
632
+ if (!cached) continue
633
+
634
+ // Find expression prop usage: <Component prop={variable} />
635
+ const exprPropMatch = findExpressionProp(cached.ast, componentName, propName)
636
+
637
+ if (exprPropMatch) {
638
+ const exprText = exprPropMatch.expressionText
639
+ // Build the path in the parent's context
640
+ const parentPath = expressionPath.replace(/^[^.[]+/, exprText)
641
+
642
+ // Check local variable definitions
643
+ for (const def of cached.variableDefinitions) {
644
+ const defPath = buildDefinitionPath(def)
645
+ if (defPath === parentPath) {
646
+ return {
647
+ file: path.relative(getProjectRoot(), fullPath),
648
+ line: def.line,
649
+ snippet: cached.lines[def.line - 1] || '',
650
+ type: 'variable',
651
+ variableName: defPath,
652
+ definitionLine: def.line,
653
+ }
654
+ }
655
+ }
656
+
657
+ // Check if exprText is from props (multi-level drilling)
658
+ const baseVar = exprText.match(/^(\w+)/)?.[1]
659
+ if (baseVar && cached.propAliases.has(baseVar)) {
660
+ const actualPropName = cached.propAliases.get(baseVar)!
661
+ const result = await searchForExpressionPropAttribute(
662
+ entry.name,
663
+ actualPropName,
664
+ parentPath,
665
+ depth + 1,
666
+ )
667
+ if (result) return result
668
+ }
669
+ }
670
+
671
+ // Try spread prop usage
672
+ const spreadMatch = findSpreadProp(cached.ast, componentName)
673
+ if (spreadMatch) {
674
+ const spreadPropPath = `${spreadMatch.spreadVarName}.${propName}`
675
+ for (const def of cached.variableDefinitions) {
676
+ const defPath = buildDefinitionPath(def)
677
+ if (defPath === spreadPropPath) {
678
+ return {
679
+ file: path.relative(getProjectRoot(), fullPath),
680
+ line: def.line,
681
+ snippet: cached.lines[def.line - 1] || '',
682
+ type: 'variable',
683
+ variableName: defPath,
684
+ definitionLine: def.line,
685
+ }
686
+ }
687
+ }
688
+ }
689
+ }
690
+ }
691
+ } catch {
692
+ // Error reading directory
693
+ }
694
+
695
+ return undefined
696
+ }
697
+
698
+ /**
699
+ * Search for attribute value in an imported file.
700
+ */
701
+ async function searchForImportedAttribute(
702
+ fromFile: string,
703
+ importInfo: ImportInfo,
704
+ expressionPath: string,
705
+ ): Promise<SourceLocation | undefined> {
706
+ const importedFilePath = await resolveImportPath(importInfo.source, fromFile)
707
+ if (!importedFilePath) return undefined
708
+
709
+ const exportedDefs = await getExportedDefinitions(importedFilePath)
710
+ if (exportedDefs.length === 0) return undefined
711
+
712
+ // Build the target path in the imported file
713
+ let targetPath: string
714
+ if (importInfo.importedName === 'default' || importInfo.importedName === importInfo.localName) {
715
+ targetPath = expressionPath
716
+ } else {
717
+ targetPath = expressionPath.replace(
718
+ new RegExp(`^${escapeRegex(importInfo.localName)}`),
719
+ importInfo.importedName,
720
+ )
721
+ }
722
+
723
+ for (const def of exportedDefs) {
724
+ const defPath = buildDefinitionPath(def)
725
+ if (defPath === targetPath) {
726
+ const importedFileContent = await fs.readFile(importedFilePath, 'utf-8')
727
+ const importedLines = importedFileContent.split('\n')
728
+
729
+ return {
730
+ file: path.relative(getProjectRoot(), importedFilePath),
731
+ line: def.line,
732
+ snippet: importedLines[def.line - 1] || '',
733
+ type: 'variable',
734
+ variableName: defPath,
735
+ definitionLine: def.line,
736
+ }
737
+ }
738
+ }
739
+
740
+ return undefined
741
+ }