From a174a00af8dbf93b171c71526be1157e4f42efd9 Mon Sep 17 00:00:00 2001 From: rvveber Date: Tue, 4 Nov 2025 11:43:27 +0100 Subject: [PATCH] improve documentation accuracy --- docs/frontend-plugins.md | 89 ++++++++++++++++++++++------------------ 1 file changed, 50 insertions(+), 39 deletions(-) diff --git a/docs/frontend-plugins.md b/docs/frontend-plugins.md index cf7cb71a..54cd958e 100644 --- a/docs/frontend-plugins.md +++ b/docs/frontend-plugins.md @@ -19,11 +19,9 @@ The plugin system allows developers to extend the application's functionality an - Build and version plugins independently. **Limitations:** -- Primarily for UI additions/modifications. -- Libraries used, that the host already provides, should match host versions to allow singleton behavior. -- Cannot extend Next.js-specific features like pages. -- No direct access to host state (only few exceptions where a global intermediary cache is used e.g. react-queries). -- Host updates may require plugin checks/updates for DOM/component changes (usually minimal). +- Focused on DOM/UI customisations; you cannot add Next.js routes or other server features. +- Runs client-side without direct host state access; shared caches (e.g. React Query) only work when the dependency is also shared as a singleton. +- Host upgrades may require tweaking selectors and matching versions for libraries the host already provides. ### Overview This diagram shows the plugin integration flow: fetching config and plugins, checking visibility, starting DOM observation, conditionally rendering components, and re-checking on DOM changes. @@ -58,10 +56,10 @@ Plugins are configured via a JSON file (e.g., `impress/configuration/plugins/def | `injection`| Object | Yes | Integration control. | | - `target` | String | Yes | CSS selector for insertion point. | | - `position` | String | No (default: "append") | Insertion position (`before`, `after`, `replace`, `prepend`, `append`). | -| - `observerRoots` | String/Boolean | No | DOM observation: CSS selector, `true` (full page), `false` (none). | +| - `observerRoots` | String/Boolean | No | DOM observation: CSS selector, `true` (default; observe whole document), `false` (disable observers). | | `props` | Object | No | Props passed to the plugin component. | | `visibility` | Object | No | Visibility controls. | -| - `routes` | Array | No | Path globs (e.g., ["/docs/*", "!/docs/secret*"]); supports negation (`!`). | +| - `routes` | Array | No | Path globs (e.g., `["/docs/*", "!/docs/secret*"]`); supports `*` and `?` wildcards plus negation (`!`). | ### Example @@ -69,7 +67,7 @@ Plugins are configured via a JSON file (e.g., `impress/configuration/plugins/def { "id": "my-custom-component-1", "remote": { - "url": "localhost:3001/remoteEntry.js", + "url": "http://localhost:3001/remoteEntry.js", "name": "my-plugin", "module": "./MyCustomComponent" }, @@ -90,15 +88,16 @@ Plugins are configured via a JSON file (e.g., `impress/configuration/plugins/def ### Key Notes - `remote` and `injection` are required. - - `remote.url` can be relative if the plugins compiled `remoteEntry.js` is placed in the hosts public folder (e.g. via k8s ConfigMap) + - `remote.url` can be relative if the plugin's compiled `remoteEntry.js` is placed in the host's public folder (e.g. via k8s ConfigMap) ```diff - "url": "http://localhost:3001/remoteEntry.js", + "url": "/plugins/my-plugin/remoteEntry.js", ``` - Use `target`/`position` for flexible placement (e.g., replace or append). -- `observerRoots` enables re-injection if target lost through DOM changes
(e.g., components re-rendering/un-mounting)
It should be a selector to the closest stable ancestor
(e.g. `"#list"` if list is a dynamically populated container). +- `observerRoots` controls DOM observation for reinjection. Prefer the closest stable ancestor selector (e.g. `"#list"`); leaving it as `true` watches the whole document and is noisier. - Restrict visibility with `routes` globs and negations. - Pass custom data via `props`. +- Plugins execute on the client only; avoid assumptions that rely on server-side rendering. #### `injection.position` @@ -206,7 +205,7 @@ Each shows the relevant JSON config and the resulting HTML structure after injec ## Development Guide ### Environment Variables -Set `NEXT_PUBLIC_DEVELOP_PLUGINS=true` in `.env.development` for debug logs, type-sharing, and hot-reload support. This also logs all exposed modules with exact versions on startup, helping match plugin dependencies. +Set `NEXT_PUBLIC_DEVELOP_PLUGINS=true` in `.env.development` for debug logs, type-sharing, and hot-reload support. The Next.js dev server prints the resolved exposes and shared singleton versions on startup, helping match plugin dependencies. ### Type-Sharing for Intellisense In plugin `tsconfig.json`: @@ -218,7 +217,7 @@ In plugin `tsconfig.json`: } } ``` -Types update on build for compatibility and autocompletion. +Types update on build for autocompletion: with `NEXT_PUBLIC_DEVELOP_PLUGINS=true` the host serves `/_next/static/chunks/@mf-types.zip`, and the sample `NativeFederationTypeScriptHost` in `webpack.config.js` unpacks it so the `@mf-types/*` aliases resolve locally. ### Exports Support The host automatically exposes components and some features under the same structure that is used in docs code. @@ -239,11 +238,11 @@ import { Icon } from 'impress/components'; - The host logs warnings for any naming conflicts during build ### Recommended Workflow -2. Enable NEXT_PUBLIC_DEVELOP_PLUGINS and start host(docs) & plugin dev servers in parallel. -3. Configure federation in plugin `webpack.config.js` and expose components (see Examples). -4. Develop with hot-reload; use host components via shared types. -5. Test in host via config; debug with logs. -6. Version and deploy independently. +1. Enable `NEXT_PUBLIC_DEVELOP_PLUGINS` and start host (docs) & plugin dev servers in parallel. +2. Configure federation in plugin `webpack.config.js` and expose components (see Examples). +3. Develop with hot-reload; use host components via shared types. +4. Test in host via config; debug with logs. +5. Version and deploy independently. ### Integration in Host 1. Build plugin and host `remoteEntry.js`. @@ -252,7 +251,7 @@ import { Icon } from 'impress/components'; 4. Verify via `[PluginSystem]` logs. ### Debugging -Enable `NEXT_PUBLIC_DEVELOP_PLUGINS=true` for console logs like `[PluginSystem] ...`. +With `NEXT_PUBLIC_DEVELOP_PLUGINS=true`, `[PluginSystem]` console logs surface load, inject, and error events. Common Errors: | Issue | Cause/Fix | @@ -267,20 +266,15 @@ Errors are isolated via host ErrorBoundary. ## Best Practices and Security ### Best Practices -- Keep components modular and self-contained. -- Use props for flexibility/reusability. -- Document interfaces and expected props. -- Version plugins for host compatibility. -- Avoid internal host dependencies; use official types/components. -- Test regularly in host; monitor logs. -- Update federation config with host changes. +- Build modular components with well-typed props so host teams can plug them in safely. +- Use the host's exposed types/components rather than private internals. +- Keep shared dependency versions aligned with the host and retest after upgrades. +- During development run the plugin and host dev servers in parallel so you benefit from hot reload and live Module Federation. ### Security and Isolation -- Plugins run isolated with ErrorBoundaries; errors don't crash host. -- No direct host state access; use interfaces/props. -- Vet third-party libraries for security. -- Check plugin functionality after host updates. -- Avoid unsafe external scripts/resources. +- Plugins run behind ErrorBoundaries, keeping failures isolated but still worth monitoring. +- Host state is inaccessible; rely on props and exposed APIs. +- Treat plugin bundles as untrusted: vet dependencies, avoid unsafe external scripts, and re-test after host changes. ## Examples @@ -288,8 +282,8 @@ Errors are isolated via host ErrorBoundary. Define `moduleFederationConfig` first for reuse: ```js -const { ModuleFederationPlugin } = require('webpack').container; -const NativeFederationTypeScriptHost = require('@module-federation/native-federation-typescript/host'); +const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin'); +const { NativeFederationTypeScriptHost } = require('@module-federation/native-federation-typescript/webpack'); const moduleFederationConfig = { name: 'my-plugin', @@ -304,23 +298,40 @@ const moduleFederationConfig = { react: { singleton: true }, 'react-dom': { singleton: true }, 'styled-components': { singleton: true }, - 'cunningham-react': { singleton: true }, + '@openfun/cunningham-react': { singleton: true }, + '@tanstack/react-query': { singleton: true }, }, }; -module.exports = { - plugins: [ - new ModuleFederationPlugin(moduleFederationConfig), - ...(dev ? [NativeFederationTypeScriptHost({ moduleFederationConfig })] : []), - ] +module.exports = (env, argv) => { + const dev = argv.mode !== 'production'; + + return { + entry: './src/index.tsx', + plugins: [ + new ModuleFederationPlugin(moduleFederationConfig), + ...(dev ? [NativeFederationTypeScriptHost({ moduleFederationConfig })] : []), + ], + }; }; ``` -Adjust names/paths; declare shared libs as singletons. Use relative remotes if the plugin `remoteEntry.js` lives in host's public folder (e.g., public/plugins/my-plugin/remoteEntry.js). +Adjust names/paths; declare shared libs as singletons. Use relative remotes if the plugin `remoteEntry.js` lives in host's public folder (e.g., `public/plugins/my-plugin/remoteEntry.js`). ```diff - impress: 'impress@localhost:3000/_next/static/chunks/remoteEntry.js' + impress: 'impress@/_next/static/chunks/remoteEntry.js', ``` +### Choosing Shared Dependencies + +The `shared` configuration in `webpack.config.js` is critical for performance and stability. Here’s a guide to help you decide which dependencies to share: + +- **Minimal Shared Libraries**: Your plugin should always share `react`, `react-dom`, `styled-components`, and `@openfun/cunningham-react`. This ensures that your plugin uses the same core libraries as the host, preventing version conflicts and unnecessary duplication. + +- **Sharing State**: Libraries that rely on a global cache or context (e.g. `@tanstack/react-query`) must be shared. If each bundle ships its own copy, the React Query context and cache live in parallel universes—the plugin's hooks will not see the host's `QueryClient`, forcing duplicate fetches or even errors. Align your plugin's dependency version with the host so the Module Federation singleton can reuse the host instance instead of bundling a second copy. + +- **Expanding Shared Libraries**: Before adding a new library to your plugin, check if the host already provides it. Running the host with `NEXT_PUBLIC_DEVELOP_PLUGINS=true` prints the effective `moduleFederationConfig.shared` map (singletons + versions) to the Next.js dev server logs on startup, so you can align your plugin's `package.json` without guessing. Reusing host-provided libraries keeps the plugin lightweight and avoids duplicate bundles. + + For plugin config example, see Configuration section. ## Summary