# Frontend Plugins

Remotes loaded by Core UI


Build a Module Federation remote that the core-ui host loads at runtime.

For the generator and first run see [Create a Plugin](https://erxes.io/docs/create-plugin); for component reuse see [Shared UI Components](https://erxes.io/docs/plugin-ui-components); for host internals see [Frontend & Plugin Loading](https://erxes.io/docs/architecture-frontend).

## `src/config.tsx`: the `IUIConfig` object

Every remote exposes `./config`, which must export `CONFIG` typed as [`IUIConfig`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/frontend/libs/erxes-ui/src/types/UIConfig.ts) (from `erxes-ui`). The host loads it once at startup and builds navigation, routes, settings, and widgets from it:

| Field | Type | Purpose |
| --- | --- | --- |
| `name` | `string` | Remote name without `_ui`; the host loads exposes `./<name>` and `./<name>Settings`. |
| `path` | `string` | URL prefix; the host mounts `/<path>/*`. |
| `icon` | `React.ElementType` | Optional plugin icon (use `@tabler/icons-react`). |
| `navigationGroup` | `{ name, defaultPath?, icon, content(), subGroup?() }` | Sidebar group; `content`/`subGroup` render lazy nav components. |
| `settingsNavigation` | `() => ReactNode` | Entries under Settings. |
| `modules` | `{ name, icon?, path, hasAutomation?, hasRelationWidget?, hasFloatingWidget?, hasSegmentConfigWidget? }[]` | Feature modules; `path` is relative to `path`. |
| `widgets` | `{ relationWidgets?, customerDetailWidgets?, formWidgets?, propertyInputs?, activityRows? }` | Named widgets other modules render. |
| `searchProviders` | `ISearchProvider[]` | Global-search providers. |
| `i18n`, `i18nNamespace` | `boolean`, `string` | Translation namespace the host loads. |
| `hasFloatingWidget`, `settingsOnly` | `boolean` | Behavior flags. |

[sales_ui's config](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/frontend/plugins/sales_ui/src/config.tsx) is the worked example: `navigationGroup` with `defaultPath: 'sales/deals'` and a `subGroup`, four `modules`, `relationWidgets`, a `propertyInputs` map, and `searchProviders`.

## `module-federation.config.ts`

The generated config declares `name: '<name>_ui'` and four exposes:

```ts
exposes: {
  './config': './src/config.tsx',
  './inventory': './src/modules/InventoryMain.tsx',
  './inventorySettings': './src/modules/InventorySettings.tsx',
  './widgets': './src/widgets/Widgets.tsx',
},
```

The expose names `./<name>` and `./<name>Settings` are fixed, not a convention: the host's [`usePluginsRouter`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/frontend/core-ui/src/modules/app/hooks/usePluginsRouter.tsx) mounts `/<path>/*` to `loadRemote('<name>_ui/<name>')` and settings routes to `loadRemote('<name>_ui/<name>Settings')`. Rename or move an expose and the host 404s. Sales adds more exposes (`./relationWidget`, `./notificationWidget`, `./automationsWidget`, …) that other modules request by name.

The `shared` function returns the default config for the `coreLibraries` set (`react`, `react-dom`, `react-router`, `react-router-dom`, `erxes-ui`, `@apollo/client`, `jotai`, `ui-modules`, `react-i18next`) and `false` for everything else. These become singletons shared with the host; anything outside the set is bundled per-plugin.

`rspack.config.ts` composes `withNx()`, `withReact()`, and `withModuleFederation(config, { dts: false })`; `rspack.config.prod.ts` re-exports it. Both need their default exports; the Nx/Rspack tooling is the one place a default export is allowed.

## How the host loads remotes

- **Development**: `pnpm dev:uis` runs `nx serve core-ui --devRemotes="<name>_ui …"`. Each plugin's `serve` target (`@nx/rspack:module-federation-dev-server`) serves `remoteEntry.js` on its `project.json` port; core-ui's `remotes` list is generated from `ENABLED_PLUGINS` as `<name>_ui` names.
- **Production**: core-ui's `bootstrap.tsx` fetches `${REACT_APP_API_URL}/get-frontend-plugins`, which returns `{ name, entry }` objects pointing at `https://plugins.erxes.io/<releaseVersion>/<name>_ui/remoteEntry.js`, and calls `init({ remotes })` at runtime. The nginx config disables caching on `remoteEntry.js`.

At runtime [`RenderPluginsComponent`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/frontend/core-ui/src/plugins/components/RenderPluginsComponent.tsx) calls `loadRemote('<pluginName>/<remoteModuleName>')` and finds a component from the module using a candidate-key list (`default`, PascalCase name, `<Name>Component`, `<Name>RemoteEntry/RemoteEntries/Widget`, `AutomationRemoteEntries`). All exposed modules must still use named exports; never rely on `default` to satisfy the loader.

## Routing inside the remote

The host mounts `/<path>/*`; your exposed main component serves everything below it with its own `<Routes>`:

```tsx
// generated pattern — sales does the same with /deals and /pos
export const InventoryMain = () => (
  <Suspense fallback={<Spinner />}>
    <Routes>
      <Route index element={<Navigate to="items" replace />} />
      <Route path="/items" element={<IndexPage />} />
    </Routes>
  </Suspense>
);
```

Keep `config.tsx` `path`, `modules[].path`, `navigationGroup.defaultPath`, and these routes aligned; a path the config advertises but no route serves renders an empty remote.

## i18n

Set `i18n: true` (namespace = `name`) or `i18nNamespace: '<ns>'` in `CONFIG`. The host calls `i18n.loadNamespaces(ns)`, which fetches `${REACT_APP_API_URL}/locales/<lng>/<ns>.json`. The gateway serves those from `backend/gateway/src/locales/<lng>/<file>` and falls back to asking each plugin API for `/locales/<lng>/<file>`, so a plugin can serve its own namespace by mounting a `/locales` route through `startPlugin`'s `expressRouter`. `react-i18next` is a shared singleton, so `useTranslation()` works directly in plugin code.

## Data, state, and the real-time rule

- Apollo Client (shared singleton) for server state; keep GraphQL documents next to the feature and prefix operation names with the plugin or module.
- Jotai for plugin-wide client state, React state for component-local state.
- React Hook Form + Zod for forms; provide loading, empty, success, and error states. No button without a handler.
- After any create/update/delete, update the Apollo cache, refetch the affected query, or `subscribeToMore`; the UI must never require a manual refresh.
- Lazy-load exposed modules inside `Suspense`.

## Ports

The generated `project.json` serve port is `3005`, which `sales_ui` already uses. Ports already in use: `3002`–`3011` (content `3003`, frontline `3004`, sales `3005`, operation `3006`, the rest held by Enterprise Edition remotes) plus host `3001`. Pick a free port.

