| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198 |
- import { Node, Edge, ExtractionResult, ExtractionError, UnresolvedReference, Language } from '../types';
- import { generateNodeId } from './tree-sitter-helpers';
- import { TreeSitterExtractor } from './tree-sitter';
- import { isLanguageSupported } from './grammars';
- /**
- * SvelteExtractor - Extracts code relationships from Svelte component files
- *
- * Svelte files are multi-language (script + template + style). Rather than
- * parsing the full Svelte grammar, we extract the <script> block content
- * and delegate it to the TypeScript/JavaScript TreeSitterExtractor.
- *
- * Every .svelte file produces a component node (Svelte components are always importable).
- */
- export class SvelteExtractor {
- private filePath: string;
- private source: string;
- private nodes: Node[] = [];
- private edges: Edge[] = [];
- private unresolvedReferences: UnresolvedReference[] = [];
- private errors: ExtractionError[] = [];
- constructor(filePath: string, source: string) {
- this.filePath = filePath;
- this.source = source;
- }
- /**
- * Extract from Svelte source
- */
- extract(): ExtractionResult {
- const startTime = Date.now();
- try {
- // Create component node for the .svelte file itself
- const componentNode = this.createComponentNode();
- // Extract and process script blocks
- const scriptBlocks = this.extractScriptBlocks();
- for (const block of scriptBlocks) {
- this.processScriptBlock(block, componentNode.id);
- }
- } catch (error) {
- this.errors.push({
- message: `Svelte extraction error: ${error instanceof Error ? error.message : String(error)}`,
- severity: 'error',
- });
- }
- return {
- nodes: this.nodes,
- edges: this.edges,
- unresolvedReferences: this.unresolvedReferences,
- errors: this.errors,
- durationMs: Date.now() - startTime,
- };
- }
- /**
- * Create a component node for the .svelte file
- */
- private createComponentNode(): Node {
- const lines = this.source.split('\n');
- const fileName = this.filePath.split(/[/\\]/).pop() || this.filePath;
- const componentName = fileName.replace(/\.svelte$/, '');
- const id = generateNodeId(this.filePath, 'component', componentName, 1);
- const node: Node = {
- id,
- kind: 'component',
- name: componentName,
- qualifiedName: `${this.filePath}::${componentName}`,
- filePath: this.filePath,
- language: 'svelte',
- startLine: 1,
- endLine: lines.length,
- startColumn: 0,
- endColumn: lines[lines.length - 1]?.length || 0,
- isExported: true, // Svelte components are always importable
- updatedAt: Date.now(),
- };
- this.nodes.push(node);
- return node;
- }
- /**
- * Extract <script> blocks from the Svelte source
- */
- private extractScriptBlocks(): Array<{
- content: string;
- startLine: number;
- isModule: boolean;
- isTypeScript: boolean;
- }> {
- const blocks: Array<{
- content: string;
- startLine: number;
- isModule: boolean;
- isTypeScript: boolean;
- }> = [];
- const scriptRegex = /<script(\s[^>]*)?>(?<content>[\s\S]*?)<\/script>/g;
- let match;
- while ((match = scriptRegex.exec(this.source)) !== null) {
- const attrs = match[1] || '';
- const content = match.groups?.content || match[2] || '';
- // Detect TypeScript from lang attribute
- const isTypeScript = /lang\s*=\s*["'](ts|typescript)["']/.test(attrs);
- // Detect module script
- const isModule = /context\s*=\s*["']module["']/.test(attrs);
- // Calculate start line of the script content (line after <script>)
- const beforeScript = this.source.substring(0, match.index);
- const scriptTagLine = (beforeScript.match(/\n/g) || []).length;
- // The content starts on the line after the opening <script> tag
- const openingTag = match[0].substring(0, match[0].indexOf('>') + 1);
- const openingTagLines = (openingTag.match(/\n/g) || []).length;
- const contentStartLine = scriptTagLine + openingTagLines + 1; // 0-indexed line
- blocks.push({
- content,
- startLine: contentStartLine,
- isModule,
- isTypeScript,
- });
- }
- return blocks;
- }
- /**
- * Process a script block by delegating to TreeSitterExtractor
- */
- private processScriptBlock(
- block: { content: string; startLine: number; isModule: boolean; isTypeScript: boolean },
- componentNodeId: string
- ): void {
- const scriptLanguage: Language = block.isTypeScript ? 'typescript' : 'javascript';
- // Check if the script language parser is available
- if (!isLanguageSupported(scriptLanguage)) {
- this.errors.push({
- message: `Parser for ${scriptLanguage} not available, cannot parse Svelte script block`,
- severity: 'warning',
- });
- return;
- }
- // Delegate to TreeSitterExtractor
- const extractor = new TreeSitterExtractor(this.filePath, block.content, scriptLanguage);
- const result = extractor.extract();
- // Offset line numbers from script block back to .svelte file positions
- for (const node of result.nodes) {
- node.startLine += block.startLine;
- node.endLine += block.startLine;
- node.language = 'svelte'; // Mark as svelte, not TS/JS
- this.nodes.push(node);
- // Add containment edge from component to this node
- this.edges.push({
- source: componentNodeId,
- target: node.id,
- kind: 'contains',
- });
- }
- // Offset edges (they reference line numbers)
- for (const edge of result.edges) {
- if (edge.line) {
- edge.line += block.startLine;
- }
- this.edges.push(edge);
- }
- // Offset unresolved references
- for (const ref of result.unresolvedReferences) {
- ref.line += block.startLine;
- ref.filePath = this.filePath;
- ref.language = 'svelte';
- this.unresolvedReferences.push(ref);
- }
- // Carry over errors
- for (const error of result.errors) {
- if (error.line) {
- error.line += block.startLine;
- }
- this.errors.push(error);
- }
- }
- }
|