From b68ec63494ded5848710bc5db667ac05dda4d8b1 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Tue, 5 May 2026 00:07:50 +0200 Subject: [PATCH] fix(extract): apply resolver fixups to JS/TS dynamic_import handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third call site that re-implemented the same .js→.ts rewrite in isolation. Previously only handled the explicit .js→.ts case; bare paths, multi-dot helper files, and alias-resolved dynamic imports all dropped silently. Now uses _resolve_js_module_path on both branches (relative and alias) — same shape as the static-import and Svelte regex paths. Real-world impact: TS files using `await import('./foo')` patterns for code splitting (e.g. lazy-loading a profanity check) now produce edges to the resolved target. --- graphify/extract.py | 10 +++-- tests/test_import_extension_resolution.py | 49 +++++++++++++++++++++++ 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/graphify/extract.py b/graphify/extract.py index 96eed61..2a4e0ee 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -453,10 +453,11 @@ def _dynamic_import_js(node, source: bytes, caller_nid: str, str_path: str, edge # Resolve path using the same logic as static imports if raw.startswith("."): resolved = Path(os.path.normpath(Path(str_path).parent / raw)) - if resolved.suffix == ".js": - resolved = resolved.with_suffix(".ts") - elif resolved.suffix == ".jsx": - resolved = resolved.with_suffix(".tsx") + # Same TS/SvelteKit resolver fixups static imports use, so + # `await import('./foo')` (bare path), `import('./bar.shared')` + # (multi-dot helper), and Svelte 5 rune-file dynamic imports + # all land on real file nodes. + resolved = _resolve_js_module_path(resolved) tgt_nid = _make_id(str(resolved)) else: aliases = _load_tsconfig_aliases(Path(str_path).parent) @@ -467,6 +468,7 @@ def _dynamic_import_js(node, source: bytes, caller_nid: str, str_path: str, edge resolved_alias = Path(os.path.normpath(Path(alias_base) / rest)) break if resolved_alias is not None: + resolved_alias = _resolve_js_module_path(resolved_alias) tgt_nid = _make_id(str(resolved_alias)) else: module_name = raw.split("/")[-1] diff --git a/tests/test_import_extension_resolution.py b/tests/test_import_extension_resolution.py index 39051c9..0d1222c 100644 --- a/tests/test_import_extension_resolution.py +++ b/tests/test_import_extension_resolution.py @@ -458,6 +458,55 @@ def test_resolve_chain_alias_and_extension_compose(tmp_path): # ── End-to-end: dynamic_import in .svelte regex pass uses resolver ────────── +def test_ts_dynamic_import_bare_path_resolves(tmp_path): + """Real-world repro: a TS file uses `await import('./foo')` (no extension) + to lazy-load a sibling module. The dynamic-import handler in JS/TS files + has its own copy of the resolution logic — distinct from the static-import + handler and from the Svelte regex pass — and was missing the bare-path + extension append, silently dropping every such edge.""" + target = _write(tmp_path / "profanity.ts", + "export const hasProfanity = (s: string) => false") + importer = _write(tmp_path / "auth-validators.ts", """\ +export async function validate(name: string) { + const { hasProfanity } = await import('./profanity') + return hasProfanity(name) +} +""") + result = extract_js(importer) + expected = _make_id(str(target)) + targets = {str(e.get("target") or "") for e in result["edges"] + if e.get("relation") in ("imports", "imports_from")} + assert expected in targets, ( + f"Bare-path TS dynamic import failed to resolve; " + f"expected {expected}; got {targets}" + ) + + +def test_ts_dynamic_import_alias_with_bare_path_resolves(tmp_path): + """The other branch of the dynamic-import handler — alias resolution — + also needs the same fixups. `import('$lib/foo')` should resolve to + `$lib/foo.ts` after both alias substitution and extension append.""" + src = tmp_path / "src" + target = _write(src / "lib" / "lazy-module.ts", "export const x = 1") + _write(tmp_path / "tsconfig.json", + '{"compilerOptions":{"paths":{"$lib":["./src/lib"],' + '"$lib/*":["./src/lib/*"]}}}') + importer = _write(src / "routes" / "page.ts", """\ +export async function load() { + const m = await import('$lib/lazy-module') + return m.x +} +""") + result = extract_js(importer) + expected = _make_id(str(target)) + targets = {str(e.get("target") or "") for e in result["edges"] + if e.get("relation") in ("imports", "imports_from")} + assert expected in targets, ( + f"Alias + bare-path dynamic import failed to resolve; " + f"expected {expected}; got {targets}" + ) + + def test_dynamic_import_bare_path_resolves(tmp_path): """The regex pass for `import('...')` in .svelte files must also use the new resolver — otherwise dynamic imports of bare paths still