You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat: support TypeScript path alias (tsconfig paths) compile #147
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.
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
Add load_tsconfig_paths(root) function in detect.py or a new tsconfig.py
In the JavaScript/TypeScript extractor in extract.py, call the path mapping function
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.
changed the title [-]feat: 支持 TypeScript path alias (tsconfig paths) 解析[/-][+]feat: Support TypeScript path alias (tsconfig paths) compile[/+]on Apr 9, 2026
changed the title [-]feat: Support TypeScript path alias (tsconfig paths) compile[/-][+]feat: support TypeScript path alias (tsconfig paths) compile[/+]on Apr 9, 2026
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)
Forraw="@/repositories/events", line159producesstem="events" → tgt_nid=_make_id("events"). Theninbuild.pylines54–56, theedgeisfilteredoutbecausenonodewiththatIDexists, andthedropissilent (nowarning, nocounter). Thisiswhytheissueissohardtospotwithoutinspectingedge-typehistograms.
Fixsketch (matchesOP'sproposal):
1.Addasmalltsconfig.pymodulethatwalksupwardfromthescannedroot, findsthenearesttsconfig.json, andreturns {alias_pattern: [target_patterns]} fromcompilerOptions.paths+baseUrl. jsconfig.jsonshouldbehandledthesameway.
2.In_import_js(), beforethebare-modulefallback, trytomatchrawagainsttheloadedaliaspatternsandresolvetoarealfilepath. Fallthroughtothecurrentbehavioronmiss.
3.Cachetheresolvedtsconfigper-rootsoit'sparsedonceperbuild.
Worthnotingthat#256 fixed the analogous problem for relative imports in v0.4.3, so the precedentfor"resolve before ID-ing"alreadyexistsin_import_js() — thisissueisbasically"extend that same resolution to aliased paths".
HappytotestaPRagainstarealNext.jscorpusoncethislands.
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):
(./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:
Follow references in a stub tsconfig, and/or fall back to Nuxt's conventional mappings when nuxt.config.{ts,js} is present.
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.
Activity
[-]feat: 支持 TypeScript path alias (tsconfig paths) 解析[/-][+]feat: Support TypeScript path alias (tsconfig paths) compile[/+][-]feat: Support TypeScript path alias (tsconfig paths) compile[/-][+]feat: support TypeScript path alias (tsconfig paths) compile[/+]fercc1097 commentedon Apr 13, 2026
Confirming this on a real-world Next.js 16 + TypeScript codebase.
graphify v0.4.11, ASTextraction 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:
imports_fromedgescontains(893),calls(93),method(16)Every
import { x } from '@/foo/bar'is silently dropped, so entire layers of the projectend 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_fromedges on top.Reproduced the root cause in
graphify/extract.py_import_js()(lines ~142–172):bhoult commentedon Sep 21, 2026
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.jsonthat is a stub with nocompilerOptions:{ "files": [], "references": [ { "path": "./.nuxt/tsconfig.app.json" }, { "path": "./.nuxt/tsconfig.node.json" } ] }The real
paths(~/*,@/*,~~/*,@@/*) live in.nuxt/tsconfig.*.json, whichnuxt preparegenerates and.gitignoreexcludes. 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_aliasfinds no mapping, the specifier falls to the treat-as-external branch, and every aliased import in the project collapses into a single node literally calledref.This differs from #3357 in that a config file is present and parses fine; it simply has no
compilerOptions.paths, and thereferencesare never followed.Impact, measured on a ~350-file Nuxt 4 app (graphifyy 0.9.65,
extract --code-only --no-cluster):pathssuppliedThe user-visible failure is silent and, I'd argue, the worst part. A shared component with eight real consumers:
"No affected nodes found" is indistinguishable from "this symbol genuinely has no consumers," so it reads as a confident correct answer. Anyone using
affectedfor change-impact analysis would conclude a shared component is safe to change. Withpathssupplied, the same command returns all eight consumers with correctfile:line.Affects
.tsand.vuesources 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'sapp/srcDir;./*for Nuxt 3 layouts.) Notejsconfig.jsondid not work — onlytsconfig.jsonwas picked up, which may itself be worth a look.Two things that might be worth doing independently of the full #155 work:
referencesin a stub tsconfig, and/or fall back to Nuxt's conventional mappings whennuxt.config.{ts,js}is present.affectedreturning "No affected nodes found" is actively misleading. Even a count of unresolved specifiers for the queried node would prevent the wrong conclusion.