App Router no soportado

Descripción general de la integración con Next.js

Proyecto obsoleto

El soporte para Next.js está llegando a su fin más información

Este plugin habilita Module Federation en Next.js.

Compatibilidad

  • next ^15 || ^14 || ^13 || ^12
  • Renderizado en el servidor (SSR)
  • Pages Router

Te recomiendo encarecidamente tomar como referencia esta aplicación, que aprovecha las mejores capacidades: https://github.com/module-federation/module-federation-examples

Requisitos

Este plugin establece internamente process.env.NEXT_PRIVATE_LOCAL_WEBPACK = 'true', pero es mejor definirlo en el entorno o exportarlo desde la línea de comandos.

"Webpack local" significa que debes tener webpack instalado como dependencia, y que Next no usará su copia empaquetada de webpack, que no sirve porque necesito acceder a todos los elementos internos de webpack

  • cross-env NEXT_PRIVATE_LOCAL_WEBPACK=true next dev o next build
  • npm install webpack

Uso

import React, { lazy } from 'react';
const SampleComponent = lazy(() => import('next2/sampleComponent'));

Para evitar errores de hidratación, usa React.lazy en lugar de next/dynamic para la carga diferida de componentes federados.

Consulta la implementación aquí: https://github.com/module-federation/module-federation-examples/tree/master/nextjs-v13/home/pages

Con un límite asíncrono instalado a nivel de página, puedes hacer lo siguiente

const SomeHook = require('next2/someHook');
import SomeComponent from 'next2/someComponent';

Demo

Puedes verlo en acción aquí: https://github.com/module-federation/module-federation-examples/tree/master/nextjs-ssr

Opciones

Este plugin funciona exactamente igual que ModuleFederationPlugin; úsalo como lo harías normalmente. Ten en cuenta que ya compartimos automáticamente por ti lo relacionado con react y next.

Además, NextFederationPlugin tiene su propio argumento opcional extraOptions, con el que puedes desbloquear funcionalidades adicionales de este plugin:

new NextFederationPlugin({
  name: '',
  filename: '',
  remotes: {},
  exposes: {},
  shared: {},
  extraOptions: {
    debug: boolean, // `false` by default
    exposePages: boolean, // `false` by default
    enableImageLoaderFix: boolean, // `false` by default
    enableUrlLoaderFix: boolean, // `false` by default
    skipSharingNextInternals: boolean, // `false` by default
    scriptTimeout: number // `8000` by default
  },
});
  • debug – activa el modo de depuración. Imprime información adicional sobre lo que ocurre internamente.
  • exposePages – expone automáticamente todas las páginas de Next.js y su ./pages-map.
  • enableImageLoaderFix – añade el hostname público a todos los assets empaquetados por nextjs-image-loader. Así, si sirves remoteEntry desde http://example.com, todos los assets empaquetados obtendrán ese hostname en runtime. Es algo parecido a la Base URL de HTML, pero para módulos federados.
  • enableUrlLoaderFix – añade el hostname público a todos los assets empaquetados por url-loader.
  • skipSharingNextInternals – desactiva la compartición de los elementos internos de Next. Puedes usarlo si quieres compartir tú mismo los elementos internos de Next o si quieres usar este plugin en aplicaciones que no son Next
  • scriptTimeout – el tiempo de espera, en milisegundos, del script de entrada del remote en el navegador para cada intento de carga. Por defecto es 8000; los valores que no son números finitos positivos vuelven a este valor por defecto. Auméntalo para conexiones lentas y usa @module-federation/retry-plugin para reintentar los fallos transitorios. Esta opción no activa reintentos ni cambia los tiempos de espera de carga en el servidor.

Next.js obtiene de mf-manifest.json los campos de assets de los exposes que usa el flushChunks() legacy, sin recorrer adicionalmente los stats de Webpack, de modo que las aplicaciones con versiones anteriores siguen funcionando durante las actualizaciones progresivas. Las herramientas personalizadas que leen otros campos legacy de los stats deberían migrar a mf-manifest.json; configurar manifest: false conserva la salida legacy completa.

Demo

Puedes verlo en acción aquí: https://github.com/module-federation/module-federation-examples/pull/2147

Implementar el plugin

  1. Usa NextFederationPlugin en el next.config.js de la app desde la que quieres exponer módulos. La llamaremos "next2".
// next.config.js
// either from default
const NextFederationPlugin = require('@module-federation/nextjs-mf');

module.exports = {
  webpack(config, options) {
    const { isServer } = options;
    config.plugins.push(
      new NextFederationPlugin({
        name: 'next2',
        remotes: {
          next1: `next1@http://localhost:3001/_next/static/${
            isServer ? 'ssr' : 'chunks'
          }/remoteEntry.js`,
        },
        filename: 'static/chunks/remoteEntry.js',
        exposes: {
          './title': './components/exposedTitle.js',
          './checkout': './pages/checkout',
        },
        shared: {
          // whatever else
        },
      }),
    );

    return config;
  },
};
// next.config.js

const NextFederationPlugin = require('@module-federation/nextjs-mf');

module.exports = {
  webpack(config, options) {
    const { isServer } = options;
    config.plugins.push(
      new NextFederationPlugin({
        name: 'next1',
        remotes: {
          next2: `next2@http://localhost:3000/_next/static/${
            isServer ? 'ssr' : 'chunks'
          }/remoteEntry.js`,
        },
      }),
    );

    return config;
  },
};
  1. Usa react.lazy, la API de bajo nivel o require/import from para importar remotes.
import React, { lazy } from 'react';

const SampleComponent = lazy(() =>
  window.next2.get('./sampleComponent').then((factory) => {
    return { default: factory() };
  }),
);

// or

const SampleComponent = lazy(() => import('next2/sampleComponent'));

//or

import Sample from 'next2/sampleComponent';

RuntimePlugins

Para aportar extensibilidad y "middleware" a la federación, puedes consultar @module-federation/enhanced/runtime

// next.config.js
new NextFederationPlugin({
  runtimePlugins: [require.resolve('./path/to/myRuntimePlugin.js')],
});

Utilidades

loadRemote se ha eliminado; puedes aprovechar las nuevas APIs de runtime: https://module-federation.io/guide/runtime/runtime-api.html#loadremote

revalidate

Habilita la recarga en caliente del servidor Node (no del cliente) en producción. Se recomienda: sin ella, los servidores no podrán obtener las actualizaciones de los remotes sin un reinicio completo.

Más información aquí: https://github.com/module-federation/nextjs-mf/tree/main/packages/node#utilities

// __document.js

import { revalidate } from '@module-federation/nextjs-mf/utils';
import Document, { Html, Head, Main, NextScript } from 'next/document';
class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);

    // can be any lifecycle or implementation you want
    ctx?.res?.on('finish', () => {
      revalidate().then((shouldUpdate) => {
        console.log('finished sending response', shouldUpdate);
      });
    });

    return initialProps;
  }
  render() {
    return (
      <Html>
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

Para Express.js

La recarga en caliente de Express.js requiere pasos adicionales: https://github.com/module-federation/core/blob/main/packages/node/README.md

¿Qué se comparte por defecto?

Internamente compartimos de forma automática algunos elementos internos de Next. No necesitas compartir estos paquetes; compartir tú mismo los elementos internos de Next provocará errores.

Haz clic para ver DEFAULT_SHARE_SCOPE:
export const DEFAULT_SHARE_SCOPE: SharedObject = {
  'next/dynamic': {
    requiredVersion: undefined,
    singleton: true,
    import: undefined,
  },
  'next/head': {
    requiredVersion: undefined,
    singleton: true,
    import: undefined,
  },
  'next/link': {
    requiredVersion: undefined,
    singleton: true,
    import: undefined,
  },
  'next/router': {
    requiredVersion: false,
    singleton: true,
    import: undefined,
  },
  'next/image': {
    requiredVersion: undefined,
    singleton: true,
    import: undefined,
  },
  'next/script': {
    requiredVersion: undefined,
    singleton: true,
    import: undefined,
  },
  react: {
    singleton: true,
    requiredVersion: false,
    import: false,
  },
  'react/': {
    singleton: true,
    requiredVersion: false,
    import: false,
  },
  'react-dom/': {
    singleton: true,
    requiredVersion: false,
    import: false,
  },
  'react-dom': {
    singleton: true,
    requiredVersion: false,
    import: false,
  },
  'react/jsx-dev-runtime': {
    singleton: true,
    requiredVersion: false,
  },
  'react/jsx-runtime': {
    singleton: true,
    requiredVersion: false,
  },
  'styled-jsx': {
    singleton: true,
    import: undefined,
    version: require('styled-jsx/package.json').version,
    requiredVersion: '^' + require('styled-jsx/package.json').version,
  },
  'styled-jsx/style': {
    singleton: true,
    import: false,
    version: require('styled-jsx/package.json').version,
    requiredVersion: '^' + require('styled-jsx/package.json').version,
  },
  'styled-jsx/css': {
    singleton: true,
    import: undefined,
    version: require('styled-jsx/package.json').version,
    requiredVersion: '^' + require('styled-jsx/package.json').version,
  },
};