@@ -90,7 +90,8 @@ func searchTSRoot(dir string, depth, maxDepth int) (string, bool) {
9090 return "" , false
9191}
9292
93- // hasTSMarkers returns true if the directory looks like a TypeScript project root.
93+ // hasTSMarkers returns true if the directory looks like a project root this
94+ // extractor should handle (TypeScript, or a JS framework it also parses).
9495func hasTSMarkers (dir string ) bool {
9596 // tsconfig.json (standard) or tsconfig.base.json (Nx monorepo)
9697 for _ , name := range []string {"tsconfig.json" , "tsconfig.base.json" } {
@@ -99,20 +100,9 @@ func hasTSMarkers(dir string) bool {
99100 }
100101 }
101102
102- // package.json with a typescript dependency
103- data , err := os .ReadFile (filepath .Join (dir , "package.json" ))
104- if err != nil {
105- return false
106- }
107- var pkg map [string ]any
108- if err := json .Unmarshal (data , & pkg ); err != nil {
109- return false
110- }
111- for _ , key := range []string {"dependencies" , "devDependencies" } {
112- if deps , ok := pkg [key ].(map [string ]any ); ok {
113- if _ , ok := deps ["typescript" ]; ok {
114- return true
115- }
103+ for _ , pkg := range []string {"typescript" , "vue" , "react" , "svelte" , "next" , "nuxt" } {
104+ if hasPkgDependency (dir , pkg ) {
105+ return true
116106 }
117107 }
118108 return false
@@ -128,8 +118,7 @@ func (e *TSExtractor) Extract(ctx context.Context, repoPath string, files []stri
128118 isNuxt := detectNuxt (repoPath )
129119 isSvelteKit := detectSvelteKit (repoPath )
130120
131- // Parse tsconfig.json for path alias mappings (e.g., "@/*" → "src/*"),
132- // one alias root per package for monorepos.
121+ // Parse tsconfig.json path aliases, one root per package for monorepos.
133122 aliasRoots := collectTSAliasRoots (repoPath )
134123
135124 // SvelteKit maps $lib → src/lib by convention.
@@ -292,13 +281,8 @@ func (e *TSExtractor) extractImports(root *sitter.Node, src []byte, relFile stri
292281 for i := range root .ChildCount () {
293282 child := root .Child (i )
294283
295- // import_statement always carries its source as a "string" child.
296- // export_statement only carries a "source" field when it has a `from`
297- // clause — i.e. it's a re-export (`export * from "x"`, `export { A }
298- // from "x"`, `export type { A } from "x"`) rather than a local
299- // declaration/value export. Barrel files consist entirely of the
300- // latter and previously emitted zero facts, making anything only
301- // reachable through them look unused.
284+ // export_statement only has a "source" field for re-exports
285+ // (export * from / export { X } from), not local declarations.
302286 var source * sitter.Node
303287 isReexport := false
304288 switch child .Kind () {
@@ -333,10 +317,10 @@ func (e *TSExtractor) extractImports(root *sitter.Node, src []byte, relFile stri
333317 }
334318
335319 result = append (result , facts.Fact {
336- Kind : facts .KindDependency ,
337- Name : dir + " -> " + resolved ,
338- File : relFile ,
339- Line : int (child .StartPosition ().Row ) + 1 ,
320+ Kind : facts .KindDependency ,
321+ Name : dir + " -> " + resolved ,
322+ File : relFile ,
323+ Line : int (child .StartPosition ().Row ) + 1 ,
340324 Props : props ,
341325 Relations : []facts.Relation {
342326 {Kind : facts .RelImports , Target : resolved },
@@ -992,24 +976,16 @@ func nodeText(node *sitter.Node, src []byte) string {
992976 return string (src [node .StartByte ():node .EndByte ()])
993977}
994978
995- // tsAliasRoot pairs a directory (relative to repoPath, forward-slash form,
996- // "" = repo root) with the path-alias map declared by the tsconfig.json /
997- // tsconfig.base.json that governs that directory. Replacement values in
998- // `aliases` are already qualified with `dir` as a prefix, so resolveImportPath
999- // can use them exactly as it always has — it never needs to know about roots.
979+ // tsAliasRoot is a directory (repoPath-relative, "" = root) and the alias
980+ // map its tsconfig declares, already qualified with dir as a prefix.
1000981type tsAliasRoot struct {
1001982 dir string
1002983 aliases map [string ]string
1003984}
1004985
1005- // collectTSAliasRoots finds every directory in the repo whose tsconfig.json
1006- // (or tsconfig.base.json) declares at least one compilerOptions.paths alias.
1007- // Monorepos (Nx, pnpm workspaces, Turborepo, ...) commonly have one tsconfig
1008- // per package, each with its own aliases, plus a paths-less root/base config
1009- // shared for compiler settings only — so unlike findTSRoot/searchTSRoot
1010- // (which stop at the first TS-marker directory, for project *detection*),
1011- // this walks the full subtree and collects every alias-bearing directory, so
1012- // sibling packages are all discovered rather than just the first one found.
986+ // collectTSAliasRoots finds every directory whose tsconfig.json (or
987+ // tsconfig.base.json) declares path aliases — unlike findTSRoot, which stops
988+ // at the first match, this covers monorepos with one tsconfig per package.
1013989func collectTSAliasRoots (repoPath string ) []tsAliasRoot {
1014990 maxDepth := 2
1015991 if isDeepNestedProject (repoPath ) {
@@ -1028,12 +1004,8 @@ func walkTSAliasRoots(repoPath, dir string, depth, maxDepth int, out *[]tsAliasR
10281004 }
10291005 rel = filepath .ToSlash (rel )
10301006
1031- // Qualify each replacement with this root's directory so the resolved
1032- // import path ends up relative to repoPath, not relative to the
1033- // tsconfig's own directory. Plain string concatenation (not
1034- // filepath.Join/Clean) is deliberate: replacement values carry a
1035- // trailing slash that resolveImportPath's `replacement + rest`
1036- // concatenation depends on, and Join would strip it.
1007+ // Concatenation, not filepath.Join, to preserve the trailing slash
1008+ // resolveImportPath's `replacement + rest` depends on.
10371009 qualified := make (map [string ]string , len (aliases ))
10381010 for prefix , replacement := range aliases {
10391011 if rel != "" {
@@ -1069,11 +1041,8 @@ func aliasesAtDir(dir string) (map[string]string, bool) {
10691041 return nil , false
10701042}
10711043
1072- // aliasesForDir returns the alias map of whichever tsAliasRoot is the
1073- // longest matching ancestor-or-equal prefix of `dir` (a repoPath-relative,
1074- // forward-slash directory) — i.e. "which package's tsconfig governs this
1075- // file". Returns nil if no root matches; ranging over a nil map is a no-op,
1076- // so callers need no separate nil-check.
1044+ // aliasesForDir returns the alias map of the root whose dir is the longest
1045+ // matching ancestor-or-equal prefix of dir, or nil if none match.
10771046func aliasesForDir (roots []tsAliasRoot , dir string ) map [string ]string {
10781047 dir = filepath .ToSlash (dir )
10791048 var best * tsAliasRoot
@@ -1094,11 +1063,8 @@ func aliasesForDir(roots []tsAliasRoot, dir string) map[string]string {
10941063 return best .aliases
10951064}
10961065
1097- // withSvelteKitLibDefault ensures every alias root defines the SvelteKit
1098- // "$lib/" convention (→ "<root>/src/lib/") unless that root's own tsconfig
1099- // already defines "$lib/". If no alias roots were found at all (the common
1100- // case: a paths-less tsconfig.json), synthesizes a repo-root entry so the
1101- // convention still applies.
1066+ // withSvelteKitLibDefault adds the "$lib/" -> "<root>/src/lib/" convention
1067+ // to every root that doesn't already define it.
11021068func withSvelteKitLibDefault (roots []tsAliasRoot ) []tsAliasRoot {
11031069 if len (roots ) == 0 {
11041070 roots = []tsAliasRoot {{dir : "" , aliases : map [string ]string {}}}
@@ -1116,11 +1082,9 @@ func withSvelteKitLibDefault(roots []tsAliasRoot) []tsAliasRoot {
11161082 return roots
11171083}
11181084
1119- // tryParseTSConfigAliases reads a single tsconfig.json and extracts path
1120- // alias mappings. For example "@/*": ["./src/*"] maps prefix "@/" to
1121- // replacement "src/". ok is false both when the file doesn't exist/parse and
1122- // when it parses but declares no usable paths — either way, callers should
1123- // keep searching rather than treating this as "found, but empty".
1085+ // tryParseTSConfigAliases reads path alias mappings from a tsconfig.json,
1086+ // e.g. "@/*": ["./src/*"] maps prefix "@/" to replacement "src/". ok is
1087+ // false if the file is missing/invalid or declares no usable paths.
11241088func tryParseTSConfigAliases (tsconfigPath string ) (map [string ]string , bool ) {
11251089 data , err := os .ReadFile (tsconfigPath )
11261090 if err != nil {
0 commit comments