Skip to content

feat: support TypeScript path alias (tsconfig paths) compile #147

Open

Description

@shimingw

Summary

Currently, graphify cannot recognize the path mapping configured in tsconfig.json (such as "@/*": "./src/*") when parsing TypeScript projects, which causes modules imported using aliases to not correctly establish relationships.

Problem

For example, in a project with:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"]
    }
  }
}

The code uses:

import Button from "@/components/Button";
import { useAuth } from "@/hooks/useAuth";

Graphify can only parse @/components/Button, but cannot map it to the actual path ./src/components/Button.ts, so it cannot establish the correct import relationship graph.

Expected Behavior

  • Parse tsconfig.json's compilerOptions.paths and baseUrl
  • When parsing import statements in AST, map alias paths back to real file system paths
  • Use real paths to build the code relationship graph

Implementation Suggestion

  1. Add load_tsconfig_paths(root) function in detect.py or a new tsconfig.py
  2. In the JavaScript/TypeScript extractor in extract.py, call the path mapping function
  3. Use real paths as the edge target, instead of the original alias string

Related

  • This is a common TypeScript project feature, supported by Vite, Next.js, Create React App, etc.

Activity

  1. changed the title [-]feat: 支持 TypeScript path alias (tsconfig paths) 解析[/-] [+]feat: Support TypeScript path alias (tsconfig paths) compile[/+] on Apr 9, 2026
  2. changed the title [-]feat: Support TypeScript path alias (tsconfig paths) compile[/-] [+]feat: support TypeScript path alias (tsconfig paths) compile[/+] on Apr 9, 2026
  3. fercc1097 commented on Apr 13, 2026

    @fercc1097

    Confirming this on a real-world Next.js 16 + TypeScript codebase. graphify v0.4.11, AST
    extraction only (no semantic layer), ~1,072 TS/TSX files. Every import in the project uses
    the @/* → src/* alias — the Next.js default.

    Impact in numbers:

    Metric Value
    Total nodes 1,794
    Total edges 1,002
    imports_from edges 0
    Orphan nodes (degree 0) 558 (31%)
    Edge types present contains (893), calls (93), method (16)

    Every import { x } from '@/foo/bar' is silently dropped, so entire layers of the project
    end up as disconnected nodes. Next.js App Router route handlers (src/app/api/**/route.ts)
    are the worst-hit because they have no incoming imports either — they're discovered by
    filesystem routing — so they show up as fully isolated nodes with degree 0, losing all their
    outbound imports_from edges on top.

    Reproduced the root cause in graphify/extract.py _import_js() (lines ~142–172):

    # Line 148: if raw.startswith(".") → resolve as relative path ✓      
    # Line 157–162 (bare module branch):                                                        
    stem = raw.split("/")[-1]                                                                   
    tgt_nid = _make_id(stem)                                                                    
                                                                                                
    For raw = "@/repositories/events", line 159 produces stem = "events" → tgt_nid =            
    _make_id("events"). Then in build.py lines 54–56, the edge is filtered out because no node  
    with that ID exists, and the drop is silent (no warning, no counter). This is why the issue 
    is so hard to spot without inspecting edge-type histograms.          
    
    Fix sketch (matches OP's proposal):                                                         
     
    1. Add a small tsconfig.py module that walks upward from the scanned root, finds the nearest
     tsconfig.json, and returns {alias_pattern: [target_patterns]} from compilerOptions.paths +
    baseUrl. jsconfig.json should be handled the same way.                                      
    2. In _import_js(), before the bare-module fallback, try to match raw against the loaded
    alias patterns and resolve to a real file path. Fall through to the current behavior on     
    miss.
    3. Cache the resolved tsconfig per-root so it's parsed once per build.                      
                                                                                                
    Worth noting that #256 fixed the analogous problem for relative imports in v0.4.3, so the   
    precedent for "resolve before ID-ing" already exists in _import_js() — this issue is        
    basically "extend that same resolution to aliased paths".                                   
                                                                         
    Happy to test a PR against a real Next.js corpus once this lands. 
  4. bhoult commented on Sep 21, 2026

    @bhoult

    Adding a variant of this that isn't covered by #3357's fix (v0.9.56), because the alias config does exist as a file — it just never contains paths.

    Nuxt (and any framework that generates its tsconfig and gitignores it).

    Nuxt 3/4 ships a root tsconfig.json that is a stub with no compilerOptions:

    {
      "files": [],
      "references": [
        { "path": "./.nuxt/tsconfig.app.json" },
        { "path": "./.nuxt/tsconfig.node.json" }
      ]
    }

    The real paths (~/*, @/*, ~~/*, @@/*) live in .nuxt/tsconfig.*.json, which nuxt prepare generates and .gitignore excludes. So in any fresh clone or CI checkout — i.e. exactly where graphify runs — there is no alias mapping on disk at all. _resolve_tsconfig_alias finds no mapping, the specifier falls to the treat-as-external branch, and every aliased import in the project collapses into a single node literally called ref.

    This differs from #3357 in that a config file is present and parses fine; it simply has no compilerOptions.paths, and the references are never followed.

    Impact, measured on a ~350-file Nuxt 4 app (graphifyy 0.9.65, extract --code-only --no-cluster):

    stock with paths supplied
    dangling import edges 254 / 754 (34%) 88 / 2177 (4%)
    import edges resolving to an in-repo file 157 1746
    total edges 2858 4483

    The user-visible failure is silent and, I'd argue, the worst part. A shared component with eight real consumers:

    $ graphify affected "WidgetTrendLineChart.vue"
    Affected nodes for WidgetTrendLineChart.vue
    Depth: 2
    No affected nodes found.
    

    "No affected nodes found" is indistinguishable from "this symbol genuinely has no consumers," so it reads as a confident correct answer. Anyone using affected for change-impact analysis would conclude a shared component is safe to change. With paths supplied, the same command returns all eight consumers with correct file:line.

    Affects .ts and .vue sources equally, so it's the alias path rather than #1141.

    Workaround for anyone hitting this now — write a synthetic tsconfig before extraction (re-apply after checkout, since checkout restores the stub):

    {
      "compilerOptions": {
        "baseUrl": ".",
        "paths": { "~/*": ["./app/*"], "@/*": ["./app/*"], "~~/*": ["./*"], "@@/*": ["./*"] }
      }
    }

    (./app/* for Nuxt 4's app/ srcDir; ./* for Nuxt 3 layouts.) Note jsconfig.json did not work — only tsconfig.json was picked up, which may itself be worth a look.

    Two things that might be worth doing independently of the full #155 work:

    1. Follow references in a stub tsconfig, and/or fall back to Nuxt's conventional mappings when nuxt.config.{ts,js} is present.
    2. Make the silent case loud — if a specifier can't be resolved to a node, affected returning "No affected nodes found" is actively misleading. Even a count of unresolved specifiers for the queried node would prevent the wrong conclusion.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions