Skip to content

extract_svelte() regex fallback: alias paths skipped + synthetic IDs don't match real file nodes, so 100% of generated edges are dropped by build #701

Description

@jippi

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions