Skip to content

fix(vue): an import through @/ or ~/ in a Vue project links the module's file - #2633

Merged
colbymchenry merged 1 commit into
mainfrom
claude/vue-module-import-file
Oct 18, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/vue-module-import-file

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Problem

In any project the Vue resolver detects (a vue / nuxt dependency, or any .vue file), every imports reference named by a module path went through vueResolver.resolve before the import resolver did. For @/… and ~/…, its Patterns 4 and 5 mapped the path to src/… by hand and returned getNodesInFile(path)[0] at 0.9 confidence. That is whichever node of the file came first, which is often not the file node.

Fresh index of a fixture, vue in package.json:

import with Vue (main) without Vue
@/flags/registry (from a .ts) constant REGISTERED_FLAGS file registry.ts
@/table (from an SFC's <script setup>) constant COLUMNS file table.ts
~/utils/helper constant flagged file helper.ts
@/components/Foo.vue, @/layout the component file
#app/where (tsconfig #app/* → client/*) the importing file itself (the Nuxt virtual-module rule) file client/where.ts

Consequences:

Fix

src/resolution/frameworks/vue.ts. For a module-path import (isJsPathImportRef):

  1. When the import resolver finds the module's file, return null and let it link that file. It follows tsconfig paths, workspace packages and the same @/ / ~/ → src/ fallbacks Patterns 4/5 hard-coded, with each language's extensions. A path that resolves only to a folder doesn't count (the context's fileExists is true for directories).
  2. Otherwise, for @/ and ~/ from a plain script, resolve the path the way an SFC's import would: the vue extension list, which adds .vue and /index.vue. Link that file node (import Layout from '@/layout' → src/layout/index.vue). This is the one thing Patterns 4/5 found that the import resolver doesn't from a .ts/.js file: what Vue CLI's bundler resolves.
  3. Only then the Nuxt virtual-module sink (#imports, #components, …), unchanged, for modules no project file is.

Patterns 4 and 5 (from the original Vue support, #66) are removed. No other resolver, no extraction and no schema change.

Validation

Tests. __tests__/vue-module-imports.test.ts has 4 tests; all 4 fail on main (run in a detached worktree at baf1906):

  • every module-path shape above links its file;
  • resolvedBy is import wherever the import resolver finds the file, and framework only for @/layout's index.vue;
  • a sync after a barrel (src/components/index.ts: export const VERSION = 1; export * from './Button') is added equals a fresh index of the final tree;
  • the same when that barrel is deleted. On main both leave Button's edges exact-match / import the other way round from a fresh index.

Mutations: dropping step 2 loses @/layout's edge, and testing step 1 by a non-null path instead of a file node loses it too. Each fails the first two tests.

Related suites (every vue*, sync*, framework, import, alias, route and SFC test file): 72 files, 914 tests passed. Full suite: 7,251 passed, 0 failed, 82 skipped (Windows).

Fresh-index A/B, main baf1906 against this PR, with the same kernel: Vue repos from the earlier kits, non-Vue controls. Edges that differ are paired by site (source, kind, line, col, refName):

repo .vue files changed edges what changed
vue-realworld 20 51 31 same target, framework → import; 12 component → file; 8 constant → file
vue-element-admin 131 115 67 same target, framework → import; 48 component → file (index.vue folders)
vue3-element-admin 143 453 439 same target, framework → import; 14 component → file
vben 695 0 (its apps import through #/, not @/)
nuxt-movies, laravel-realworld 42, 1 0
ts-express, proshop (React), gin (Go) 0 0

Every changed edge is a module-path imports edge re-pointed at the module's file node at the same site. Nodes and files are byte-identical on all 9 repos, and no edge was removed or added.

Sync A/B: every 7th file held out, emptied, or deleted, then synced; each arm compared with its own fresh index.

scenario vue-realworld vue-element-admin vue3-element-admin nuxt-movies, laravel-realworld, ts-express
held out 0 → 0 8 → 8 0 → 0 0 → 0
emptied 4 → 0 18 → 18 182 → 182 0 → 0
deleted 0 → 0 12 → 12 8 → 8 0 → 0

vue-realworld's 4: on main, @/common/jwt.service linked a constant in the fresh index but the file node after the sync, because the first node changed with the storage order. The other residues are line-for-line identical on both arms. vben is left out of the sync runs: its syncs fail with FOREIGN KEY constraint failed on main too (#2565, fix in #2574), and its fresh graph is unchanged.

🤖 Generated with Claude Code

…odule's file

In a project with a `vue` dependency or a `.vue` file, a script's module
import through `@/` or `~/` went through the Vue resolver's own mapping to
`src/`, which linked the first node of the file it found: a constant the
module declares, or a component. A tsconfig alias that starts like a Nuxt
virtual module (`#app/where`) linked the importing file itself. Without Vue
the import resolver links the module's file node, following tsconfig
paths. So the walks a sync makes from a module or barrel that appears or
goes away to the files importing it (#2512, #2560) never found these
importers. Which node the edge hit also depended on the order a file's
nodes were stored, so a sync and a fresh index could disagree.

The Vue resolver now leaves a module import to the import resolver whenever
that finds the file, and only adds what a Vue app's bundler finds from a
plain script: a component named without `.vue`, or a folder's `index.vue`
(`import Layout from '@/layout'`), linked to the file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit 986de95 into main Oct 18, 2026
@colbymchenry
colbymchenry deleted the claude/vue-module-import-file branch October 18, 2026 16:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant