Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

190 lines
6.8 KiB
Go

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.<ext>`.
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
// `<specifier>::<export>` 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.<ext>`, `stem/index.<ext>`); 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.<ext>` 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 `<specifier>::<exportName>`; a module-level edge is
// just `<specifier>`. 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
}