@@ -2287,6 +2287,201 @@ export class LiquidExtractor {
22872287 }
22882288}
22892289
2290+ /**
2291+ * SvelteExtractor - Extracts code relationships from Svelte component files
2292+ *
2293+ * Svelte files are multi-language (script + template + style). Rather than
2294+ * parsing the full Svelte grammar, we extract the <script> block content
2295+ * and delegate it to the TypeScript/JavaScript TreeSitterExtractor.
2296+ *
2297+ * Every .svelte file produces a component node (Svelte components are always importable).
2298+ */
2299+ export class SvelteExtractor {
2300+ private filePath : string ;
2301+ private source : string ;
2302+ private nodes : Node [ ] = [ ] ;
2303+ private edges : Edge [ ] = [ ] ;
2304+ private unresolvedReferences : UnresolvedReference [ ] = [ ] ;
2305+ private errors : ExtractionError [ ] = [ ] ;
2306+
2307+ constructor ( filePath : string , source : string ) {
2308+ this . filePath = filePath ;
2309+ this . source = source ;
2310+ }
2311+
2312+ /**
2313+ * Extract from Svelte source
2314+ */
2315+ extract ( ) : ExtractionResult {
2316+ const startTime = Date . now ( ) ;
2317+
2318+ try {
2319+ // Create component node for the .svelte file itself
2320+ const componentNode = this . createComponentNode ( ) ;
2321+
2322+ // Extract and process script blocks
2323+ const scriptBlocks = this . extractScriptBlocks ( ) ;
2324+
2325+ for ( const block of scriptBlocks ) {
2326+ this . processScriptBlock ( block , componentNode . id ) ;
2327+ }
2328+ } catch ( error ) {
2329+ captureException ( error , { operation : 'svelte-extraction' , filePath : this . filePath } ) ;
2330+ this . errors . push ( {
2331+ message : `Svelte extraction error: ${ error instanceof Error ? error . message : String ( error ) } ` ,
2332+ severity : 'error' ,
2333+ } ) ;
2334+ }
2335+
2336+ return {
2337+ nodes : this . nodes ,
2338+ edges : this . edges ,
2339+ unresolvedReferences : this . unresolvedReferences ,
2340+ errors : this . errors ,
2341+ durationMs : Date . now ( ) - startTime ,
2342+ } ;
2343+ }
2344+
2345+ /**
2346+ * Create a component node for the .svelte file
2347+ */
2348+ private createComponentNode ( ) : Node {
2349+ const lines = this . source . split ( '\n' ) ;
2350+ const fileName = this . filePath . split ( / [ / \\ ] / ) . pop ( ) || this . filePath ;
2351+ const componentName = fileName . replace ( / \. s v e l t e $ / , '' ) ;
2352+ const id = generateNodeId ( this . filePath , 'component' , componentName , 1 ) ;
2353+
2354+ const node : Node = {
2355+ id,
2356+ kind : 'component' ,
2357+ name : componentName ,
2358+ qualifiedName : `${ this . filePath } ::${ componentName } ` ,
2359+ filePath : this . filePath ,
2360+ language : 'svelte' ,
2361+ startLine : 1 ,
2362+ endLine : lines . length ,
2363+ startColumn : 0 ,
2364+ endColumn : lines [ lines . length - 1 ] ?. length || 0 ,
2365+ isExported : true , // Svelte components are always importable
2366+ updatedAt : Date . now ( ) ,
2367+ } ;
2368+
2369+ this . nodes . push ( node ) ;
2370+ return node ;
2371+ }
2372+
2373+ /**
2374+ * Extract <script> blocks from the Svelte source
2375+ */
2376+ private extractScriptBlocks ( ) : Array < {
2377+ content : string ;
2378+ startLine : number ;
2379+ isModule : boolean ;
2380+ isTypeScript : boolean ;
2381+ } > {
2382+ const blocks : Array < {
2383+ content : string ;
2384+ startLine : number ;
2385+ isModule : boolean ;
2386+ isTypeScript : boolean ;
2387+ } > = [ ] ;
2388+
2389+ const scriptRegex = / < s c r i p t ( \s [ ^ > ] * ) ? > (?< content > [ \s \S ] * ?) < \/ s c r i p t > / g;
2390+ let match ;
2391+
2392+ while ( ( match = scriptRegex . exec ( this . source ) ) !== null ) {
2393+ const attrs = match [ 1 ] || '' ;
2394+ const content = match . groups ?. content || match [ 2 ] || '' ;
2395+
2396+ // Detect TypeScript from lang attribute
2397+ const isTypeScript = / l a n g \s * = \s * [ " ' ] ( t s | t y p e s c r i p t ) [ " ' ] / . test ( attrs ) ;
2398+
2399+ // Detect module script
2400+ const isModule = / c o n t e x t \s * = \s * [ " ' ] m o d u l e [ " ' ] / . test ( attrs ) ;
2401+
2402+ // Calculate start line of the script content (line after <script>)
2403+ const beforeScript = this . source . substring ( 0 , match . index ) ;
2404+ const scriptTagLine = ( beforeScript . match ( / \n / g) || [ ] ) . length ;
2405+ // The content starts on the line after the opening <script> tag
2406+ const openingTag = match [ 0 ] . substring ( 0 , match [ 0 ] . indexOf ( '>' ) + 1 ) ;
2407+ const openingTagLines = ( openingTag . match ( / \n / g) || [ ] ) . length ;
2408+ const contentStartLine = scriptTagLine + openingTagLines + 1 ; // 0-indexed line
2409+
2410+ blocks . push ( {
2411+ content,
2412+ startLine : contentStartLine ,
2413+ isModule,
2414+ isTypeScript,
2415+ } ) ;
2416+ }
2417+
2418+ return blocks ;
2419+ }
2420+
2421+ /**
2422+ * Process a script block by delegating to TreeSitterExtractor
2423+ */
2424+ private processScriptBlock (
2425+ block : { content : string ; startLine : number ; isModule : boolean ; isTypeScript : boolean } ,
2426+ componentNodeId : string
2427+ ) : void {
2428+ const scriptLanguage : Language = block . isTypeScript ? 'typescript' : 'javascript' ;
2429+
2430+ // Check if the script language parser is available
2431+ if ( ! isLanguageSupported ( scriptLanguage ) ) {
2432+ this . errors . push ( {
2433+ message : `Parser for ${ scriptLanguage } not available, cannot parse Svelte script block` ,
2434+ severity : 'warning' ,
2435+ } ) ;
2436+ return ;
2437+ }
2438+
2439+ // Delegate to TreeSitterExtractor
2440+ const extractor = new TreeSitterExtractor ( this . filePath , block . content , scriptLanguage ) ;
2441+ const result = extractor . extract ( ) ;
2442+
2443+ // Offset line numbers from script block back to .svelte file positions
2444+ for ( const node of result . nodes ) {
2445+ node . startLine += block . startLine ;
2446+ node . endLine += block . startLine ;
2447+ node . language = 'svelte' ; // Mark as svelte, not TS/JS
2448+
2449+ this . nodes . push ( node ) ;
2450+
2451+ // Add containment edge from component to this node
2452+ this . edges . push ( {
2453+ source : componentNodeId ,
2454+ target : node . id ,
2455+ kind : 'contains' ,
2456+ } ) ;
2457+ }
2458+
2459+ // Offset edges (they reference line numbers)
2460+ for ( const edge of result . edges ) {
2461+ if ( edge . line ) {
2462+ edge . line += block . startLine ;
2463+ }
2464+ this . edges . push ( edge ) ;
2465+ }
2466+
2467+ // Offset unresolved references
2468+ for ( const ref of result . unresolvedReferences ) {
2469+ ref . line += block . startLine ;
2470+ ref . filePath = this . filePath ;
2471+ ref . language = 'svelte' ;
2472+ this . unresolvedReferences . push ( ref ) ;
2473+ }
2474+
2475+ // Carry over errors
2476+ for ( const error of result . errors ) {
2477+ if ( error . line ) {
2478+ error . line += block . startLine ;
2479+ }
2480+ this . errors . push ( error ) ;
2481+ }
2482+ }
2483+ }
2484+
22902485/**
22912486 * Extract nodes and edges from source code
22922487 */
@@ -2297,6 +2492,12 @@ export function extractFromSource(
22972492) : ExtractionResult {
22982493 const detectedLanguage = language || detectLanguage ( filePath ) ;
22992494
2495+ // Use custom extractor for Svelte
2496+ if ( detectedLanguage === 'svelte' ) {
2497+ const extractor = new SvelteExtractor ( filePath , source ) ;
2498+ return extractor . extract ( ) ;
2499+ }
2500+
23002501 // Use custom extractor for Liquid
23012502 if ( detectedLanguage === 'liquid' ) {
23022503 const extractor = new LiquidExtractor ( filePath , source ) ;
0 commit comments