Follow-up to #692 (and the v0.7.1 fix). The regex fallback was added at extract.py:1699-1732, and a unit-level call confirms it does emit dynamic_import edges. But none of them survive into graph.json. Two independent bugs:
Bug A — alias paths excluded by design
extract.py:1712-1715:
for m in _re.finditer(r"""import\(\s*['"]([^'"]+)['"]\s*\)""", src):
raw = m.group(1)
if not raw.startswith("."):
continue # ← skips $lib/…, $partials/…, ~/, @/, src/…, etc.
Only relative ./X paths are kept. The original ticket explicitly listed both relative and aliased examples — and in real SvelteKit codebases the aliased form dominates.
Bug B — synthetic node IDs use the source file's path, so edges never resolve
extract.py:1711, 1716:
file_node_id = _make_id(path.stem, str(path))
node_id = _make_id(raw, str(path)) # path = the importer
But the real file nodes elsewhere in the graph are created with _make_id(str(path)) where path is the target file (relative, no stem prefix). So:
real target node id : src_partials_series_grid_card_series_card_library_svelte
regex-pass target id : card_series_card_library_svelte_users_jippi_…_src_partials_series_grid_series_single_view_svelte
real source file id : src_partials_series_grid_series_single_view_svelte
regex-pass source id : series_single_view_users_jippi_…_src_partials_series_grid_series_single_view_svelte
build_from_json in build.py:99-104 tries normalized fallback, then drops the edge:
if src not in node_set or tgt not in node_set:
continue # skip edges to external/stdlib nodes
Both endpoints are phantom, so every dynamic_import edge from the regex pass is discarded. The phantom nodes themselves leak through (I see one labelled ./Card/series-card-library.svelte orbiting nothing), but no edge connects to the real target file.
Repro
>>> from graphify.extract import extract_svelte
>>> r = extract_svelte(Path('src/partials/Series/Grid/series-single-view.svelte'))
>>> [e for e in r['edges'] if e['relation'] == 'dynamic_import']
[{'source': 'series_single_view_…', 'target': 'card_series_card_library_svelte_…', ...}] # produced
Then in graph.json:
$ jq '[.links[] | select(.relation == "dynamic_import")] | length' graphify-out/graph.json
0
Dynamic-import shapes seen in this codebase
A real SvelteKit app uses many flavors. The regex fallback needs to handle all of these. Counts from our 1,870-file corpus, then a minimal example for each:
| Shape |
Where it lives |
Count |
Currently captured? |
{#await import('$lib/...')} |
markup |
21 |
no — Bug A |
{#await import('./...')} |
markup |
1 |
no — Bug B |
import('$lib/...').then(...) |
<script> |
23 |
no — Bug A (tree-sitter doesn't see alias as a file) |
import('./...').then(...) |
<script> |
2 |
only via tree-sitter, not the regex pass |
typeof import('./...') (type-only) |
<script> |
5 |
no |
typeof import('$lib/...') (type-only) |
<script> |
1 |
no |
Markup, aliased — 21 occurrences
<!-- src/routes/(series)/[id=id]/(view)/+layout.svelte:263 -->
{#await import('$lib/components/discover/queue-bar.svelte') then { default: QueueBar }}
<QueueBar series_id={series.id} />
{/await}
<!-- same file: $partials alias -->
{#await import('$partials/Catalog/SeriesEditBadge.svelte') then { default: CatalogSeriesEditBadge }}
<CatalogSeriesEditBadge {series} />
{/await}
Markup, relative — 1 occurrence
<!-- src/partials/Series/Grid/series-single-view.svelte:63 -->
{#await import('./Card/series-card-library.svelte') then { default: SeriesCardLibrary }}
<SeriesCardLibrary {series} library_entry={cardCtx.library_entry} />
{/await}
Script, relative .then() — 2 occurrences
<!-- src/lib/components/display-mode.svelte:47 -->
body_promise = import('./display-mode-body.svelte').then((m) => {
body_component = m.default
})
This one is captured because tree-sitter-js sees the script body. It's the only category that works today.
Script, relative inside Promise.all — same file, app-sidebar.svelte
<!-- src/lib/components/app-sidebar.svelte:550-554 -->
void Promise.all([
import('./change-locale.svelte'),
import('./content-rating-selector.svelte'),
import('./theme-picker.svelte'),
]).then(...)
Script, type-only typeof import(...) — 6 occurrences
<!-- src/lib/components/app-sidebar.svelte:515,522,535-539 -->
type AppSidebarModQueueComponentType = (typeof import('./app-sidebar-mod-queue.svelte'))['default']
let CommandPalette: (typeof import('./command-palette.svelte'))['default'] | undefined = $state()
let LanguagePicker = $state<Component<ComponentProps<typeof import('./change-locale.svelte').default>> | null>(null)
These are type positions, but in modern Svelte 5 + $state() they're load-bearing — the runtime import('./command-palette.svelte').then(...) lives a few lines below the type alias and is the only thing that actually loads the component. A graph that ignores typeof import(...) and gets the runtime import(...) is fine; one that misses both leaves the lazy-loaded component looking like a pure orphan.
Impact
For our corpus, ~150 of the ~314 currently-orphaned .svelte files are reached only through the patterns above — the same population originally cited in #691/#692. None of those edges exist in the current graph.
Follow-up to #692 (and the v0.7.1 fix). The regex fallback was added at
extract.py:1699-1732, and a unit-level call confirms it does emitdynamic_importedges. But none of them survive intograph.json. Two independent bugs:Bug A — alias paths excluded by design
extract.py:1712-1715:Only relative
./Xpaths are kept. The original ticket explicitly listed both relative and aliased examples — and in real SvelteKit codebases the aliased form dominates.Bug B — synthetic node IDs use the source file's path, so edges never resolve
extract.py:1711, 1716:But the real file nodes elsewhere in the graph are created with
_make_id(str(path))wherepathis the target file (relative, no stem prefix). So:build_from_jsoninbuild.py:99-104tries normalized fallback, then drops the edge:Both endpoints are phantom, so every
dynamic_importedge from the regex pass is discarded. The phantom nodes themselves leak through (I see one labelled./Card/series-card-library.svelteorbiting nothing), but no edge connects to the real target file.Repro
Then in
graph.json:$ jq '[.links[] | select(.relation == "dynamic_import")] | length' graphify-out/graph.json 0Dynamic-import shapes seen in this codebase
A real SvelteKit app uses many flavors. The regex fallback needs to handle all of these. Counts from our 1,870-file corpus, then a minimal example for each:
{#await import('$lib/...')}{#await import('./...')}import('$lib/...').then(...)<script>import('./...').then(...)<script>typeof import('./...')(type-only)<script>typeof import('$lib/...')(type-only)<script>Markup, aliased — 21 occurrences
Markup, relative — 1 occurrence
Script, relative
.then()— 2 occurrencesThis one is captured because tree-sitter-js sees the script body. It's the only category that works today.
Script, relative inside
Promise.all— same file, app-sidebar.svelte<!-- src/lib/components/app-sidebar.svelte:550-554 --> void Promise.all([ import('./change-locale.svelte'), import('./content-rating-selector.svelte'), import('./theme-picker.svelte'), ]).then(...)Script, type-only
typeof import(...)— 6 occurrencesThese are type positions, but in modern Svelte 5 +
$state()they're load-bearing — the runtimeimport('./command-palette.svelte').then(...)lives a few lines below the type alias and is the only thing that actually loads the component. A graph that ignorestypeof import(...)and gets the runtimeimport(...)is fine; one that misses both leaves the lazy-loaded component looking like a pure orphan.Impact
For our corpus, ~150 of the ~314 currently-orphaned
.sveltefiles are reached only through the patterns above — the same population originally cited in #691/#692. None of those edges exist in the current graph.