svelte-extractor.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. import { Node, Edge, ExtractionResult, ExtractionError, UnresolvedReference, Language } from '../types';
  2. import { generateNodeId } from './tree-sitter-helpers';
  3. import { TreeSitterExtractor } from './tree-sitter';
  4. import { isLanguageSupported } from './grammars';
  5. /**
  6. * SvelteExtractor - Extracts code relationships from Svelte component files
  7. *
  8. * Svelte files are multi-language (script + template + style). Rather than
  9. * parsing the full Svelte grammar, we extract the <script> block content
  10. * and delegate it to the TypeScript/JavaScript TreeSitterExtractor.
  11. *
  12. * Every .svelte file produces a component node (Svelte components are always importable).
  13. */
  14. export class SvelteExtractor {
  15. private filePath: string;
  16. private source: string;
  17. private nodes: Node[] = [];
  18. private edges: Edge[] = [];
  19. private unresolvedReferences: UnresolvedReference[] = [];
  20. private errors: ExtractionError[] = [];
  21. constructor(filePath: string, source: string) {
  22. this.filePath = filePath;
  23. this.source = source;
  24. }
  25. /**
  26. * Extract from Svelte source
  27. */
  28. extract(): ExtractionResult {
  29. const startTime = Date.now();
  30. try {
  31. // Create component node for the .svelte file itself
  32. const componentNode = this.createComponentNode();
  33. // Extract and process script blocks
  34. const scriptBlocks = this.extractScriptBlocks();
  35. for (const block of scriptBlocks) {
  36. this.processScriptBlock(block, componentNode.id);
  37. }
  38. } catch (error) {
  39. this.errors.push({
  40. message: `Svelte extraction error: ${error instanceof Error ? error.message : String(error)}`,
  41. severity: 'error',
  42. });
  43. }
  44. return {
  45. nodes: this.nodes,
  46. edges: this.edges,
  47. unresolvedReferences: this.unresolvedReferences,
  48. errors: this.errors,
  49. durationMs: Date.now() - startTime,
  50. };
  51. }
  52. /**
  53. * Create a component node for the .svelte file
  54. */
  55. private createComponentNode(): Node {
  56. const lines = this.source.split('\n');
  57. const fileName = this.filePath.split(/[/\\]/).pop() || this.filePath;
  58. const componentName = fileName.replace(/\.svelte$/, '');
  59. const id = generateNodeId(this.filePath, 'component', componentName, 1);
  60. const node: Node = {
  61. id,
  62. kind: 'component',
  63. name: componentName,
  64. qualifiedName: `${this.filePath}::${componentName}`,
  65. filePath: this.filePath,
  66. language: 'svelte',
  67. startLine: 1,
  68. endLine: lines.length,
  69. startColumn: 0,
  70. endColumn: lines[lines.length - 1]?.length || 0,
  71. isExported: true, // Svelte components are always importable
  72. updatedAt: Date.now(),
  73. };
  74. this.nodes.push(node);
  75. return node;
  76. }
  77. /**
  78. * Extract <script> blocks from the Svelte source
  79. */
  80. private extractScriptBlocks(): Array<{
  81. content: string;
  82. startLine: number;
  83. isModule: boolean;
  84. isTypeScript: boolean;
  85. }> {
  86. const blocks: Array<{
  87. content: string;
  88. startLine: number;
  89. isModule: boolean;
  90. isTypeScript: boolean;
  91. }> = [];
  92. const scriptRegex = /<script(\s[^>]*)?>(?<content>[\s\S]*?)<\/script>/g;
  93. let match;
  94. while ((match = scriptRegex.exec(this.source)) !== null) {
  95. const attrs = match[1] || '';
  96. const content = match.groups?.content || match[2] || '';
  97. // Detect TypeScript from lang attribute
  98. const isTypeScript = /lang\s*=\s*["'](ts|typescript)["']/.test(attrs);
  99. // Detect module script
  100. const isModule = /context\s*=\s*["']module["']/.test(attrs);
  101. // Calculate start line of the script content (line after <script>)
  102. const beforeScript = this.source.substring(0, match.index);
  103. const scriptTagLine = (beforeScript.match(/\n/g) || []).length;
  104. // The content starts on the line after the opening <script> tag
  105. const openingTag = match[0].substring(0, match[0].indexOf('>') + 1);
  106. const openingTagLines = (openingTag.match(/\n/g) || []).length;
  107. const contentStartLine = scriptTagLine + openingTagLines + 1; // 0-indexed line
  108. blocks.push({
  109. content,
  110. startLine: contentStartLine,
  111. isModule,
  112. isTypeScript,
  113. });
  114. }
  115. return blocks;
  116. }
  117. /**
  118. * Process a script block by delegating to TreeSitterExtractor
  119. */
  120. private processScriptBlock(
  121. block: { content: string; startLine: number; isModule: boolean; isTypeScript: boolean },
  122. componentNodeId: string
  123. ): void {
  124. const scriptLanguage: Language = block.isTypeScript ? 'typescript' : 'javascript';
  125. // Check if the script language parser is available
  126. if (!isLanguageSupported(scriptLanguage)) {
  127. this.errors.push({
  128. message: `Parser for ${scriptLanguage} not available, cannot parse Svelte script block`,
  129. severity: 'warning',
  130. });
  131. return;
  132. }
  133. // Delegate to TreeSitterExtractor
  134. const extractor = new TreeSitterExtractor(this.filePath, block.content, scriptLanguage);
  135. const result = extractor.extract();
  136. // Offset line numbers from script block back to .svelte file positions
  137. for (const node of result.nodes) {
  138. node.startLine += block.startLine;
  139. node.endLine += block.startLine;
  140. node.language = 'svelte'; // Mark as svelte, not TS/JS
  141. this.nodes.push(node);
  142. // Add containment edge from component to this node
  143. this.edges.push({
  144. source: componentNodeId,
  145. target: node.id,
  146. kind: 'contains',
  147. });
  148. }
  149. // Offset edges (they reference line numbers)
  150. for (const edge of result.edges) {
  151. if (edge.line) {
  152. edge.line += block.startLine;
  153. }
  154. this.edges.push(edge);
  155. }
  156. // Offset unresolved references
  157. for (const ref of result.unresolvedReferences) {
  158. ref.line += block.startLine;
  159. ref.filePath = this.filePath;
  160. ref.language = 'svelte';
  161. this.unresolvedReferences.push(ref);
  162. }
  163. // Carry over errors
  164. for (const error of result.errors) {
  165. if (error.line) {
  166. error.line += block.startLine;
  167. }
  168. this.errors.push(error);
  169. }
  170. }
  171. }