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
190 lines
6.8 KiB
Go
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
|
|
}
|