Skip to content

Follow tsconfig.json extends + project-relative node IDs for JS/TS#867

Closed
ghaiat-yoobic wants to merge 54 commits into
Graphify-Labs:v7from
ghaiat-yoobic:upstream/tsconfig-extends-and-collisions
Closed

Follow tsconfig.json extends + project-relative node IDs for JS/TS#867
ghaiat-yoobic wants to merge 54 commits into
Graphify-Labs:v7from
ghaiat-yoobic:upstream/tsconfig-extends-and-collisions

Conversation

@ghaiat-yoobic

Copy link
Copy Markdown

Builds on #155 (by @nuthalapativarun) which added compilerOptions.paths resolution. Three issues that surface on a real-world Nx/Angular monorepo (~7,725 TS files):

1. extends chains are ignored

Every project tsconfig in Nx/Angular monorepos typically looks like:

{ "extends": "../../tsconfig.base.json" }

with no paths of its own — all aliases live in the shared base file. The previous implementation returned an empty alias map for these files, so every alias-style import silently fell back to basename matching.

Fix: load_tsconfig_paths is refactored into a recursive helper that walks the extends chain. Each level's own paths override anything inherited; paths defined in a base file resolve relative to that base file's directory, matching TypeScript's behaviour.

2. Node IDs collapse on basename

Even with alias resolution working, every node ID was derived from path.stem (filename without extension). Across a 7,725-file project we found:

target id edges before what it actually was
data 2,003 every test/data.ts file
base 1,394 several base.tsx components
text 765 many text.tsx components

All those files collapsed onto a single node.

Fix: new optional LanguageConfig.id_for_path callable. When set, _extract_generic rebinds stem to its return value before deriving file/class/function/method IDs. extract_js computes the project root via find_project_root (new helper that walks up looking for .git) and passes a closure that returns the project-relative stem.

_import_js takes a matching project_root kwarg. When the resolved import path is absolute (via alias resolution) or relative (./, ../), it derives the target ID the same way — so source-file IDs and import-target IDs always agree.

3. Directory-style imports point at phantom nodes

Resolving @yobi/layout/flex to /.../src/flex and then computing a node ID misses that the actual file is flex/index.ts. The file extractor emits src_flex_index; the import resolver emits src_flex. Edge points at a phantom.

Fix: new _resolve_module_file helper — tries the path as-is, then with .ts/.tsx/.d.ts/.js/.jsx/.mjs/.cjs, then as a directory with index.{ts,tsx,js,...}. Import handler uses the resolved file path (when found) to derive the target ID. Falls back to the un-resolved path if nothing on disk matches, so broken imports still get a stable ID.

Backwards-compat & scope

  • External imports (npm packages like rxjs, @angular/core) and unresolved aliases continue to use the legacy basename behaviour.
  • The basename-collision fix is JS/TS only via the optional id_for_path field — Python/Java/Go/etc. extractors are untouched. Same fix could be applied per-language in follow-ups.
  • The extends-walking change in load_tsconfig_paths is contained — empty-paths case still returns {}.

Measured impact on the test corpus

Same 7,725-file Nx/Angular repo, before and after, holding everything else constant:

imports → real in-repo file nodes
PR #155 alone ~0 (extends never resolved)
+ extends fix 3,737
+ project-relative IDs (path collisions resolved but phantom directory targets)
+ module file resolution 20,890

Note on the head commit / split

This branch carries the 3 PR #155 commits at the base. Once #155 merges, this can be rebased to drop them. Happy to split into separate PRs — the three commits here are independent and could each land on their own.

Minidoracat and others added 30 commits April 8, 2026 19:39
* fix: git hooks fail when graphify is installed via pipx

When installed via pipx, the graphify module is only available in
pipx's isolated venv, not the system python3. The git hooks
(post-commit, post-checkout) hardcoded `python3` which cannot import
graphify in this case.

Detect the correct Python interpreter from the graphify binary's
shebang line, matching the approach already used in skill.md Step 1.
Falls back to python3 for system installs.

* fix: handle env-style shebangs and improve interpreter detection

- Use POSIX `command -v` instead of non-standard `which`
- Parse `#!/usr/bin/env python3` shebangs correctly (previous
  `tr -d ' '` would produce `/usr/bin/envpython3`)
- Add import validation fallback to python3 if resolved interpreter
  cannot import graphify
… buffer

* fix: suppress graspologic ANSI output that breaks PowerShell scrolling

graspologic's leiden() emits ANSI escape sequences (progress bars,
colored warnings) that corrupt PowerShell 5.1's scroll buffer on
Windows, disabling vertical scrolling. Redirect stdout/stderr to
StringIO during leiden() calls to prevent any escape codes from
reaching the terminal.

Add 2 tests verifying cluster() produces no stdout/stderr output.

Fixes #19

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add PowerShell troubleshooting section to Windows skill

Document the PowerShell 5.1 scrolling issue and provide 4
workarounds: upgrade graphify, use Windows Terminal, reset
terminal, or uninstall graspologic to use Louvain fallback.

Fixes #19

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Register 'trae' and 'trae-cn' in _PLATFORM_CONFIG (skill-trae.md,
  ~/.trae/skills/ and ~/.trae-cn/skills/, claude_md=False)
- Add CLI subcommands: graphify trae install/uninstall,
  graphify trae-cn install/uninstall (routes to _agents_install/uninstall)
- Update help text with new platform entries
- Create skill-trae.md (Agent-tool based extraction, AGENTS.md integration,
  no PreToolUse hook support per Trae limitations)
- Update README.md and README.zh-CN.md with Trae platform docs

Co-authored-by: lijinshuan <lijinshuan@bytedance.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…utput

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…coverage, .graphify_python persistence

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…#71)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e relations in innerHTML (#sec)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tree-sitter resolves call targets directly from source — marking them
INFERRED was incorrect. Cross-file class-level uses edges remain INFERRED.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…AST calls

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ooks (#140)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sh (#137, #148, #149)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
safishamsi and others added 24 commits April 9, 2026 17:44
Add load_tsconfig_paths() to detect.py that walks up the directory
tree to find tsconfig.json and extracts compilerOptions.paths.
Add resolve_ts_alias() to map alias prefixes (e.g. @/*) to their
resolved filesystem paths.

Update extract_js() to load aliases for the file being processed and
pass them through a closure to _import_js(), so aliased imports like
@/components/Button resolve to real module names in the edge graph.
…he, graphifyignore parent discovery, MCP fixes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cription

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…port

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nstall

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…attribute

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…r prompt itself

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Real-world tsconfig.json files use JSONC syntax (// and /* */ comments,
trailing commas) by default. The plain json.loads() call silently returned
{} on any such file, making alias resolution a no-op in practice.

Replace with a string-aware JSONC stripper that handles line comments,
block comments (including multi-line), trailing commas, and string
literals containing comment-like sequences.
extract_js() now resolves aliases from tsconfig.json, but the cache
was keyed only on source file contents. A tsconfig change would leave
stale import edges in the cache until the source files themselves
changed.

Add an extra_key parameter to file_hash/load_cached/save_cached.
In the extraction loop, JS/TS files with a non-empty alias map hash
the serialised alias map and mix it into the cache key, so any change
to compilerOptions.paths or baseUrl triggers a cache miss.
PR #155 added compilerOptions.paths resolution but only read the first
tsconfig.json found while walking up from the source file. In Nx/Angular
monorepos every project tsconfig.json typically looks like:

    { "extends": "../../tsconfig.base.json" }

with no paths of its own — all aliases live in the shared base file. The
previous implementation returned an empty alias map for these files, so
every alias-style import silently fell back to basename matching.

Refactor load_tsconfig_paths into a recursive helper that walks the
extends chain. Each level's own paths override anything inherited from
its parents; paths defined in a base file resolve relative to that base
file's directory, matching TypeScript's actual behaviour.

Also add find_project_root() (walks up looking for .git) which will be
used by the import resolver in a follow-up commit to derive
project-relative node IDs.
Even with alias resolution working, every node ID was derived from
path.stem (Path.stem strips one extension, so 'foo/test/data.ts' becomes
'data'). Files sharing a basename across the project all collapsed onto
the same node — a 7,700-file monorepo produced 'test/data' targets with
2,000+ incoming edges (every test/data.ts file in the codebase).

Add an optional LanguageConfig.id_for_path callable that converts an
absolute Path to a per-file identifier. When set, the generic extractor
rebinds 'stem' to its return value before deriving file/class/function/
method IDs, so all derived IDs inherit the project-relative prefix.

extract_js() now computes the project root once (via find_project_root)
and passes a closure that returns the path-relative stem.

_import_js() takes a matching project_root kwarg. When the resolved
import path is absolute (via alias resolution) or relative ('./',
'../'), it derives the target ID the same way — so source-file IDs and
import-target IDs always agree.

External imports (npm packages, bare module names like 'rxjs',
'@angular/core') and unresolved aliases continue to use the legacy
basename behaviour.
…ndex.*)

When an alias resolves to features/yobi/layout/src/flex but the actual
file is flex/index.ts (Node-style directory resolution), the import
edge needs to target the same node ID the file extractor produces for
flex/index.ts, otherwise the edge points at a phantom node.

Add _resolve_module_file() that tries the path as-is, then with each
common JS/TS extension, then as a directory with index.{ts,tsx,js,...}.
The import handler now uses the resolved file path (when found) to
derive the target node ID — keeping it in sync with what the file
extractor emits.

Falls back to the un-resolved path if nothing on disk matches, so broken
imports still get a stable id (just an orphan node) rather than skipping
the edge entirely.
@ghaiat-yoobic
ghaiat-yoobic deleted the upstream/tsconfig-extends-and-collisions branch May 14, 2026 15:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants