Skip to content

fix(hmr): use virtual module ids in import.meta.hot.accept() for vite 8 - #4065

Merged
BobbieGoede merged 3 commits into
nuxt-modules:mainfrom
sawa-ko:fix/vite-8-hmr-virtual-module-accept
Jul 23, 2026
Merged

fix(hmr): use virtual module ids in import.meta.hot.accept() for vite 8#4065
BobbieGoede merged 3 commits into
nuxt-modules:mainfrom
sawa-ko:fix/vite-8-hmr-virtual-module-accept

Conversation

@sawa-ko

@sawa-ko sawa-ko commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

🔗 Linked issue

📚 Description

Vite 8 removed the import.meta.hot.accept() resolution fallback. Previously, accept() could resolve filesystem-relative paths to the correct module in the module graph. In Vite 8, accept() requires the exact module ID as it appears in the import graph.

The i18n module imports locale files and vue-i18n configs through virtual module IDs (#nuxt-i18n/<hash>) via the ResourcePlugin, but the HMR accept() calls in genLocaleLoaderHMR and genVueI18nConfigHMR were using filesystem-relative paths (e.g. ../i18n/locales/en-us.json). This mismatch caused accept() to silently fail, making Vite fall back to sending { type: "full-reload" } via WebSocket — which results in a navigation request that gets stuck in a pending state.

Root Cause

In src/gen.ts, the generateLoaderOptions function creates loader objects with a relative property using relative(nuxt.options.buildDir, meta.path), but the actual import() uses asI18nVirtual(meta.hash):

// The dynamic import uses the virtual ID
load: genDynamicImport(asI18nVirtual(meta.hash), ...)

// But HMR accept was using the filesystem-relative path
relative: relative(nuxt.options.buildDir, meta.path)

Then in src/template.ts, the generated HMR code used the wrong identifier:

// ❌ Before — filesystem path that Vite 8 can't resolve
import.meta.hot.accept("${loader.relative}", async mod => { ... })

// ✅ After — virtual module ID matching the import graph
import.meta.hot.accept("${loader.virtualId}", async mod => { ... })

Changes

  • src/gen.ts: Added virtualId property to LocaleLoaderData type and populated it with asI18nVirtual(hash) in both locale loaders and vue-i18n config loaders.
  • src/template.ts: Changed import.meta.hot.accept() calls to use virtualId instead of relative in both genLocaleLoaderHMR and genVueI18nConfigHMR.

How to Reproduce

  1. Use Nuxt 4.5+ (ships with Vite 8)
  2. Configure @nuxtjs/i18n with JSON locale files
  3. Start dev server and navigate to any page
  4. Edit a translation JSON file
  5. Expected: Messages update in-place via HMR
  6. Actual: A new navigation request appears in the Network tab (e.g. http://localhost:3000/current-page) that stays in a pending state indefinitely

Summary by CodeRabbit

  • Bug Fixes
    • Improved hot module replacement behavior for locale files and Vue I18n configuration updates by using stable generated identifiers for the hot-update wiring.
    • Ensured generated loader and configuration metadata no longer relies on relative paths, improving consistency of dynamic import identifiers.
  • Tests
    • Updated generator tests to simplify Nuxt options setup and align expectations with the new loader option generation behavior.

… 8 compatibility

Vite 8 removed the import.meta.hot.accept() resolution fallback (vitejs/vite#21382).
Previously, accept() could resolve filesystem-relative paths to the correct module in the
module graph. In Vite 8, accept() requires the exact module ID as it appears in the import graph.

The i18n module imports locale files and vue-i18n configs through virtual module IDs
(#nuxt-i18n/<hash>) via the ResourcePlugin, but the HMR accept() calls were using
filesystem-relative paths (e.g. '../i18n/locales/en-us.json'). This mismatch caused
accept() to silently fail, making Vite fall back to a full-reload that would get stuck
as a pending navigation request.

This fix adds a virtualId property to the loader data and uses it in the generated
import.meta.hot.accept() calls instead of the filesystem-relative path.
@sawa-ko
sawa-ko requested a review from BobbieGoede as a code owner July 23, 2026 14:33
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: df075b72-e4a6-4599-b3a5-2cfba38c51f7

📥 Commits

Reviewing files that changed from the base of the PR and between 58c7a57 and d3394a4.

⛔ Files ignored due to path filters (1)
  • test/__snapshots__/gen.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (4)
  • src/gen.ts
  • src/module.ts
  • src/nitro.ts
  • test/gen.test.ts

Walkthrough

Loader and Vue I18n config metadata now includes virtual IDs derived from hashes. Generated HMR handlers for both locale loaders and Vue I18n configs use these virtual IDs in import.meta.hot.accept calls instead of relative identifiers. Module and Nitro templates, along with generator tests, now call generateLoaderOptions without the removed Nuxt options argument.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: HMR now uses virtual module IDs in import.meta.hot.accept() for Vite 8 compatibility.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@BobbieGoede

Copy link
Copy Markdown
Member

Thanks you for your contribution! We will need to check if HMR will still work for webpack/rspack with this change.

@sawa-ko

sawa-ko commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @BobbieGoede!

I just pushed an update that fixes the failed unit tests by updating the snapshots in test/gen.test.ts.snap.

Regarding webpack/rspack support:
The dynamic imports generated for locale loaders (genDynamicImport) already use the virtual module path
(#nuxt-i18n/<hash>) across all bundlers.

Since ResourcePlugin resolves both the dynamic import() and import.meta.hot.accept() requests, using the
exact same virtual module ID (virtualId) ensures that the HMR module graph stays aligned regardless of whether
Vite, Webpack, or Rspack is used.

Let me know if you'd like me to add an end-to-end fixture or additional test cases for bundler validation!

@BobbieGoede BobbieGoede changed the title Bug Fix: Use virtual module IDs in import.meta.hot.accept() for Vite 8 compatibility fix(hmr): use virtual module ids in import.meta.hot.accept() for vite 8 Jul 23, 2026

@BobbieGoede BobbieGoede left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cleaned up the now unused logic and tested with vite 7 and 8 (I misremembered, we don't support HMR for webpack/rspack), LGTM!

@BobbieGoede
BobbieGoede merged commit 329db4e into nuxt-modules:main Jul 23, 2026
10 checks passed
@sawa-ko
sawa-ko deleted the fix/vite-8-hmr-virtual-module-accept branch July 23, 2026 19:37
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.

2 participants