vite icon indicating copy to clipboard operation
vite copied to clipboard

react: Uncaught TypeError: factory is not a function

Open PaRoxUs opened this issue 1 year ago • 19 comments

Everythign works in dev mode, but if you build without adding:

        exposes: {
          './App': './src/App', // Needed for the build to work
        },

the app won't work and you get Uncaught TypeError: factory is not a function

PaRoxUs avatar Dec 04 '24 09:12 PaRoxUs

hi @PaRoxUs which framework are you using? can you create a basic reproduction please?

gioboa avatar Dec 04 '24 10:12 gioboa

We are using react, I'll try my best to create an example, but it has to wait ATM.

PaRoxUs avatar Dec 06 '24 12:12 PaRoxUs

Got this error once, the cause was that I was using vite-plugin-top-level-await plugin and building for target >chrome87, which doesn't need this plugin.

sougiovn avatar Dec 19 '24 22:12 sougiovn

@PaRoxUs did you solve this issue?

gioboa avatar Jan 08 '25 08:01 gioboa

Well, it works if we add the App to the build and I did't have time to create an example with the error.

PaRoxUs avatar Jan 08 '25 08:01 PaRoxUs

I'll try to recreate it, so from the remote you are only exposing the components, right?

gioboa avatar Jan 08 '25 08:01 gioboa

Its our main app that only host remote components, so it dosen't need to expose any components.

PaRoxUs avatar Jan 08 '25 08:01 PaRoxUs

Its our main app that only host remote components, so it dosen't need to expose any components.

Can you share the configurations here please?

gioboa avatar Jan 08 '25 08:01 gioboa

import { defineConfig, UserConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { federation } from '@module-federation/vite'
import { dependencies } from './package.json'
import dotenv from 'dotenv'

dotenv.config({
  path: '.env.' + process.env.ENV,
})

export default defineConfig((): UserConfig => {
  return {
    base: process.env.VITE_APP_BASE,
    plugins: [
      react(),
      federation({
        name: 'main',
        filename: 'main.js',
        shared: {
          react: {
            requiredVersion: dependencies.react,
            singleton: true,
          },
          'react-dom': {
            requiredVersion: dependencies['react-dom'],
            singleton: true,
          },
          jotai: {
            requiredVersion: dependencies.jotai,
            singleton: true,
          },
          'react-router-dom': {
            requiredVersion: dependencies['react-router-dom'],
            singleton: true,
          },
          'react-hook-form': {
            requiredVersion: dependencies['react-hook-form'],
            singleton: true,
          },
          '@tanstack/react-query': {
            requiredVersion: dependencies['@tanstack/react-query'],
            singleton: true,
          },
          '@tanstack/react-table': {
            requiredVersion: dependencies['@tanstack/react-table'],
            singleton: true,
          },
        },
        exposes: {
          './App': './src/App',
        },
        remotes: {
          components: {
            type: 'module',
            name: 'components',
            entry: process.env.VITE_REMOTE_URL_COMPONENTS,
            entryGlobalName: 'components',
            shareScope: 'default',
          },


        },
      }),
    ],
    server: {
      host: 'localhost',
      origin: process.env.VITE_APP_BASE,
      port: 5170,
      proxy: {
        '/api': {
          target: 'http://localhost:8054',
          changeOrigin: true,
        },
      },
    },
    build: {
      modulePreload: false,
      target: 'esnext',
      minify: false,
    },
  }
})

PaRoxUs avatar Jan 08 '25 09:01 PaRoxUs

you should solve the issue with modulepreload: true it's the default btw

gioboa avatar Jan 08 '25 15:01 gioboa

Hey @gioboa Here is our case (Vue 3.5.13, Vite 6.0.7) with the same behavior: if we don't expose App we get "TypeError: factory is not a function". So, it's not react-specific. My guess: there is something with wrong order of imports if we don't expose App.

import { fileURLToPath, URL } from 'node:url'
import { existsSync } from 'node:fs'

import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import components from 'unplugin-vue-components/vite'
import autoImport from 'unplugin-auto-import/vite'
import { federation } from '@module-federation/vite'
import unheadVite from '@unhead/addons/vite'

import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
//@ts-expect-error
import { StudioUiResolver } from '@appmaster/studio-ui/resolver/index'

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '')

  return {
    base: env.VITE_BASE_URL,
    server: {
      port: 8080,
      proxy: {
        '/api': { target: 'https://[MASKED]' },
      },
    },
    preview: {
      proxy: {
        '/api': { target: 'https://[MASKED]' },
      },
    },
    optimizeDeps: {
      include: ['qs', 'tailwindcss/plugin', 'naive-ui'],
    },
    build: {
      target: 'esnext',
      sourcemap: true,
    },
    resolve: {
      alias: {
        '@': fileURLToPath(new URL('./src', import.meta.url)),
      },
    },
    plugins: [
      vue(),
      unheadVite(),
      components({
        extensions: ['vue'],
        include: [/\.vue$/, /\.vue\?vue/],
        directoryAsNamespace: true,
        resolvers: [
          NaiveUiResolver(),
          StudioUiResolver({ existsSync, sideEffects: env.NODE_ENV === 'development' }),
        ],
      }),
      autoImport({
        dirs: ['src/use', 'src/store', 'src/utils', 'src/types'],
        imports: [
          'vue',
          'vue-i18n',
          'vue-router',
          {
            'naive-ui': ['useMessage'],
            '@vueuse/rxjs': ['useSubscription'],
          },
        ],
      }),
      federation({
        name: 'base-app',
        filename: 'remoteEntry-[hash].js',
        manifest: true,
        // Uncomment to fix factory is not a function error
        // exposes: {
        //   './App': './src/App.vue',
        // },
        remotes: {
          'mod-auth': {
            type: 'module',
            name: 'mod-auth',
            entry: '/auth/mf-manifest.json',
          },
          'mod-workspace': {
            type: 'module',
            name: 'mod-workspace',
            entry: '/workspace/mf-manifest.json',
          },
        },
        shared: {
          axios: { singleton: true },
          vue: { singleton: true },
          'vue-i18n': { singleton: true },
          'vue-router': { singleton: true },
          dayjs: { singleton: true },
        },
      }),
    ],
  }
})

OlegSotnikov avatar Jan 08 '25 22:01 OlegSotnikov

It looks like in our particular case we have an issue with unplugin-auto-import If we remove it and use manual imports across the project it starts working.

OlegSotnikov avatar Jan 09 '25 07:01 OlegSotnikov

you should solve the issue with modulepreload: true it's the default btw

Did not help. Actually I came across this issue #78 (which has been closed, but I guess need to re-open it) and it seems to be very similar w.r.t this issue. I've shared my findings there and I think it might be helpful for debugging this.

tejas-1206 avatar Mar 19 '25 11:03 tejas-1206

but @PaRoxUs @OlegSotnikov , why do we need to expose the entire App in the first place ? 🤔

tejas-1206 avatar Mar 19 '25 11:03 tejas-1206

@PaRoxUs @tejas-1206 @OlegSotnikov

I hit this recently but it was due to mismatched app names causing multiple FederatedHosts to exists and using the one without shared items.

If you are able to give a reprod/demo to your case it could be helpful. What I can say is that factory comes from the sharing code which means at that moment the shared map is empty.

pcfreak30 avatar Jul 27 '25 14:07 pcfreak30

but @PaRoxUs @OlegSotnikov , why do we need to expose the entire App in the first place ? 🤔

Did you read through the whole issue? The build fails without including it.

PaRoxUs avatar Jul 27 '25 16:07 PaRoxUs

can confirm this same issue - was able to fix it by doing the mentioned expose in OP

rrussell0 avatar Oct 23 '25 01:10 rrussell0

but @PaRoxUs @OlegSotnikov , why do we need to expose the entire App in the first place ? 🤔

Did you read through the whole issue? The build fails without including it.

Sorry for the late response, but I did. I did. But if I only want to expose certain parts of my app and not the entire thing, how would I do that then ? That was what I meant. Why expose the entire App ? Why not only the exact parts of the App, if both remote and host run on the same react, ts, UI lib versions ?

tejas-1206 avatar Oct 23 '25 05:10 tejas-1206

but @PaRoxUs @OlegSotnikov , why do we need to expose the entire App in the first place ? 🤔

Did you read through the whole issue? The build fails without including it.

Sorry for the late response, but I did. I did. But if I only want to expose certain parts of my app and not the entire thing, how would I do that then ? That was what I meant. Why expose the entire App ? Why not only the exact parts of the App, if both remote and host run on the same react, ts, UI lib versions ?

I do this just fine. See https://github.com/LumeWeb/web/blob/31f29d40d6a725654b452c643ec1c314080e2799/libs/portal-plugin-dashboard/plugin.config.ts and im the one whos been working to improve stuff like https://github.com/module-federation/vite/pull/330

Ive had these issues before and it can be advanced to solve them, but it was usually a design/setup error, though it took me a lot of debugging to trace.

If you are able to create a somewhat minimal repro, then we might be able to help.

pcfreak30 avatar Oct 23 '25 05:10 pcfreak30