Subscribe on changes!

HMR not working when importing a library with embedded Vue

avatar
Dec 15th 2022

Vue version

3.2.45

Link to minimal reproduction

https://stackblitz.com/edit/vue-consume-vue-hmr?file=README.md

Steps to reproduce

  • Run the vue app with npm run dev
  • Edit content in src/components/HelloWorld.vue

What is expected?

  • Browser automatically shows updated content when a component is edited

What is actually happening?

  • Terminal output indicates that vite recognizes the changed module
  • Browser console output from vite indicates that the component was hot updated
  • No content changes are actually visible in the browser until manual reload

System Info

No response

Any additional comments?

I submit this as a Vue issue because the behavior of vite seems correct, from simple observation.

The usage of a library with embedded Vue is helpful for sharing a self-contained "black box" featureset without dependency or structural conflict for the consuming app.

Related resources for reproduction example

The library app is a minimal Vue 3 app, built with Vite in library mode, visible here:

avatar
Dec 17th 2022

I don't think you should package the vue code into your library because it will overwrite some of the global variables that vue needs to use.

avatar
Dec 17th 2022

I don't think you should package the vue code into your library because it will overwrite some of the global variables that vue needs to use.

@zhangzhonghe, thank you for the reply. It is true that removing vue from the library package will avoid this conflict, but that also greatly weakens my real use case for this library pattern.

Following Vue's goal of being flexible and incrementally adoptable, packaging vue within the library has allowed me to provide a rich Vue-developed featureset to several different consumers, ranging from unstructured legacy web apps to modern framework apps using various versions of Angular, React, and Vue (as examples). Aside from the broken HMR at development time when consumed by a Vue 3 app, the pattern works quite well!

The major benefit of the embedded vue is similar to the spirit of Embedded Web Components, where we want to "leverage Vue in a completely consumer-agnostic fashion". Because Vue is a very modern and fast-evolving framework (which I love), the ability to generate an insulated output module is quite valuable to ensure the library can be consumed by applications which are slow to upgrade or have conflicting Vue version dependencies for an extended time.

I think it makes sense for Vue as "The Progressive Framework" to avoid conflict with itself in this usage, and it seems very close to working already. Are there any mechanisms for protecting the global variables you mention from interference? Perhaps a configuration or transformation to be applied to Vue in the library code or its build process?

avatar
Dec 19th 2022

Are there any mechanisms for protecting the global variables you mention from interference? Perhaps a configuration or transformation to be applied to Vue in the library code or its build process?

You can change the vite.config.ts to this:

export default defineConfig(({ mode }) => {
    return {
        plugins: [vue()],
        build: {
            lib: {
                entry: resolve(__dirname, "src/lib.js"),
                name: "SampleLibraryVueApp",
                fileName: "library",
            },
        },
        // to replace the process.env.NODE_ENV in the vue library
        define: {
            'process.env.NODE_ENV': JSON.stringify(mode)
        }
    };
});

This will ensure that the tree shakes off HMR-related code during build time. It might solve your problem.

avatar
Dec 19th 2022

This will ensure that the tree shakes off HMR-related code during build time. It might solve your problem.

I started thinking in this direction after your first comment, and a fresh skim of the code led me to one portion of global HMR registration in Vue's runtime-core/src/hmr.ts which looks like this:

if (__DEV__) {
  getGlobalThis().__VUE_HMR_RUNTIME__ = {
    // ... removed for brevity
  }
}

An inspection of my library's build output shows that __DEV__ is translated into process.env.NODE_ENV !== 'production', so it makes sense that this behavior is active when my consuming application is running on development server.

I don't know where exactly this __DEV__ replacement happens, but the result is clear enough to identify the issue as a mistake in my configuration of Vite rather than an issue with Vue (in fact, there are comments in the Vue code explaining the intent of the __DEV__ check to support proper tree-shaking at build time).

For my consumer-agnostic use case, I want my library to build in a production-like manner, without including any dynamic dev-time hooks, so I believe this resolution strategy is sound.

Your example modification of the vite.config.ts looks like a nice solution. I should have some time to try this soon, and I will report back here for documentation purposes and close my issue if it is successful.

Thank you again for taking the time to look, I appreciate the help and thoughtful response.

avatar
Dec 19th 2022

The recommended change in vite.config.ts worked to prevent my library from interfering with the consuming app's HMR operations. This makes my library build output closer to a production-like artifact, as I intended.

Thank you @zhangzhonghe, I am closing this issue!