Issue and patch by Claude Code, reviewed by me (I'm not super familiar with Python, but SWE with +20 years experience).
After #700, #701, #712 (PR #715), and #713 (PR #714), dynamic_import and most static imports from .svelte files now resolve correctly. But the static-import resolver in _import_js still misses three common TypeScript/Svelte 5 import shapes — and these are by far the most common shapes in any real SvelteKit codebase.
Three missed cases
graphify/extract.py::_import_js rewrites .js → .ts and .jsx → .tsx, but does not try:
-
Bare paths (no extension). Standard TS convention: import { foo } from '$lib/types/type-helpers'. The resolver builds _make_id('src/lib/types/type-helpers') — but the real file node is _make_id('src/lib/types/type-helpers.ts'). ID mismatch → build_from_json drops the edge as external.
-
.svelte paths that point at Svelte 5 rune files (.svelte.ts). import { isMobile } from '$lib/hooks/is-mobile.svelte' — there's no .svelte file at that path; SvelteKit/Vite resolves it to is-mobile.svelte.ts (a Svelte 5 rune-only TypeScript file). graphify writes _make_id('...is-mobile.svelte'), real node is ..._is_mobile_svelte_ts.
-
Directory imports / barrel index.ts. import { x } from '$lib/server/queue' resolves to src/lib/server/queue/index.ts. graphify writes _make_id('src/lib/server/queue'), real node is ..._queue_index_ts.
Scope on a real codebase
A 1,873-file SvelteKit app:
| Import shape |
Count |
Currently resolves? |
| Bare paths (no extension) |
3,576 |
❌ |
.svelte paths that are actually .svelte.ts files |
288 |
❌ |
| Total potentially-missed import edges |
~3,864 |
|
For comparison, #713 was worth ~3,500 edges. This is the same order of magnitude — and it's not theoretical: spot-checked 6 "isolated" .ts files and confirmed each had real importers in the codebase that the graph couldn't see:
src/lib/types/type-helpers.ts — 3 importers, all bare-path
src/lib/server/middleware/api-headers.ts — 2 importers, bare-path
src/lib/server/recommendations/tag-idf.ts — 3 importers, bare-path
src/lib/server/auth/email-verification.ts — 2 importers, bare-path
src/lib/server/queue/index.ts — 3 importers, all directory-style (from '$lib/server/queue')
src/lib/frontend/models/API_V1_Series_Default.ts — 3 importers, bare-path
Reproducer
Pure TS-to-TS:
// src/lib/foo.ts
export const foo = 1
// src/lib/page.ts
import { foo } from './foo' // bare path — TS convention
After graphify update .:
import json, re
g = json.load(open('graphify-out/graph.json'))
def make_id(p): return re.sub(r"[^a-zA-Z0-9]+", "_", p).strip("_").lower()
fid = make_id('src/lib/page.ts')
out = [e for e in g['links'] if e.get('source') == fid]
print(out)
# → edge target is 'src_lib_foo' (no _ts), but the real node id is 'src_lib_foo_ts'
# → mismatch → edge dropped as external in build_from_json
Root cause
graphify/extract.py::_import_js (~line 274–283):
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\")
tgt_nid = _make_id(str(resolved))
The .js → .ts and .jsx → .tsx rewrites are unconditional and don't check filesystem state. There's no fallback for the (much more common) bare-path / .svelte.ts / directory cases. The same gap exists in the alias branch and in extract_svelte()'s regex pass for dynamic_import.
Suggested fix
Add a single resolver helper that walks Vite/TS resolver order, used by both _import_js and extract_svelte:
_TS_RESOLVE_EXTS = (\".ts\", \".tsx\", \".svelte\", \".js\", \".jsx\", \".mjs\")
_TS_INDEX_FILES = (\"index.ts\", \"index.tsx\", \"index.js\", \"index.jsx\")
def _resolve_with_extensions(p: Path) -> Path:
\"\"\"Resolve a TS-style import path to an actual file on disk.
Mirrors Vite/TS resolver order:
1. exact path
2. .js → .ts, .jsx → .tsx (TS ESM convention)
3. bare path → try .ts/.tsx/.svelte/.js/.jsx/.mjs
4. bare path → try directory's index.{ts,tsx,js,jsx}
5. .svelte → .svelte.ts (Svelte 5 rune file at same path)
Falls back to the original path on no match (keeps current
behaviour — dropped as external by build_from_json).
\"\"\"
if p.exists():
return p
if p.suffix == \".js\":
c = p.with_suffix(\".ts\")
if c.exists(): return c
if p.suffix == \".jsx\":
c = p.with_suffix(\".tsx\")
if c.exists(): return c
if p.suffix == \"\":
for ext in _TS_RESOLVE_EXTS:
c = p.with_suffix(ext)
if c.exists(): return c
for idx in _TS_INDEX_FILES:
c = p / idx
if c.exists(): return c
if p.suffix == \".svelte\":
# SvelteKit imports written as `from './foo.svelte'` may actually
# point at `foo.svelte.ts` (a Svelte 5 rune file). Append .ts to
# the full filename rather than swapping the suffix.
c = p.parent / (p.name + \".ts\")
if c.exists(): return c
return p
Same call sites: _import_js (relative + alias branches), extract_svelte (regex dynamic_import branches). Tiny diff; same shape as #701/#712/#713.
Related
This is the natural next step in the resolver chain. With #714 + #715 merged, the alias + script-slicing layers are correct; this issue is the remaining "file → file" identity problem.
Environment
After #700, #701, #712 (PR #715), and #713 (PR #714),
dynamic_importand most static imports from.sveltefiles now resolve correctly. But the static-import resolver in_import_jsstill misses three common TypeScript/Svelte 5 import shapes — and these are by far the most common shapes in any real SvelteKit codebase.Three missed cases
graphify/extract.py::_import_jsrewrites.js → .tsand.jsx → .tsx, but does not try:Bare paths (no extension). Standard TS convention:
import { foo } from '$lib/types/type-helpers'. The resolver builds_make_id('src/lib/types/type-helpers')— but the real file node is_make_id('src/lib/types/type-helpers.ts'). ID mismatch →build_from_jsondrops the edge as external..sveltepaths that point at Svelte 5 rune files (.svelte.ts).import { isMobile } from '$lib/hooks/is-mobile.svelte'— there's no.sveltefile at that path; SvelteKit/Vite resolves it tois-mobile.svelte.ts(a Svelte 5 rune-only TypeScript file). graphify writes_make_id('...is-mobile.svelte'), real node is..._is_mobile_svelte_ts.Directory imports / barrel
index.ts.import { x } from '$lib/server/queue'resolves tosrc/lib/server/queue/index.ts. graphify writes_make_id('src/lib/server/queue'), real node is..._queue_index_ts.Scope on a real codebase
A 1,873-file SvelteKit app:
.sveltepaths that are actually.svelte.tsfilesFor comparison, #713 was worth ~3,500 edges. This is the same order of magnitude — and it's not theoretical: spot-checked 6 "isolated"
.tsfiles and confirmed each had real importers in the codebase that the graph couldn't see:src/lib/types/type-helpers.ts— 3 importers, all bare-pathsrc/lib/server/middleware/api-headers.ts— 2 importers, bare-pathsrc/lib/server/recommendations/tag-idf.ts— 3 importers, bare-pathsrc/lib/server/auth/email-verification.ts— 2 importers, bare-pathsrc/lib/server/queue/index.ts— 3 importers, all directory-style (from '$lib/server/queue')src/lib/frontend/models/API_V1_Series_Default.ts— 3 importers, bare-pathReproducer
Pure TS-to-TS:
After
graphify update .:Root cause
graphify/extract.py::_import_js(~line 274–283):The
.js → .tsand.jsx → .tsxrewrites are unconditional and don't check filesystem state. There's no fallback for the (much more common) bare-path /.svelte.ts/ directory cases. The same gap exists in the alias branch and inextract_svelte()'s regex pass fordynamic_import.Suggested fix
Add a single resolver helper that walks Vite/TS resolver order, used by both
_import_jsandextract_svelte:Same call sites:
_import_js(relative + alias branches),extract_svelte(regexdynamic_importbranches). Tiny diff; same shape as #701/#712/#713.Related
source_filestamped to importer (PR fix(svelte): stamp dynamic-import stub source_file to target, not importer (#712) #715)import X from './foo.svelte'— script-block content not sliced before tree-sitter JS #713 —<script>block parsing for static imports (PR fix(svelte): parse <script> blocks so static imports produce edges (#713) #714)This is the natural next step in the resolver chain. With #714 + #715 merged, the alias + script-slicing layers are correct; this issue is the remaining "file → file" identity problem.
Environment