fix(extract): apply resolver fixups to JS/TS dynamic_import handler

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.
This commit is contained in:
Christian Winther
2026-05-05 00:07:50 +02:00
parent 0dfc26e57f
commit b68ec63494
2 changed files with 55 additions and 4 deletions
+6 -4
View File
@@ -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 # Resolve path using the same logic as static imports
if raw.startswith("."): if raw.startswith("."):
resolved = Path(os.path.normpath(Path(str_path).parent / raw)) resolved = Path(os.path.normpath(Path(str_path).parent / raw))
if resolved.suffix == ".js": # Same TS/SvelteKit resolver fixups static imports use, so
resolved = resolved.with_suffix(".ts") # `await import('./foo')` (bare path), `import('./bar.shared')`
elif resolved.suffix == ".jsx": # (multi-dot helper), and Svelte 5 rune-file dynamic imports
resolved = resolved.with_suffix(".tsx") # all land on real file nodes.
resolved = _resolve_js_module_path(resolved)
tgt_nid = _make_id(str(resolved)) tgt_nid = _make_id(str(resolved))
else: else:
aliases = _load_tsconfig_aliases(Path(str_path).parent) 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)) resolved_alias = Path(os.path.normpath(Path(alias_base) / rest))
break break
if resolved_alias is not None: if resolved_alias is not None:
resolved_alias = _resolve_js_module_path(resolved_alias)
tgt_nid = _make_id(str(resolved_alias)) tgt_nid = _make_id(str(resolved_alias))
else: else:
module_name = raw.split("/")[-1] module_name = raw.split("/")[-1]
+49
View File
@@ -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 ────────── # ── 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): def test_dynamic_import_bare_path_resolves(tmp_path):
"""The regex pass for `import('...')` in .svelte files must also use """The regex pass for `import('...')` in .svelte files must also use
the new resolver — otherwise dynamic imports of bare paths still the new resolver — otherwise dynamic imports of bare paths still