fix(extract): invalidate tsconfig alias/baseUrl caches per run (#2917)

The tsconfig/jsconfig alias and baseUrl caches were keyed by config path with no
mtime/content component and never cleared, so an edit to compilerOptions.paths or
baseUrl was never observed again for the life of the process — graphify watch and the
MCP server, which call extract() repeatedly in one process, kept resolving imports
through the stale alias map. Clear both caches per run alongside the other run-scoped
caches (within-run caching is preserved).
This commit is contained in:
sashankh
2026-08-21 16:49:55 +01:00
committed by safishamsi
parent b2cd362674
commit cce41efb7c
2 changed files with 69 additions and 0 deletions
+9
View File
@@ -70,6 +70,7 @@ from graphify.extractors.resolution import ( # noqa: E402,F401
_JS_PRIMITIVE_TYPES,
_JS_RESOLVE_EXTS,
_TSCONFIG_ALIAS_CACHE,
_TSCONFIG_BASEURL_CACHE,
_VUE_SCRIPT_LANG_RE,
_VUE_SCRIPT_RE,
_WORKSPACE_MANIFEST_NAMES,
@@ -5523,6 +5524,14 @@ def extract(
_raise_recursion_limit()
# Workspace package manifests/globs can change during watch or repeated extraction.
_WORKSPACE_PACKAGE_CACHE.clear()
# tsconfig/jsconfig compilerOptions are the same kind of state: keyed by
# config path with no mtime component, so an edit to `paths` or `baseUrl`
# was never observed again for the life of the process. `graphify watch`
# and the MCP server both call extract() repeatedly in one process, so
# every rebuild after the edit kept resolving through the stale alias map.
# Clearing per run, not per file, leaves within-run caching intact.
_TSCONFIG_ALIAS_CACHE.clear()
_TSCONFIG_BASEURL_CACHE.clear()
_XAML_CSHARP_CLASS_CACHE.clear()
_MD_LINK_INDEX_CACHE.clear()
+60
View File
@@ -191,3 +191,63 @@ def test_tsconfig_wins_when_both_configs_present(tmp_path):
targets = _targets(r)
assert _cid(tmp_path, ts_hit) in targets
assert _cid(tmp_path, tmp_path / "js_root" / "mods" / "W.js") not in targets
# --- config edits must survive the per-process caches (#2917) ---------------
#
# `_TSCONFIG_ALIAS_CACHE` and `_TSCONFIG_BASEURL_CACHE` are keyed on the config
# path with no mtime component. Every test above calls extract() exactly once
# under its own tmp_path, so each gets a fresh cache key and the staleness never
# shows. `graphify watch` and the MCP server are the opposite shape: one process,
# one config path, many extract() calls — so an edit to compilerOptions was never
# observed again for the life of the process. These two run extract() twice
# against the SAME config path, which is the case that was unguarded.
def test_edited_paths_alias_is_observed_by_a_later_extract(tmp_path):
"""Editing `paths` mid-session must retarget the alias, not keep the old map."""
_write(tmp_path / "src" / "target.ts", "export function hit() { return 1; }\n")
_write(tmp_path / "lib" / "target.ts", "export function hit() { return 2; }\n")
importer = _write(tmp_path / "main.ts", "import { hit } from '@app/target';\nhit();\n")
def _run() -> set[str]:
return _targets(extract([importer], root=tmp_path))
_write(tmp_path / "tsconfig.json",
'{\n "compilerOptions": {\n'
' "baseUrl": ".",\n'
' "paths": { "@app/*": ["src/*"] }\n'
' }\n}\n')
assert _cid(tmp_path, tmp_path / "src" / "target.ts") in _run()
_write(tmp_path / "tsconfig.json",
'{\n "compilerOptions": {\n'
' "baseUrl": ".",\n'
' "paths": { "@app/*": ["lib/*"] }\n'
' }\n}\n')
second = _run()
assert _cid(tmp_path, tmp_path / "lib" / "target.ts") in second, (
"second extract() resolved @app/target through the alias map cached by the "
"first run, so the edited tsconfig was never read"
)
assert _cid(tmp_path, tmp_path / "src" / "target.ts") not in second
def test_edited_baseurl_is_observed_by_a_later_extract(tmp_path):
"""Same contract for the separately cached `baseUrl` root (#2153)."""
_write(tmp_path / "one" / "mods" / "Widget.js", "export default function Widget() {}\n")
_write(tmp_path / "two" / "mods" / "Widget.js", "export default function Widget() {}\n")
importer = _write(tmp_path / "packs" / "dashboard.js",
"import Widget from 'mods/Widget.js';\nexport default Widget;\n")
def _run() -> set[str]:
return _targets(extract([importer], root=tmp_path))
_write(tmp_path / "jsconfig.json", '{\n "compilerOptions": { "baseUrl": "one" }\n}\n')
assert _cid(tmp_path, tmp_path / "one" / "mods" / "Widget.js") in _run()
_write(tmp_path / "jsconfig.json", '{\n "compilerOptions": { "baseUrl": "two" }\n}\n')
second = _run()
assert _cid(tmp_path, tmp_path / "two" / "mods" / "Widget.js") in second, (
"second extract() resolved through the baseUrl cached by the first run"
)