package resolver import ( "strings" "github.com/zzet/gortex/internal/graph" ) // PathAliasResolver expands a JS/TS path-alias import specifier — one // declared in the importing file's nearest-ancestor tsconfig.json / // jsconfig.json `compilerOptions.paths` (e.g. `@/lib/auth`) or resolved // against `baseUrl` — to the repo-prefixed, extension-stripped file stem // it targets (`src/lib/auth`). It returns "" when the specifier is not an // alias / baseUrl import for that file (a relative path or a genuine // third-party package). // // Like NpmAliasResolver it is defined in the resolver package so the // resolver carries no compile-time dependency on the indexer or the // filesystem: the indexer constructs a concrete implementation (which // reads tsconfig from disk via the tsalias package) and injects it via // SetPathAliasResolver. type PathAliasResolver func(callerFile, specifier string) string // SetPathAliasResolver installs a tsconfig path-alias expander. Pass nil // to detach. Must be called before ResolveAll / ResolveFile — the // resolver caches no alias state across passes. func (r *Resolver) SetPathAliasResolver(fn PathAliasResolver) { r.pathAlias = fn } // SetPathAliasResolver installs a tsconfig path-alias expander on the // cross-repo resolver. Same contract as the Resolver method. func (cr *CrossRepoResolver) SetPathAliasResolver(fn PathAliasResolver) { cr.pathAlias = fn } // jsTSImportExts are the source extensions a resolved JS/TS module // specifier may carry on disk, probed in order. The stem (no extension) // is also probed as a directory whose entry point is `index.`. var jsTSImportExts = []string{ ".ts", ".tsx", ".d.ts", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs", } // resolveJSTSImportTarget resolves a JS/TS EdgeImports / EdgeReExports // specifier onto the in-repo file (or exported symbol) it names, or // returns "" when the caller is not JS/TS, the specifier is a genuine // third-party package, or no indexed file matches. importPath is the raw // `import::` payload — the module specifier for a module-level edge, or // `::` for a per-binding edge. getNode is the resolver's // node lookup (cachedGetNode); pathAlias is the injected tsconfig expander // (may be nil). // // Two specifier shapes resolve: // // - Relative (`./auth`, `../lib/auth`) — joined against the importing // file's directory. // - tsconfig / jsconfig path alias or baseUrl import (`@/lib/auth`) — // expanded via the injected PathAliasResolver. // // The expanded stem is probed against the indexed file nodes // (`stem.`, `stem/index.`); the first match wins. A module-level // edge resolves to the file node; a per-binding edge resolves to that // file's exported-symbol node when it exists, else the file node. // // Running this inside resolveImport (rather than as a post-pass) means the // cold ResolveAll worker phase AND every incremental ResolveFile path land // the import edge before buildImportClosure reads it — so a cross-directory // JS/TS import contributes real reachability and the cross-package guard // stops reverting its callers to `unresolved::*` (issue #136). Shared by // Resolver.resolveImport and CrossRepoResolver.resolveImport. func resolveJSTSImportTarget(getNode func(string) *graph.Node, pathAlias PathAliasResolver, callerFile, importPath string) string { if !isJSTSPath(callerFile) { return "" } spec, symbol := splitImportSpecSymbol(importPath) stem := expandJSTSSpecifier(pathAlias, callerFile, spec) if stem == "" { return "" } file := probeJSTSFile(getNode, stem) if file == "" { return "" } if symbol != "" { if symID := file + "::" + symbol; getNode(symID) != nil { return symID } } return file } // jsTSImportCallerFile returns the importing file's graph path for an // import edge, preferring the explicit FilePath and falling back to the // From end (a file node ID is the file path). func jsTSImportCallerFile(e *graph.Edge) string { if e.FilePath != "" { return e.FilePath } return e.From } // expandJSTSSpecifier turns a JS/TS module specifier into a repo-prefixed, // extension-stripped file stem, or "" when it is a genuine third-party // package. Relative specifiers are joined against the importing file's // directory; everything else is handed to the injected PathAliasResolver // (tsconfig `paths` / `baseUrl`). func expandJSTSSpecifier(pathAlias PathAliasResolver, callerFile, spec string) string { if spec == "" { return "" } if strings.HasPrefix(spec, "./") || strings.HasPrefix(spec, "../") { dir := "" if i := strings.LastIndex(callerFile, "/"); i >= 0 { dir = callerFile[:i] } return joinRelativePath(dir, spec) } if pathAlias != nil { return pathAlias(callerFile, spec) } return "" } // probeJSTSFile returns the indexed KindFile node ID an extension-stripped // stem resolves to — trying an explicit author-written extension first, // then each source extension, then a directory `index.` barrel — or // "" when no indexed file matches. func probeJSTSFile(getNode func(string) *graph.Node, stem string) string { if stem == "" { return "" } isFile := func(id string) bool { n := getNode(id) return n != nil && n.Kind == graph.KindFile } // An explicit source extension the author wrote (`./auth.js`) is tried // verbatim first. switch jsTSExt(stem) { case ".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs": if isFile(stem) { return stem } } for _, ext := range jsTSImportExts { if id := stem + ext; isFile(id) { return id } } for _, ext := range jsTSImportExts { if id := stem + "/index" + ext; isFile(id) { return id } } return "" } // splitImportSpecSymbol splits an `import::` edge payload into the module // specifier and the per-binding export name. The per-binding edge the JS // extractor emits is `::`; a module-level edge is // just ``. A module specifier never contains `::`, so the final // `::` (when present) is the separator. // // "./auth" → ("./auth", "") // "./auth::getAuth" → ("./auth", "getAuth") // "@/lib/auth::Foo" → ("@/lib/auth", "Foo") func splitImportSpecSymbol(importPath string) (spec, symbol string) { if i := strings.LastIndex(importPath, "::"); i >= 0 { return importPath[:i], importPath[i+2:] } return importPath, "" } // jsTSExt returns the lowercase file extension of p, or "" when none. func jsTSExt(p string) string { dot := strings.LastIndexByte(p, '.') if dot < 0 { return "" } if slash := strings.LastIndexByte(p, '/'); slash > dot { return "" } return strings.ToLower(p[dot:]) } // isJSTSPath reports whether a file path is a JavaScript / TypeScript // source file — the only importers whose specifiers this resolver expands. func isJSTSPath(p string) bool { switch jsTSExt(p) { case ".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs": return true } return false }