Skip to content

Conversation

@Flo0806
Copy link
Member

@Flo0806 Flo0806 commented Nov 6, 2025

🔗 Linked issue

Fixes: #33468

Description

When changing a composable's export type during development (e.g., switching between export default and export function), the dev server crashes with (0, __vite_ssr_import_2__.default) is not a function.

This happens because Vite's HMR doesn't invalidate the module cache when export types change. The old import statement tries to use the new export type, causing a runtime error.

Solution

Added a Vite plugin in the imports module that:

  1. Detects export type changes - Compares old and new exports in composables using scanDirExports
  2. Recursively invalidates module cache - Clears the module and all its importers in both client and SSR Vite servers
  3. Triggers full reload - Forces a full page reload instead of attempting HMR (which would fail)

The plugin runs in the handleHotUpdate hook before Vite's HMR logic executes, ensuring proper cache invalidation. For normal code changes without export type modifications, HMR continues to work as usual.

Works with both Vite's new Environment API and legacy mode by tracking all Vite server instances via the vite:serverCreated hook.

Testing

Tested by switching between:
export default function() { return 'default' }
and
export function useName() { return 'named' }

@Flo0806 Flo0806 requested a review from danielroe as a code owner November 6, 2025 19:56
@bolt-new-by-stackblitz
Copy link

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Nov 6, 2025

Open in StackBlitz

@nuxt/kit

npm i https://pkg.pr.new/@nuxt/kit@33671

nuxt

npm i https://pkg.pr.new/nuxt@33671

@nuxt/rspack-builder

npm i https://pkg.pr.new/@nuxt/rspack-builder@33671

@nuxt/schema

npm i https://pkg.pr.new/@nuxt/schema@33671

@nuxt/vite-builder

npm i https://pkg.pr.new/@nuxt/vite-builder@33671

@nuxt/webpack-builder

npm i https://pkg.pr.new/@nuxt/webpack-builder@33671

commit: 549663a

@coderabbitai
Copy link

coderabbitai bot commented Nov 6, 2025

Walkthrough

This change introduces HMR handling for composables directory files in Nuxt. The implementation tracks Vite servers (client and SSR) and registers a new Vite plugin (nuxt:composables-hmr) that detects when an export type changes (default versus named export) within updated composables files. When an export type change is detected, the plugin recursively invalidates affected modules across all tracked Vite servers and triggers a full page reload. If no export type change occurs, the standard HMR flow continues.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45–60 minutes

  • The Vite server tracking mechanism and vite:serverCreated hook registration require verification of correct lifecycle management
  • The export type comparison logic needs careful review to ensure accurate detection of default versus named export changes
  • Module invalidation across client and SSR contexts must be validated to prevent inconsistent behaviour
  • The page reload triggering logic should be examined for any potential race conditions or unintended side effects
  • Integration with the existing HMR pipeline and overall module configuration flow requires thorough understanding

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarises the main change: fixing module cache invalidation when composable export types change during development.
Description check ✅ Passed The description is well-detailed and directly relates to the changeset, explaining the problem, solution, and implementation approach.
Linked Issues check ✅ Passed The PR successfully addresses issue #33468 by implementing export type change detection and module cache invalidation to prevent dev server crashes.
Out of Scope Changes check ✅ Passed All changes in the imports module are directly scoped to fixing the composable export type change issue; no extraneous modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8539fe4 and 549663a.

📒 Files selected for processing (1)
  • packages/nuxt/src/imports/module.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Follow standard TypeScript conventions and best practices

Files:

  • packages/nuxt/src/imports/module.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
Repo: nuxt/nuxt PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-07-18T16:46:07.446Z
Learning: Applies to **/*.vue : Use `<script setup lang="ts">` and the composition API when creating Vue components
Learnt from: huang-julien
Repo: nuxt/nuxt PR: 29366
File: packages/nuxt/src/app/components/nuxt-root.vue:16-19
Timestamp: 2024-12-12T12:36:34.871Z
Learning: In `packages/nuxt/src/app/components/nuxt-root.vue`, when optimizing bundle size by conditionally importing components based on route metadata, prefer using inline conditional imports like:

```js
const IsolatedPage = route?.meta?.isolate ? defineAsyncComponent(() => import('#build/isolated-page.mjs')) : null
```

instead of wrapping the import in a computed property or importing the component unconditionally.
📚 Learning: 2024-11-05T15:22:54.759Z
Learnt from: GalacticHypernova
Repo: nuxt/nuxt PR: 26468
File: packages/nuxt/src/components/plugins/loader.ts:24-24
Timestamp: 2024-11-05T15:22:54.759Z
Learning: In `packages/nuxt/src/components/plugins/loader.ts`, the references to `resolve` and `distDir` are legacy code from before Nuxt used the new unplugin VFS and will be removed.

Applied to files:

  • packages/nuxt/src/imports/module.ts
🧬 Code graph analysis (1)
packages/nuxt/src/imports/module.ts (1)
packages/kit/src/ignore.ts (1)
  • isIgnored (14-34)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: code

Comment on lines +119 to +128
// Check if export type changed (default ↔ named)
let exportTypeChanged = false
for (const newImport of newImports) {
const oldImport = oldImports.find(i => (i.as || i.name) === (newImport.as || newImport.name))
if (oldImport && oldImport.name !== newImport.name) {
exportTypeChanged = true
break
}
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Detection misses default↔named switches when the alias changes

Line 120: if the export flips between default and named while you also rename the symbol (e.g. export default function useKey()export function useKeyRenamed()), oldImport is never found so exportTypeChanged stays false. The module cache is left untouched and the original (…).default is not a function crash still reproduces. Please derive exportTypeChanged from the presence of a name === 'default' entry before versus after (and only fall back to the alias comparison when that check is inconclusive) so we catch these alias-renaming cases as well. Otherwise the fix remains incomplete.

@codspeed-hq
Copy link

codspeed-hq bot commented Nov 6, 2025

CodSpeed Performance Report

Merging #33671 will not alter performance

Comparing Flo0806:fix/composable-export-type-change-hmr (549663a) with main (8539fe4)

Summary

✅ 10 untouched


// Vite plugin to detect and handle export type changes BEFORE HMR
nuxt.hook('vite:configResolved', (viteConfig) => {
if (!viteConfig.plugins) { return }
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldnt we initialise the list here and push the plugin in anyway?

Copy link
Member Author

Choose a reason for hiding this comment

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

viteConfig.plugins? I guess no. Because it is readonly and the vite:configResolved hook is called after Vite's config resolution, which always includes a plugins array. The early return is defensive programming - if plugins doesn't exist, something is fundamentally wrong with the Vite setup and we shouldn't proceed.

@danielroe
Copy link
Member

danielroe commented Nov 7, 2025

this may also be an approach which also works for some linked issues:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Changing exports in files tracked by unimport crashes dev server in certain cases

3 participants