@sudodevstudio/astro-ai 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 (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +114 -0
  3. package/dist/agent/agent-fallback.d.ts +50 -0
  4. package/dist/agent/agent-fallback.d.ts.map +1 -0
  5. package/dist/agent/agent-fallback.js +31 -0
  6. package/dist/agent/agent-fallback.js.map +1 -0
  7. package/dist/agent/cli-agent-fallback.d.ts +79 -0
  8. package/dist/agent/cli-agent-fallback.d.ts.map +1 -0
  9. package/dist/agent/cli-agent-fallback.js +1056 -0
  10. package/dist/agent/cli-agent-fallback.js.map +1 -0
  11. package/dist/integration/index.d.ts +28 -0
  12. package/dist/integration/index.d.ts.map +1 -0
  13. package/dist/integration/index.js +389 -0
  14. package/dist/integration/index.js.map +1 -0
  15. package/dist/resolver/astro-resolver.d.ts +28 -0
  16. package/dist/resolver/astro-resolver.d.ts.map +1 -0
  17. package/dist/resolver/astro-resolver.js +835 -0
  18. package/dist/resolver/astro-resolver.js.map +1 -0
  19. package/dist/resolver/types.d.ts +39 -0
  20. package/dist/resolver/types.d.ts.map +1 -0
  21. package/dist/resolver/types.js +2 -0
  22. package/dist/resolver/types.js.map +1 -0
  23. package/dist/shared/protocol.d.ts +150 -0
  24. package/dist/shared/protocol.d.ts.map +1 -0
  25. package/dist/shared/protocol.js +28 -0
  26. package/dist/shared/protocol.js.map +1 -0
  27. package/dist/shared/selection-context.d.ts +75 -0
  28. package/dist/shared/selection-context.d.ts.map +1 -0
  29. package/dist/shared/selection-context.js +2 -0
  30. package/dist/shared/selection-context.js.map +1 -0
  31. package/dist/shared/visual-components.d.ts +15 -0
  32. package/dist/shared/visual-components.d.ts.map +1 -0
  33. package/dist/shared/visual-components.js +2 -0
  34. package/dist/shared/visual-components.js.map +1 -0
  35. package/dist/toolbar/action-model.d.ts +20 -0
  36. package/dist/toolbar/action-model.d.ts.map +1 -0
  37. package/dist/toolbar/action-model.js +49 -0
  38. package/dist/toolbar/action-model.js.map +1 -0
  39. package/dist/toolbar/app.d.ts +3 -0
  40. package/dist/toolbar/app.d.ts.map +1 -0
  41. package/dist/toolbar/app.js +274 -0
  42. package/dist/toolbar/app.js.map +1 -0
  43. package/dist/toolbar/chat-drawer.d.ts +111 -0
  44. package/dist/toolbar/chat-drawer.d.ts.map +1 -0
  45. package/dist/toolbar/chat-drawer.js +1575 -0
  46. package/dist/toolbar/chat-drawer.js.map +1 -0
  47. package/dist/toolbar/chat-windows.d.ts +62 -0
  48. package/dist/toolbar/chat-windows.d.ts.map +1 -0
  49. package/dist/toolbar/chat-windows.js +287 -0
  50. package/dist/toolbar/chat-windows.js.map +1 -0
  51. package/dist/toolbar/contextual-actions.d.ts +24 -0
  52. package/dist/toolbar/contextual-actions.d.ts.map +1 -0
  53. package/dist/toolbar/contextual-actions.js +371 -0
  54. package/dist/toolbar/contextual-actions.js.map +1 -0
  55. package/dist/toolbar/diagnostic-actions.d.ts +22 -0
  56. package/dist/toolbar/diagnostic-actions.d.ts.map +1 -0
  57. package/dist/toolbar/diagnostic-actions.js +127 -0
  58. package/dist/toolbar/diagnostic-actions.js.map +1 -0
  59. package/dist/toolbar/layers.d.ts +26 -0
  60. package/dist/toolbar/layers.d.ts.map +1 -0
  61. package/dist/toolbar/layers.js +31 -0
  62. package/dist/toolbar/layers.js.map +1 -0
  63. package/dist/toolbar/overlay.d.ts +40 -0
  64. package/dist/toolbar/overlay.d.ts.map +1 -0
  65. package/dist/toolbar/overlay.js +619 -0
  66. package/dist/toolbar/overlay.js.map +1 -0
  67. package/dist/visual/capability-resolver.d.ts +11 -0
  68. package/dist/visual/capability-resolver.d.ts.map +1 -0
  69. package/dist/visual/capability-resolver.js +69 -0
  70. package/dist/visual/capability-resolver.js.map +1 -0
  71. package/dist/visual/command-engine.d.ts +10 -0
  72. package/dist/visual/command-engine.d.ts.map +1 -0
  73. package/dist/visual/command-engine.js +199 -0
  74. package/dist/visual/command-engine.js.map +1 -0
  75. package/dist/visual/commands.d.ts +36 -0
  76. package/dist/visual/commands.d.ts.map +1 -0
  77. package/dist/visual/commands.js +2 -0
  78. package/dist/visual/commands.js.map +1 -0
  79. package/dist/visual/patch-transactions.d.ts +57 -0
  80. package/dist/visual/patch-transactions.d.ts.map +1 -0
  81. package/dist/visual/patch-transactions.js +259 -0
  82. package/dist/visual/patch-transactions.js.map +1 -0
  83. package/dist/vite/build-ai-plugin.d.ts +11 -0
  84. package/dist/vite/build-ai-plugin.d.ts.map +1 -0
  85. package/dist/vite/build-ai-plugin.js +45 -0
  86. package/dist/vite/build-ai-plugin.js.map +1 -0
  87. package/package.json +85 -0
  88. package/src/agent/agent-fallback.ts +95 -0
  89. package/src/agent/cli-agent-fallback.ts +1382 -0
  90. package/src/integration/index.ts +475 -0
  91. package/src/resolver/astro-resolver.ts +1024 -0
  92. package/src/resolver/types.ts +47 -0
  93. package/src/shared/protocol.ts +181 -0
  94. package/src/shared/selection-context.ts +91 -0
  95. package/src/shared/visual-components.ts +16 -0
  96. package/src/toolbar/action-model.ts +67 -0
  97. package/src/toolbar/app.ts +294 -0
  98. package/src/toolbar/chat-drawer.ts +1742 -0
  99. package/src/toolbar/chat-windows.ts +331 -0
  100. package/src/toolbar/contextual-actions.ts +401 -0
  101. package/src/toolbar/diagnostic-actions.ts +151 -0
  102. package/src/toolbar/layers.ts +32 -0
  103. package/src/toolbar/overlay.ts +638 -0
  104. package/src/visual/capability-resolver.ts +83 -0
  105. package/src/visual/command-engine.ts +250 -0
  106. package/src/visual/commands.ts +37 -0
  107. package/src/visual/patch-transactions.ts +270 -0
  108. package/src/vite/build-ai-plugin.ts +46 -0
@@ -0,0 +1,835 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { extname, relative, resolve, sep } from 'node:path';
3
+ import { parse as parseAstro } from '@astrojs/compiler-rs';
4
+ import { parse as parseJavaScript } from '@babel/parser';
5
+ import MagicString from 'magic-string';
6
+ import { VisualCapabilityResolver } from '../visual/capability-resolver.js';
7
+ const NODE_ATTRIBUTE = 'data-astro-ai-id';
8
+ const NAME_ATTRIBUTE = 'data-astro-ai-name';
9
+ const SOURCE_ATTRIBUTE = 'data-astro-ai-source';
10
+ const RESERVED_ATTRIBUTES = new Set([NODE_ATTRIBUTE, NAME_ATTRIBUTE, SOURCE_ATTRIBUTE]);
11
+ const NON_RENDERED_TAGS = new Set(['script', 'style']);
12
+ export class AstroResolver {
13
+ #projectRoot;
14
+ #capabilities;
15
+ #nodes = new Map();
16
+ #nodesByFile = new Map();
17
+ #indexCache = new Map();
18
+ #skillFiles;
19
+ #sourceLengths = new Map();
20
+ constructor(projectRoot, capabilities = new VisualCapabilityResolver(), skillFiles = []) {
21
+ this.#projectRoot = resolve(projectRoot);
22
+ this.#capabilities = capabilities;
23
+ this.#skillFiles = [...skillFiles];
24
+ }
25
+ get projectRoot() {
26
+ return this.#projectRoot;
27
+ }
28
+ ownsFile(filePath) {
29
+ try {
30
+ this.toProjectFile(filePath);
31
+ return true;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
37
+ instrumentFile(filePath, source) {
38
+ const nodes = this.indexFile(filePath, source);
39
+ const output = new MagicString(source);
40
+ for (const node of nodes) {
41
+ if (!node.instrumentable)
42
+ continue;
43
+ const insertionPoint = findAttributeInsertionPoint(source, node.openingRange);
44
+ const name = node.componentName ?? node.tagName ?? 'Astro node';
45
+ const sourceHint = `${node.source.file}:${node.source.start.line}`;
46
+ output.appendLeft(insertionPoint, ` ${NODE_ATTRIBUTE}="${escapeAttribute(node.nodeId)}"` +
47
+ ` ${NAME_ATTRIBUTE}="${escapeAttribute(name)}"` +
48
+ ` ${SOURCE_ATTRIBUTE}="${escapeAttribute(sourceHint)}"`);
49
+ }
50
+ return {
51
+ code: output.toString(),
52
+ map: output.generateMap({
53
+ source: this.toProjectPath(filePath),
54
+ includeContent: true,
55
+ hires: true,
56
+ }),
57
+ };
58
+ }
59
+ indexFile(filePath, source) {
60
+ const absoluteFile = this.toProjectFile(filePath);
61
+ const projectFile = this.toProjectPath(absoluteFile);
62
+ const sourceHash = hash(source);
63
+ const cached = this.#indexCache.get(absoluteFile);
64
+ if (cached?.sourceHash === sourceHash)
65
+ return cached.nodes;
66
+ const sourceLanguage = getSourceLanguage(absoluteFile);
67
+ const parsed = parseSource(source, sourceLanguage, projectFile);
68
+ this.#clearFileNodes(absoluteFile);
69
+ const lineStarts = getLineStarts(source);
70
+ const declarations = collectDeclarations(parsed.declarationProgram, source, projectFile, lineStarts);
71
+ const nodes = [];
72
+ const siblingGroups = new Map();
73
+ const structuralOccurrences = new Map();
74
+ const walk = (value, state) => {
75
+ if (Array.isArray(value)) {
76
+ for (const item of value)
77
+ walk(item, { ...state, siblingGroupId: undefined });
78
+ return;
79
+ }
80
+ if (!isAstNode(value))
81
+ return;
82
+ if (value.type === 'JSXElement') {
83
+ const opening = value.openingElement;
84
+ const name = getJsxName(opening?.name);
85
+ if (opening === undefined ||
86
+ name === undefined ||
87
+ value.start === undefined ||
88
+ value.end === undefined ||
89
+ opening.start === undefined ||
90
+ opening.end === undefined) {
91
+ return;
92
+ }
93
+ const component = isComponentName(name);
94
+ const hydratedIsland = sourceLanguage === 'astro' &&
95
+ component &&
96
+ hasHydrationDirective(opening.attributes);
97
+ const location = toLocation(projectFile, value.start, value.end, lineStarts);
98
+ const structuralKey = `${state.structuralPath}/${name}`;
99
+ const occurrence = structuralOccurrences.get(structuralKey) ?? 0;
100
+ structuralOccurrences.set(structuralKey, occurrence + 1);
101
+ const structuralPath = `${structuralKey}[${occurrence}]`;
102
+ const nodeId = createNodeId(projectFile, structuralPath);
103
+ const content = analyzeContent(value, source, projectFile, declarations, lineStarts, state.repeatContext, sourceLanguage);
104
+ const literalProps = collectLiteralProps(opening.attributes);
105
+ const attributes = Array.isArray(opening.attributes) ? opening.attributes : [];
106
+ const hasSpread = attributes.some((attribute) => attribute?.type === 'JSXSpreadAttribute');
107
+ const hasReservedAttribute = attributes.some((attribute) => RESERVED_ATTRIBUTES.has(getJsxName(attribute?.name) ?? ''));
108
+ const nativeTag = component ? undefined : name;
109
+ const instrumentable = !hasReservedAttribute &&
110
+ !NON_RENDERED_TAGS.has(name) &&
111
+ name !== 'Fragment' &&
112
+ // React component calls are not DOM nodes and may not forward data
113
+ // attributes. Their native JSX output is instrumented instead.
114
+ (sourceLanguage === 'astro' || !component) &&
115
+ // A native root with a spread can deliberately forward a component's
116
+ // selection id. Appending another id would overwrite that provenance.
117
+ !(nativeTag !== undefined && hasSpread);
118
+ const record = {
119
+ nodeId,
120
+ filePath: absoluteFile,
121
+ sourceLanguage,
122
+ sourceHash,
123
+ structuralPath,
124
+ source: location,
125
+ range: { start: value.start, end: value.end },
126
+ openingRange: { start: opening.start, end: opening.end },
127
+ ...(content.textRange === undefined ? {} : { textRange: content.textRange }),
128
+ ...(content.textValue === undefined ? {} : { textValue: content.textValue }),
129
+ ...(component ? { componentName: name } : { tagName: name }),
130
+ ...(state.parentNodeId === undefined ? {} : { parentNodeId: state.parentNodeId }),
131
+ ...(state.siblingGroupId === undefined
132
+ ? {}
133
+ : { siblingGroupId: state.siblingGroupId }),
134
+ parentComponents: state.parentComponents,
135
+ literalProps,
136
+ sourceKind: hydratedIsland ? 'hydrated-island' : component ? 'component' : content.sourceKind,
137
+ dataProvenance: content.dataProvenance,
138
+ ...(state.repeatContext === undefined
139
+ ? {}
140
+ : { repeatContext: state.repeatContext }),
141
+ hydratedIsland,
142
+ instrumentable,
143
+ selfClosing: opening.selfClosing === true,
144
+ };
145
+ nodes.push(record);
146
+ this.#nodes.set(nodeId, record);
147
+ if (state.siblingGroupId !== undefined) {
148
+ const group = siblingGroups.get(state.siblingGroupId) ?? [];
149
+ group.push(record);
150
+ siblingGroups.set(state.siblingGroupId, group);
151
+ }
152
+ const nextParents = component
153
+ ? [
154
+ ...state.parentComponents,
155
+ { name, source: location },
156
+ ]
157
+ : state.parentComponents;
158
+ const childGroupId = `${nodeId}:children`;
159
+ const children = Array.isArray(value.children) ? value.children : [];
160
+ for (const child of children) {
161
+ walk(child, {
162
+ parentNodeId: nodeId,
163
+ parentComponents: nextParents,
164
+ structuralPath,
165
+ ...(state.repeatContext === undefined
166
+ ? {}
167
+ : { repeatContext: state.repeatContext }),
168
+ ...(child?.type === 'JSXElement'
169
+ ? { siblingGroupId: childGroupId }
170
+ : {}),
171
+ });
172
+ }
173
+ return;
174
+ }
175
+ if (value.type === 'JSXFragment') {
176
+ const fragmentId = `fragment:${value.start ?? 0}`;
177
+ const children = Array.isArray(value.children) ? value.children : [];
178
+ for (const child of children) {
179
+ walk(child, {
180
+ ...state,
181
+ structuralPath: `${state.structuralPath}/fragment`,
182
+ ...(child?.type === 'JSXElement'
183
+ ? { siblingGroupId: fragmentId }
184
+ : { siblingGroupId: undefined }),
185
+ });
186
+ }
187
+ return;
188
+ }
189
+ const componentDefinition = sourceLanguage === 'astro'
190
+ ? undefined
191
+ : getReactComponentDefinition(value, projectFile, lineStarts);
192
+ if (componentDefinition !== undefined) {
193
+ for (const [key, child] of Object.entries(value)) {
194
+ if (key === 'start' || key === 'end' || key === 'type')
195
+ continue;
196
+ walk(child, {
197
+ ...state,
198
+ parentComponents: [...state.parentComponents, componentDefinition],
199
+ structuralPath: `${state.structuralPath}/component:${componentDefinition.name}`,
200
+ });
201
+ }
202
+ return;
203
+ }
204
+ const nextRepeat = isMapCall(value)
205
+ ? {
206
+ kind: 'map',
207
+ description: 'Rendered by a .map() template; edits affect every rendered instance.',
208
+ source: toLocation(projectFile, value.start ?? 0, value.end ?? value.start ?? 0, lineStarts),
209
+ affectsAllInstances: true,
210
+ }
211
+ : state.repeatContext;
212
+ for (const [key, child] of Object.entries(value)) {
213
+ if (key === 'start' || key === 'end' || key === 'type')
214
+ continue;
215
+ walk(child, {
216
+ parentNodeId: state.parentNodeId,
217
+ parentComponents: state.parentComponents,
218
+ structuralPath: state.structuralPath,
219
+ ...(nextRepeat === undefined ? {} : { repeatContext: nextRepeat }),
220
+ });
221
+ }
222
+ };
223
+ walk(parsed.templateRoot, { parentComponents: [], structuralPath: 'root' });
224
+ assignSafeSiblings(siblingGroups, source);
225
+ this.#nodesByFile.set(absoluteFile, new Set(nodes.map(({ nodeId }) => nodeId)));
226
+ this.#indexCache.set(absoluteFile, { sourceHash, nodes });
227
+ this.#sourceLengths.set(absoluteFile, source.length);
228
+ return nodes;
229
+ }
230
+ resolveSelection(nodeId, route) {
231
+ const node = this.requireNode(nodeId);
232
+ const capabilities = this.#capabilities.resolve(node);
233
+ return {
234
+ route,
235
+ selectedNode: {
236
+ nodeId,
237
+ ...(node.tagName === undefined ? {} : { tagName: node.tagName }),
238
+ ...(node.componentName === undefined
239
+ ? {}
240
+ : { componentName: node.componentName }),
241
+ ...(node.textValue === undefined ? {} : { literalText: node.textValue }),
242
+ source: node.source,
243
+ },
244
+ parentComponents: node.parentComponents,
245
+ capabilities,
246
+ relevantFiles: [...new Set([
247
+ node.source.file,
248
+ ...(node.dataProvenance.declaredAt === undefined ? [] : [node.dataProvenance.declaredAt.file]),
249
+ ...(node.dataProvenance.sourceFile === undefined ? [] : [node.dataProvenance.sourceFile]),
250
+ ])],
251
+ skillFiles: [...this.#skillFiles],
252
+ };
253
+ }
254
+ requireNode(nodeId) {
255
+ const node = this.#nodes.get(nodeId);
256
+ if (node === undefined)
257
+ throw new Error('The selected source node is no longer available.');
258
+ return node;
259
+ }
260
+ listNodes(filePath) {
261
+ if (filePath === undefined)
262
+ return [...this.#nodes.values()];
263
+ const absoluteFile = this.toProjectFile(filePath);
264
+ const ids = this.#nodesByFile.get(absoluteFile);
265
+ return ids === undefined
266
+ ? []
267
+ : [...ids].flatMap((id) => {
268
+ const node = this.#nodes.get(id);
269
+ return node === undefined ? [] : [node];
270
+ });
271
+ }
272
+ findInsertionPoints(filePath) {
273
+ const absoluteFile = this.toProjectFile(filePath);
274
+ const projectFile = this.toProjectPath(absoluteFile);
275
+ const zones = [{
276
+ id: `${projectFile}:root`,
277
+ file: projectFile,
278
+ offset: this.#sourceLengths.get(absoluteFile) ?? 0,
279
+ acceptedChildTypes: ['*'],
280
+ }];
281
+ for (const node of this.listNodes(absoluteFile)) {
282
+ if (node.selfClosing || node.hydratedIsland || node.componentName === undefined)
283
+ continue;
284
+ for (const slot of this.#capabilities.slotsFor(node.componentName)) {
285
+ zones.push({
286
+ id: `${node.nodeId}:slot:${slot.name}`,
287
+ file: projectFile,
288
+ offset: node.range.end - `</${node.componentName}>`.length,
289
+ parentNodeId: node.nodeId,
290
+ slot: slot.name,
291
+ acceptedChildTypes: slot.accepts ?? ['*'],
292
+ });
293
+ }
294
+ }
295
+ return zones;
296
+ }
297
+ findInsertionPointsForRoute(route) {
298
+ const pathname = route.split(/[?#]/, 1)[0] ?? '/';
299
+ const segments = pathname.split('/').filter(Boolean);
300
+ const stem = segments.length === 0 ? 'index' : segments.join('/');
301
+ const candidates = new Set([
302
+ `src/pages/${stem}.astro`,
303
+ `src/pages/${stem}/index.astro`,
304
+ ]);
305
+ for (const file of this.#sourceLengths.keys()) {
306
+ if (candidates.has(this.toProjectPath(file)))
307
+ return this.findInsertionPoints(file);
308
+ }
309
+ return [];
310
+ }
311
+ acceptsSlot(parentComponent, slot, childType) {
312
+ return this.#capabilities.acceptsSlot(parentComponent, slot, childType);
313
+ }
314
+ removeFile(filePath) {
315
+ const absoluteFile = this.toProjectFile(filePath);
316
+ this.#clearFileNodes(absoluteFile);
317
+ this.#indexCache.delete(absoluteFile);
318
+ this.#sourceLengths.delete(absoluteFile);
319
+ }
320
+ #clearFileNodes(absoluteFile) {
321
+ const ids = this.#nodesByFile.get(absoluteFile);
322
+ if (ids !== undefined) {
323
+ for (const id of ids)
324
+ this.#nodes.delete(id);
325
+ }
326
+ this.#nodesByFile.delete(absoluteFile);
327
+ }
328
+ toProjectPath(filePath) {
329
+ const absoluteFile = this.toProjectFile(filePath);
330
+ return relative(this.#projectRoot, absoluteFile).split(sep).join('/');
331
+ }
332
+ toAbsoluteProjectFile(projectFile) {
333
+ return this.toProjectFile(resolve(this.#projectRoot, projectFile));
334
+ }
335
+ toProjectFile(filePath) {
336
+ const absoluteFile = resolve(filePath);
337
+ const projectRelative = relative(this.#projectRoot, absoluteFile);
338
+ if (projectRelative === '..' ||
339
+ projectRelative.startsWith(`..${sep}`) ||
340
+ resolve(this.#projectRoot, projectRelative) !== absoluteFile) {
341
+ throw new Error('Source access outside the Astro project root is not allowed.');
342
+ }
343
+ return absoluteFile;
344
+ }
345
+ }
346
+ function getSourceLanguage(filePath) {
347
+ const extension = extname(filePath).toLowerCase();
348
+ if (extension === '.astro')
349
+ return 'astro';
350
+ if (extension === '.tsx' || extension === '.ts')
351
+ return 'tsx';
352
+ if (extension === '.jsx' || extension === '.js')
353
+ return 'jsx';
354
+ throw new Error(`Unsupported source file type: ${extension || '(none)'}.`);
355
+ }
356
+ function parseSource(source, language, projectFile) {
357
+ if (language === 'astro') {
358
+ const parsed = parseAstro(source);
359
+ const error = parsed.diagnostics.find((diagnostic) => diagnostic.severity === 'error');
360
+ if (error !== undefined)
361
+ throw new Error(`Cannot parse ${projectFile}: ${error.text}`);
362
+ return {
363
+ templateRoot: parsed.ast.body,
364
+ declarationProgram: parsed.ast.frontmatter?.program,
365
+ };
366
+ }
367
+ try {
368
+ const parsed = parseJavaScript(source, {
369
+ sourceType: 'unambiguous',
370
+ plugins: language === 'tsx' ? ['jsx', 'typescript'] : ['jsx'],
371
+ });
372
+ return {
373
+ templateRoot: parsed.program,
374
+ declarationProgram: parsed.program,
375
+ };
376
+ }
377
+ catch (error) {
378
+ const detail = error instanceof Error ? error.message : 'Unknown parser error.';
379
+ throw new Error(`Cannot parse ${projectFile}: ${detail}`);
380
+ }
381
+ }
382
+ function getReactComponentDefinition(node, projectFile, lineStarts) {
383
+ let name;
384
+ if ((node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') &&
385
+ typeof node.id?.name === 'string') {
386
+ name = node.id.name;
387
+ }
388
+ else if (node.type === 'VariableDeclarator' &&
389
+ typeof node.id?.name === 'string' &&
390
+ (node.init?.type === 'ArrowFunctionExpression' || node.init?.type === 'FunctionExpression')) {
391
+ name = node.id.name;
392
+ }
393
+ if (name === undefined || !isComponentName(name))
394
+ return undefined;
395
+ const source = nodeLocation(node, projectFile, lineStarts);
396
+ return { name, ...(source === undefined ? {} : { source }) };
397
+ }
398
+ function analyzeContent(node, source, projectFile, declarations, lineStarts, repeatContext, sourceLanguage) {
399
+ const children = Array.isArray(node.children) ? node.children : [];
400
+ const meaningful = children.filter((child) => child?.type !== 'JSXText' || String(child.raw ?? child.value ?? '').trim() !== '');
401
+ if (meaningful.length === 1 &&
402
+ meaningful[0]?.type === 'JSXText' &&
403
+ meaningful[0].start !== undefined &&
404
+ meaningful[0].end !== undefined) {
405
+ const raw = source.slice(meaningful[0].start, meaningful[0].end);
406
+ return {
407
+ textRange: { start: meaningful[0].start, end: meaningful[0].end },
408
+ textValue: raw.trim(),
409
+ sourceKind: repeatContext === undefined ? 'literal-source' : 'repeated-template',
410
+ dataProvenance: {
411
+ kind: 'literal',
412
+ description: repeatContext === undefined
413
+ ? `Literal text in this ${sourceLanguage === 'astro' ? 'Astro' : 'React'} template.`
414
+ : `Literal text in a repeated ${sourceLanguage === 'astro' ? 'Astro' : 'React'} template.`,
415
+ readOnly: false,
416
+ },
417
+ };
418
+ }
419
+ if (meaningful.length === 1 && meaningful[0]?.type === 'JSXExpressionContainer') {
420
+ return classifyExpression(meaningful[0].expression, source, declarations, repeatContext);
421
+ }
422
+ return {
423
+ sourceKind: repeatContext === undefined ? 'generated-unknown' : 'repeated-template',
424
+ dataProvenance: {
425
+ kind: 'unknown',
426
+ description: repeatContext === undefined
427
+ ? 'Mixed or generated content; use source navigation or the agent fallback.'
428
+ : 'Generated inside a repeated template; edits may affect every instance.',
429
+ readOnly: true,
430
+ },
431
+ };
432
+ }
433
+ function classifyExpression(expression, source, declarations, repeatContext) {
434
+ const symbol = getRootSymbol(expression);
435
+ const declaration = symbol === undefined ? undefined : declarations.get(symbol);
436
+ if (isAstroPropsMember(expression)) {
437
+ const name = getJsxName(expression?.property) ?? 'Astro.props value';
438
+ return {
439
+ sourceKind: repeatContext === undefined ? 'prop' : 'repeated-template',
440
+ dataProvenance: {
441
+ kind: 'prop',
442
+ symbol: name,
443
+ description: `Prop “${name}”; edit it at the parent component call site.`,
444
+ readOnly: true,
445
+ },
446
+ };
447
+ }
448
+ if (symbol !== undefined && declaration !== undefined) {
449
+ const descriptions = {
450
+ local: `Local variable “${symbol}”; navigate to its declaration.`,
451
+ prop: `Prop “${symbol}”; trace it to the parent component call site.`,
452
+ import: `Imported value “${symbol}”; navigate to its definition.`,
453
+ external: `External/API-backed value “${symbol}”; read-only without an editable source integration.`,
454
+ };
455
+ const kinds = {
456
+ local: 'local-variable',
457
+ prop: 'prop',
458
+ import: 'external',
459
+ external: 'external',
460
+ };
461
+ const provenanceKinds = {
462
+ local: 'local',
463
+ prop: 'prop',
464
+ import: 'import',
465
+ external: 'external',
466
+ };
467
+ return {
468
+ sourceKind: repeatContext === undefined ? kinds[declaration.kind] : 'repeated-template',
469
+ dataProvenance: {
470
+ kind: provenanceKinds[declaration.kind],
471
+ symbol,
472
+ description: descriptions[declaration.kind],
473
+ declaredAt: declaration.location,
474
+ ...(declaration.sourceFile === undefined ? {} : { sourceFile: declaration.sourceFile }),
475
+ ...(declaration.sourceType === undefined ? {} : { sourceType: declaration.sourceType }),
476
+ readOnly: true,
477
+ },
478
+ };
479
+ }
480
+ const expressionText = expression?.start === undefined || expression.end === undefined
481
+ ? ''
482
+ : source.slice(expression.start, expression.end);
483
+ const sourceType = detectDataSource(expressionText);
484
+ const external = sourceType !== undefined;
485
+ return {
486
+ sourceKind: repeatContext === undefined
487
+ ? external
488
+ ? 'external'
489
+ : 'generated-unknown'
490
+ : 'repeated-template',
491
+ dataProvenance: {
492
+ kind: external ? 'external' : 'unknown',
493
+ description: external
494
+ ? `${dataSourceLabel(sourceType)} content; read-only without an editable source integration.`
495
+ : 'Generated or unresolved content; use source navigation or the agent fallback.',
496
+ ...(sourceType === undefined ? {} : { sourceType }),
497
+ readOnly: true,
498
+ },
499
+ };
500
+ }
501
+ function collectDeclarations(program, source, projectFile, lineStarts) {
502
+ const declarations = new Map();
503
+ if (program === undefined)
504
+ return declarations;
505
+ const visit = (value) => {
506
+ if (Array.isArray(value)) {
507
+ for (const item of value)
508
+ visit(item);
509
+ return;
510
+ }
511
+ if (!isAstNode(value))
512
+ return;
513
+ if (value.type === 'ImportDeclaration') {
514
+ const location = nodeLocation(value, projectFile, lineStarts);
515
+ const importSource = typeof value.source?.value === 'string' ? value.source.value : undefined;
516
+ const sourceType = importSource === undefined ? 'import' : detectImportSource(importSource);
517
+ const sourceFile = importSource?.startsWith('.') === true
518
+ ? resolveRelativeImport(projectFile, importSource)
519
+ : undefined;
520
+ for (const specifier of value.specifiers ?? []) {
521
+ const name = specifier?.local?.name;
522
+ if (typeof name === 'string' && location !== undefined) {
523
+ declarations.set(name, { kind: sourceType === 'import' ? 'import' : 'external', location, sourceType, ...(sourceFile === undefined ? {} : { sourceFile }) });
524
+ }
525
+ }
526
+ return;
527
+ }
528
+ if (value.type === 'VariableDeclarator') {
529
+ const location = nodeLocation(value, projectFile, lineStarts);
530
+ if (location === undefined)
531
+ return;
532
+ const declarationText = source.slice(value.start ?? 0, value.end ?? 0);
533
+ const sourceType = detectDataSource(declarationText);
534
+ const external = sourceType !== undefined;
535
+ const fromProps = isAstroPropsMember(value.init) || isAstroPropsObject(value.init);
536
+ if (value.id?.type === 'Identifier' && typeof value.id.name === 'string') {
537
+ declarations.set(value.id.name, {
538
+ kind: external ? 'external' : fromProps ? 'prop' : 'local',
539
+ location,
540
+ ...(sourceType === undefined ? {} : { sourceType }),
541
+ });
542
+ }
543
+ else if (value.id?.type === 'ObjectPattern') {
544
+ for (const property of value.id.properties ?? []) {
545
+ const name = property?.value?.name ?? property?.key?.name;
546
+ if (typeof name === 'string') {
547
+ declarations.set(name, {
548
+ kind: fromProps ? 'prop' : external ? 'external' : 'local',
549
+ location,
550
+ ...(sourceType === undefined ? {} : { sourceType }),
551
+ });
552
+ }
553
+ }
554
+ }
555
+ if (value.init?.type === 'ArrowFunctionExpression' ||
556
+ value.init?.type === 'FunctionExpression') {
557
+ for (const parameter of value.init.params ?? []) {
558
+ const parameterLocation = isAstNode(parameter)
559
+ ? nodeLocation(parameter, projectFile, lineStarts)
560
+ : undefined;
561
+ if (parameterLocation !== undefined) {
562
+ for (const name of collectBindingNames(parameter)) {
563
+ declarations.set(name, { kind: 'prop', location: parameterLocation });
564
+ }
565
+ }
566
+ }
567
+ }
568
+ }
569
+ if (value.type === 'FunctionDeclaration' ||
570
+ value.type === 'FunctionExpression' ||
571
+ value.type === 'ArrowFunctionExpression') {
572
+ for (const parameter of value.params ?? []) {
573
+ const location = isAstNode(parameter)
574
+ ? nodeLocation(parameter, projectFile, lineStarts)
575
+ : undefined;
576
+ if (location !== undefined) {
577
+ for (const name of collectBindingNames(parameter)) {
578
+ declarations.set(name, { kind: 'prop', location });
579
+ }
580
+ }
581
+ }
582
+ }
583
+ for (const [key, child] of Object.entries(value)) {
584
+ if (key !== 'start' && key !== 'end' && key !== 'type')
585
+ visit(child);
586
+ }
587
+ };
588
+ visit(program.body);
589
+ return declarations;
590
+ }
591
+ function collectLiteralProps(attributes) {
592
+ if (!Array.isArray(attributes))
593
+ return [];
594
+ const props = [];
595
+ for (const attribute of attributes) {
596
+ if (attribute?.type !== 'JSXAttribute' ||
597
+ attribute.start === undefined ||
598
+ attribute.end === undefined) {
599
+ continue;
600
+ }
601
+ const name = getJsxName(attribute.name);
602
+ if (name === undefined || RESERVED_ATTRIBUTES.has(name) || name.includes(':'))
603
+ continue;
604
+ if (attribute.value == null) {
605
+ props.push({
606
+ name,
607
+ type: 'boolean',
608
+ value: true,
609
+ range: { start: attribute.start, end: attribute.end },
610
+ syntax: 'shorthand',
611
+ });
612
+ continue;
613
+ }
614
+ if (isLiteralNode(attribute.value) &&
615
+ typeof attribute.value.value === 'string' &&
616
+ attribute.value.start !== undefined &&
617
+ attribute.value.end !== undefined) {
618
+ props.push({
619
+ name,
620
+ type: 'string',
621
+ value: attribute.value.value,
622
+ range: { start: attribute.value.start, end: attribute.value.end },
623
+ syntax: 'quoted',
624
+ });
625
+ continue;
626
+ }
627
+ const expression = attribute.value.type === 'JSXExpressionContainer'
628
+ ? attribute.value.expression
629
+ : undefined;
630
+ if (!isLiteralNode(expression) ||
631
+ !['string', 'number', 'boolean'].includes(typeof expression.value) ||
632
+ attribute.value.start === undefined ||
633
+ attribute.value.end === undefined) {
634
+ continue;
635
+ }
636
+ props.push({
637
+ name,
638
+ type: typeof expression.value,
639
+ value: expression.value,
640
+ range: { start: attribute.value.start, end: attribute.value.end },
641
+ syntax: 'expression',
642
+ });
643
+ }
644
+ return props;
645
+ }
646
+ function collectBindingNames(node) {
647
+ if (node.type === 'Identifier' && typeof node.name === 'string')
648
+ return [node.name];
649
+ if (node.type === 'AssignmentPattern' || node.type === 'RestElement') {
650
+ return isAstNode(node.left ?? node.argument)
651
+ ? collectBindingNames((node.left ?? node.argument))
652
+ : [];
653
+ }
654
+ if (node.type === 'TSParameterProperty' && isAstNode(node.parameter)) {
655
+ return collectBindingNames(node.parameter);
656
+ }
657
+ if (node.type === 'ObjectPattern' || node.type === 'ArrayPattern') {
658
+ const candidates = node.type === 'ObjectPattern'
659
+ ? (node.properties ?? []).map((property) => property?.value ?? property?.argument)
660
+ : node.elements ?? [];
661
+ return candidates.flatMap((candidate) => isAstNode(candidate) ? collectBindingNames(candidate) : []);
662
+ }
663
+ return [];
664
+ }
665
+ function isLiteralNode(node) {
666
+ return (node?.type === 'Literal' ||
667
+ node?.type === 'StringLiteral' ||
668
+ node?.type === 'NumericLiteral' ||
669
+ node?.type === 'BooleanLiteral');
670
+ }
671
+ function assignSafeSiblings(groups, source) {
672
+ for (const group of groups.values()) {
673
+ group.sort((left, right) => left.range.start - right.range.start);
674
+ for (let index = 0; index < group.length - 1; index += 1) {
675
+ const left = group[index];
676
+ const right = group[index + 1];
677
+ if (left === undefined || right === undefined)
678
+ continue;
679
+ if (source.slice(left.range.end, right.range.start).trim() !== '')
680
+ continue;
681
+ left.nextSiblingId = right.nodeId;
682
+ right.previousSiblingId = left.nodeId;
683
+ }
684
+ }
685
+ }
686
+ function findAttributeInsertionPoint(source, range) {
687
+ const opening = source.slice(range.start, range.end);
688
+ const relativePoint = opening.endsWith('/>') ? opening.length - 2 : opening.length - 1;
689
+ return range.start + relativePoint;
690
+ }
691
+ function escapeAttribute(value) {
692
+ return value.replaceAll('&', '&amp;').replaceAll('"', '&quot;');
693
+ }
694
+ function getJsxName(node) {
695
+ if (node?.type === 'JSXIdentifier' && typeof node.name === 'string')
696
+ return node.name;
697
+ if (node?.type === 'Identifier' && typeof node.name === 'string')
698
+ return node.name;
699
+ if (node?.type === 'Literal' && typeof node.value === 'string')
700
+ return node.value;
701
+ if (node?.type === 'JSXNamespacedName') {
702
+ const namespace = getJsxName(node.namespace);
703
+ const name = getJsxName(node.name);
704
+ return namespace === undefined || name === undefined ? undefined : `${namespace}:${name}`;
705
+ }
706
+ if (node?.type === 'JSXMemberExpression') {
707
+ const object = getJsxName(node.object);
708
+ const property = getJsxName(node.property);
709
+ return object === undefined || property === undefined ? undefined : `${object}.${property}`;
710
+ }
711
+ return undefined;
712
+ }
713
+ function getRootSymbol(expression) {
714
+ if (expression?.type === 'Identifier' && typeof expression.name === 'string')
715
+ return expression.name;
716
+ if (expression?.type === 'MemberExpression')
717
+ return getRootSymbol(expression.object);
718
+ if (expression?.type === 'ChainExpression')
719
+ return getRootSymbol(expression.expression);
720
+ return undefined;
721
+ }
722
+ function isAstroPropsObject(node) {
723
+ return (node?.type === 'MemberExpression' &&
724
+ node.object?.type === 'Identifier' &&
725
+ node.object.name === 'Astro' &&
726
+ node.property?.type === 'Identifier' &&
727
+ node.property.name === 'props');
728
+ }
729
+ function isAstroPropsMember(node) {
730
+ return node?.type === 'MemberExpression' && isAstroPropsObject(node.object);
731
+ }
732
+ function isMapCall(node) {
733
+ return (node.type === 'CallExpression' &&
734
+ node.callee?.type === 'MemberExpression' &&
735
+ node.callee.property?.type === 'Identifier' &&
736
+ node.callee.property.name === 'map');
737
+ }
738
+ function hasHydrationDirective(attributes) {
739
+ return (Array.isArray(attributes) &&
740
+ attributes.some((attribute) => getJsxName(attribute?.name)?.startsWith('client:')));
741
+ }
742
+ function isComponentName(name) {
743
+ return /^[A-Z]/.test(name) || name.includes('.');
744
+ }
745
+ function detectImportSource(source) {
746
+ if (source === 'astro:content')
747
+ return 'content-collection';
748
+ if (source === 'astro:actions' || source.includes('/actions'))
749
+ return 'action';
750
+ if (/graphql|apollo|urql/i.test(source))
751
+ return 'graphql';
752
+ return 'import';
753
+ }
754
+ function detectDataSource(source) {
755
+ if (/\b(?:getCollection|getEntry|getEntries|render)\s*\(/.test(source))
756
+ return 'content-collection';
757
+ if (/\b(?:Astro\.callAction|actions\.|defineAction)\b/.test(source))
758
+ return 'action';
759
+ if (/\b(?:gql|graphql|useQuery|client\.query)\b/.test(source))
760
+ return 'graphql';
761
+ if (/\b(?:fetch|axios\.|ky\.|request)\s*\(/.test(source))
762
+ return 'api';
763
+ return undefined;
764
+ }
765
+ function dataSourceLabel(sourceType) {
766
+ const labels = {
767
+ api: 'API',
768
+ action: 'Astro Action',
769
+ 'content-collection': 'Astro Content Collection',
770
+ graphql: 'GraphQL',
771
+ import: 'Imported',
772
+ };
773
+ return labels[sourceType];
774
+ }
775
+ function resolveRelativeImport(projectFile, source) {
776
+ const segments = projectFile.split('/');
777
+ segments.pop();
778
+ for (const segment of source.split('/')) {
779
+ if (segment === '' || segment === '.')
780
+ continue;
781
+ if (segment === '..')
782
+ segments.pop();
783
+ else
784
+ segments.push(segment);
785
+ }
786
+ return segments.join('/');
787
+ }
788
+ function createNodeId(projectFile, structuralPath) {
789
+ return createHash('sha256').update(`${projectFile}:${structuralPath}`).digest('hex').slice(0, 16);
790
+ }
791
+ function hash(source) {
792
+ return createHash('sha256').update(source).digest('hex');
793
+ }
794
+ function getLineStarts(source) {
795
+ const starts = [0];
796
+ for (let index = 0; index < source.length; index += 1) {
797
+ if (source[index] === '\n')
798
+ starts.push(index + 1);
799
+ }
800
+ return starts;
801
+ }
802
+ function toLocation(file, start, end, lineStarts) {
803
+ return {
804
+ file,
805
+ start: toPosition(start, lineStarts),
806
+ end: toPosition(end, lineStarts),
807
+ };
808
+ }
809
+ function nodeLocation(node, file, lineStarts) {
810
+ return node.start === undefined || node.end === undefined
811
+ ? undefined
812
+ : toLocation(file, node.start, node.end, lineStarts);
813
+ }
814
+ function toPosition(offset, lineStarts) {
815
+ let low = 0;
816
+ let high = lineStarts.length - 1;
817
+ while (low <= high) {
818
+ const middle = Math.floor((low + high) / 2);
819
+ const lineStart = lineStarts[middle] ?? 0;
820
+ if (lineStart <= offset)
821
+ low = middle + 1;
822
+ else
823
+ high = middle - 1;
824
+ }
825
+ const lineIndex = Math.max(0, high);
826
+ return {
827
+ line: lineIndex + 1,
828
+ column: offset - (lineStarts[lineIndex] ?? 0) + 1,
829
+ offset,
830
+ };
831
+ }
832
+ function isAstNode(value) {
833
+ return typeof value === 'object' && value !== null;
834
+ }
835
+ //# sourceMappingURL=astro-resolver.js.map