0

I am building module vue.js app that is composed from different Vite/Vue projects.

The workflow - to have Theme plugin / **Menu **plugin which can be dynamically applied within vue app.

The components renders at the root level of the app, but it fails to render when nested. (Theme component renders Menu component), however it appears in DOM as just component name....

I have tried to create separate vite/vue apps, build them, and then manually load them within index.html.

ThemePlugin project

  • theme/vite.config.ts
import { fileURLToPath, URL } from "node:url";

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue(), vueJsx()],
  build: {
    rollupOptions: {
      output: {
        dir: "./dist",
        entryFileNames: "plugin.js",
        assetFileNames: "plugin.css",
        chunkFileNames: "chunk.js",
        manualChunks: undefined,
      },
    },
  },
  resolve: {
    alias: {
      "@": fileURLToPath(new URL("./src", import.meta.url)),
    },
  },
});

  • theme/ThemeWrapper.vue
<template>
  <div class="vite-theme-plugin-build">
    <MenuLauncher></MenuLauncher>
    <div style="color:red" v-if="test1">Is Rendered from Theme Wrapper</div>
  </div>
</template>

<script lang="ts">
import type { App, Component } from 'vue';

type Framework = {
  app: App;
  externalComponents: {
    ThemeWrapper: Component;
    MenuLauncher: Component;
  };
};
const w = window as unknown as Framework;

console.log(w);

export default {
  name: "ThemeWrapper",
  components: {
    MenuLauncher: w.externalComponents.MenuLauncher
  },
  setup() {
    return {
      test1: true,
    };
  },
};
</script>
  • theme/main.ts
import type { App, Component } from "vue";
import ThemeWrapperVue from "./ThemeWrapper.vue";

type Framework = {
  app: App;
  externalComponents: {
    ThemeWrapper: Component;
    MenuLauncher: Component;
  };
};

const w = window as unknown as Framework;
w.externalComponents = w.externalComponents || {};
w.externalComponents.ThemeWrapper = ThemeWrapperVue;

MenuPlugin project

  • menu/vite.config.ts
Identical to the ThemePlugin project configuration
  • menu/MenuLauncher.vue
<template>
  <div class="vite-menu-plugin-build">
    <div>
      This is dynamic menu launcher!
    </div>
    <div style="color:blue" v-if="test1">Is Rendered from MenuLauncher</div>
  </div>
</template>

<script lang="ts">
export default {
  name: "MenuLauncher",
  setup() {
    return {
      test1: true,
    };
  },
};
</script>

  • menu/main.ts
import type { App, Component } from "vue";

import MenuLauncherVue from "./MenuLauncher.vue";

type Framework = {
  app: App;
  externalComponents: {
    ThemeWrapper: Component;
    MenuLauncher: Component;
  };
};

const w = window as unknown as Framework;

w.externalComponents = w.externalComponents || {};
w.externalComponents.MenuLauncher = MenuLauncherVue;



index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Test</title>
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
    <script  src="./menu/dist/plugin.js"></script>
    <script  src="./theme/dist/plugin.js"></script>
  </head>
  <body>
    <div id="app"></div>

    <script>
      document.addEventListener("DOMContentLoaded", (event) => {
        window.app = Vue.createApp({
          setup() {
            return {};
          },
          template: `
        <div>
            <div style="border: 1px solid gainsboro; margin:1rem;">
              <div>ThemeWrapper component doesn't load MenuLauncher component....</div>
              <ThemeWrapper></ThemeWrapper>
            </div>
            <div style="border: 1px solid gainsboro; margin:1rem;">
              <div>Root component is able to render MenuLauncher... But above "ThemeWrapper" component cannot render it Why???</div>
              <MenuLauncher></MenuLauncher>    
            </div>
        </div>`,
        });
        window.app.component('MenuLauncher', window.externalComponents.MenuLauncher)
        window.app.component('ThemeWrapper', window.externalComponents.ThemeWrapper)
        window.app.mount("#app");
      });
    </script>
  </body>
</html>

Preview of end result: enter image description here

Desired result: MenuLauncherVue component should be able to render within ThemeWrapper component.

5
  • 1
    You have multiple "Vue" instances. Don't mix imports and CDN Commented Apr 24, 2024 at 17:39
  • Thanks for your response. Any suggestions how could I have single instance shared between 2 projects? Commented Apr 25, 2024 at 8:07
  • 1
    Through window. Since you rely on cdn's window.Vue, make it be used instead of imported one in the plugin any any other bundles. If you don't want to replace imports with globals in code, it's something like that github.com/vitejs/vite/discussions/3744 Commented Apr 25, 2024 at 8:16
  • I have added CDN link for this example only. But in the the prod app, the idea would be that we would have globally available dependencies for the whole app, and the modular Theme / Menu plugins would re-use them. The end goal is to instead of calling each time vite build to bundle Theme / Menu plugin is to do it only once. And the backend would scaffold the application from pre-built packages. E.g. - Theme plugin version released , compiled. - Menu plugin version released, compiled. Backend then wouldn't recompile them each time, but just re-use builds... Commented Apr 25, 2024 at 8:43
  • Makes sense. Looks like the case for module federation. Or do window.Vue = Vue in main app instead of cdn and stick to mapping vue import in plugins as in the previous comment Commented Apr 25, 2024 at 8:50

2 Answers 2

2
+200

I'm in no way an expert in vue, but I found this quite interesting. So after reproducing your code, it seems like the following line will only register the component on build time:

  components: {
    MenuLauncher: w.externalComponents.MenuLauncher
  },

So I thought maybe we could use something like defineAsyncComponent, but I tried using <component :is="" /> and it seems like it suffices for this requirement (it seems to be able to register on run time, not build time). So in template, instead of using this:

<MenuLauncher></MenuLauncher>

We can use this:

<component :is="menuLauncherComponent"></component>

Then in the script part, i'm not sure why we can't use ref for menuLauncherComponent, so we can just use the good ol' options API, like this:

  data() {
    return {
      menuLauncherComponent: null,
    };
  },
  ...
  mounted() {
    this.menuLauncherComponent = (window as any).externalComponents.MenuLauncher; // register the component here
  },

So the full code for ThemeWrapper.vue can be something like this:

<template>
  <div class="vite-theme-plugin-build">
    <component :is="menuLauncherComponent"></component>
    <div style="color: red" v-if="test1">Is Rendered from Theme Wrapper</div>
  </div>
</template>

<script lang="ts">
import type { App, Component } from "vue";

type Framework = {
  app: App;
  externalComponents: {
    ThemeWrapper: Component;
    MenuLauncher: Component;
  };
};

export default {
  name: "ThemeWrapper",
  data() {
    return {
      menuLauncherComponent: null,
    };
  },
  setup() {
    return {
      test1: true,
    };
  },
  mounted() {
    this.menuLauncherComponent = (window as any).externalComponents.MenuLauncher; // register the component here
  },
};
</script>

I tested this on this repository.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for having look into this. I have found a workaround that allows me to have the desired flow. Something is wrong with es bundle or I don't understand what makes it not to work in my desired setup, however bundling to umd seemed to solve problem.
1

After lots of reading and trying I have found solution that worked for me.

Posting here in case anyone would be facing similar problem.

Vite configiguration for Theme / Menu plugins:

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  build: {
    rollupOptions: {
      external: ["vue"],
      output: {
        dir: "./dist",
        // entryFileNames: "plugin.js",
        // assetFileNames: "plugin.css",
        // chunkFileNames: "chunk.js",
        // manualChunks: undefined,
        globals: {
          vue: "Vue",
        },
      },
    },
    lib: {
      name: 'menu',
      formats: ["umd", "iife"],
      entry: "src/main.ts",
    },
  },
});

Notice 2 important things:

  1. rollupOptions.outout.globals indicates that Vue will be available globally.
  2. lib setting was set to build dist in formats of ["umd", "iife"].(If we would set format to be es then we run into the error when the component is not being rendered...)

It might still be some kind of bug, I would prefer to use es bundle, however I have no idea why it doesn't work.

Full example if anyone needs: https://github.com/ninode97/vue-multi-example

3 Comments

Consider giving the bounty to Damzaky because it gonna be lost anyway for you + he/she made some effort to help you.
Good point. I thought I have assigned it, thanks for flagging it.
I might not deserve it, but thanks!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.