Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6ddab4e
chore: checkpoint current work
ScriptedAlchemy Feb 8, 2026
60f03ef
fix(nextjs-mf): stabilize e2e + hydration (#4388)
ScriptedAlchemy Feb 9, 2026
e06f355
Federation hydration stability (#4402)
ScriptedAlchemy Feb 9, 2026
acf8166
Package implementation audit (#4404)
ScriptedAlchemy Feb 10, 2026
56c40ba
Merge branch 'main' into nextjs-mf-v9-e2e-stabilize-20260207
ScriptedAlchemy Feb 12, 2026
1443bdc
chore(nextjs-mf): add contextual changeset coverage
ScriptedAlchemy Feb 12, 2026
171abf3
chore(core): add changeset coverage for pr #4425
ScriptedAlchemy Feb 12, 2026
af5af21
chore(nextjs-mf): remove redundant auto changeset
ScriptedAlchemy Feb 12, 2026
f69a5bf
Merge origin/main into nextjs-mf-v9-e2e-stabilize-20260207
ScriptedAlchemy Feb 13, 2026
5c9a959
style(nextjs-mf): format asset loader test
ScriptedAlchemy Feb 14, 2026
15fa1da
fix(module-federation): stabilize checkout-install and pkg preview
ScriptedAlchemy Feb 14, 2026
adfcfde
fix(nextjs-mf): address route review feedback
ScriptedAlchemy Feb 14, 2026
b182887
fix(nextjs-mf): correct try/catch brace in fixUrlLoader shim
ScriptedAlchemy Feb 14, 2026
4124205
fix: address review regressions in route ordering and hot reload
ScriptedAlchemy Feb 14, 2026
ebcaace
Merge branch 'main' into nextjs-mf-v9-e2e-stabilize-20260207
ScriptedAlchemy Feb 14, 2026
031d009
Merge branch 'main' into nextjs-mf-v9-e2e-stabilize-20260207
ScriptedAlchemy Feb 14, 2026
ba97c10
Merge branch 'main' into nextjs-mf-v9-e2e-stabilize-20260207
ScriptedAlchemy Feb 15, 2026
bf95ecb
Merge branch 'main' into nextjs-mf-v9-e2e-stabilize-20260207
ScriptedAlchemy Feb 16, 2026
7afe225
Merge branch 'main' into nextjs-mf-v9-e2e-stabilize-20260207
ScriptedAlchemy Feb 16, 2026
04ff45b
Merge branch 'main' into nextjs-mf-v9-e2e-stabilize-20260207
ScriptedAlchemy Feb 19, 2026
cbc1c5a
Merge branch 'main' into nextjs-mf-v9-e2e-stabilize-20260207
ScriptedAlchemy Feb 19, 2026
7c125bc
Merge remote-tracking branch 'origin/main' into nextjs-mf-v9-e2e-stab…
ScriptedAlchemy Feb 24, 2026
bd02ef0
fix(dts-plugin): align sdk and error-codes package entrypoints
ScriptedAlchemy Feb 24, 2026
8de92b5
Merge remote-tracking branch 'origin/main' into nextjs-mf-v9-e2e-stab…
ScriptedAlchemy Feb 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/mean-dogs-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@module-federation/runtime-core": patch
---

Fix a race where concurrent `Module.init()` calls could run remote container initialization more than once.

`Module.init()` now deduplicates in-flight initialization with a shared promise so `beforeInitContainer`/`initContainer` logic executes once per module while preserving stable initialized state after completion.

Also adds regression coverage for concurrent initialization behavior.
1 change: 1 addition & 0 deletions .github/workflows/actionlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ jobs:
NODE_PATH: ${{ runner.temp }}/node_modules
with:
matcher: true
working-directory: ${{ runner.temp }}
45 changes: 34 additions & 11 deletions apps/3000-home/components/SharedNav.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,36 @@
import React from 'react';
import { Menu, Layout } from 'antd';
import { useRouter } from 'next/router';
import { useRouter } from 'next/compat/router';
import './menu';

const SharedNav = () => {
const { asPath, push } = useRouter();
let activeMenu;
type SharedNavProps = {
currentPath?: string;
};

function getActiveMenu(path: string | undefined): string | undefined {
if (!path) {
return undefined;
}

if (path === '/' || path.startsWith('/home')) {
return '/';
}

if (path.startsWith('/shop')) {
return '/shop';
}

if (asPath === '/' || asPath.startsWith('/home')) {
activeMenu = '/';
} else if (asPath.startsWith('/shop')) {
activeMenu = '/shop';
} else if (asPath.startsWith('/checkout')) {
activeMenu = '/checkout';
if (path.startsWith('/checkout')) {
return '/checkout';
}

return undefined;
}

const SharedNav = ({ currentPath }: SharedNavProps) => {
const router = useRouter();
const activeMenu = getActiveMenu(currentPath);

const menuItems = [
{
className: 'home-menu-link',
Expand Down Expand Up @@ -53,7 +69,14 @@ const SharedNav = () => {
mode="horizontal"
selectedKeys={activeMenu ? [activeMenu] : undefined}
onClick={({ key }) => {
push(key);
if (router?.push) {
router.push(key);
return;
}

if (typeof window !== 'undefined') {
window.location.assign(key);
}
}}
items={menuItems}
/>
Expand Down
28 changes: 22 additions & 6 deletions apps/3000-home/components/menu.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as React from 'react';
import type { ItemType } from 'antd/es/menu/interface';

import { useRouter } from 'next/router';
import { Menu } from 'antd';
import { useRouter } from 'next/compat/router';

const menuItems: ItemType[] = [
{ label: 'Main home', key: '/' },
Expand All @@ -15,23 +16,38 @@ const menuItems: ItemType[] = [
},
];

export default function AppMenu() {
type AppMenuProps = {
currentPath?: string;
};

export default function AppMenu({ currentPath }: AppMenuProps) {
const router = useRouter();
const resolvedPath = currentPath || '/';

return (
<>
<div>
<div
style={{ padding: '10px', fontWeight: 600, backgroundColor: '#fff' }}
>
Home App Menu
</div>
<Menu
mode="inline"
selectedKeys={[router.asPath]}
selectedKeys={[resolvedPath]}
style={{ height: '100%' }}
onClick={({ key }) => router.push(key)}
onClick={({ key }) => {
const href = String(key);
if (router?.push) {
router.push(href);
return;
}

if (typeof window !== 'undefined') {
window.location.assign(href);
}
}}
items={menuItems}
/>
</>
</div>
);
}
3 changes: 2 additions & 1 deletion apps/3000-home/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import './.next/dev/types/routes.d.ts';

// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
103 changes: 46 additions & 57 deletions apps/3000-home/next.config.js
Original file line number Diff line number Diff line change
@@ -1,77 +1,66 @@
const NextFederationPlugin = require('@module-federation/nextjs-mf');
const path = require('path');
const reactPath = path.dirname(require.resolve('react/package.json'));
const reactDomPath = path.dirname(require.resolve('react-dom/package.json'));
const { withNextFederation } = require('@module-federation/nextjs-mf');

/** @type {import('next').NextConfig} */
const nextConfig = {
const baseConfig = {
typescript: {
ignoreBuildErrors: true,
},
eslint: {
ignoreDuringBuilds: true,
},
webpack(config, options) {
const { isServer } = options;
config.watchOptions = {
ignored: ['**/node_modules/**', '**/@mf-types/**'],
};
// used for testing build output snapshots
const remotes = {
checkout: `checkout@http://localhost:3002/_next/static/${
isServer ? 'ssr' : 'chunks'
}/remoteEntry.js`,
home_app: `home_app@http://localhost:3000/_next/static/${
isServer ? 'ssr' : 'chunks'
}/remoteEntry.js`,
shop: `shop@http://localhost:3001/_next/static/${
isServer ? 'ssr' : 'chunks'
}/remoteEntry.js`,
};

config.plugins.push(
new NextFederationPlugin({
name: 'home_app',
filename: 'static/chunks/remoteEntry.js',
remotes: {
shop: remotes.shop,
checkout: remotes.checkout,
},
exposes: {
'./SharedNav': './components/SharedNav',
'./menu': './components/menu',
},
shared: {
'lodash/': {},
antd: {
requiredVersion: '5.19.1',
version: '5.19.1',
},
'@ant-design/': {
singleton: true,
},
},
extraOptions: {
debug: false,
exposePages: true,
enableImageLoaderFix: true,
enableUrlLoaderFix: true,
},
}),
);
config.plugins.push({
name: 'nx-dev-webpack-plugin',
apply(compiler) {
compiler.options.devtool = false;
compiler.options.resolve.alias = {
...compiler.options.resolve.alias,
react: reactPath,
'react-dom': reactDomPath,
};
},
});

return config;
},
};

module.exports = nextConfig;
module.exports = withNextFederation(baseConfig, {
name: 'home_app',
mode: 'pages',
filename: 'static/chunks/remoteEntry.js',
remotes: ({ isServer }) => ({
shop: `shop@http://localhost:3001/_next/static/${
isServer ? 'ssr' : 'chunks'
}/remoteEntry.js`,
checkout: `checkout@http://localhost:3002/_next/static/${
isServer ? 'ssr' : 'chunks'
}/remoteEntry.js`,
}),
exposes: {
'./SharedNav': './components/SharedNav',
'./menu': './components/menu',
},
shared: {
'lodash/': {},
'@ant-design/cssinjs': {
singleton: true,
requiredVersion: false,
eager: true,
},
antd: {
singleton: true,
requiredVersion: '5.19.1',
version: '5.19.1',
},
'@ant-design/': {
singleton: true,
},
},
pages: {
exposePages: true,
pageMapFormat: 'routes-v2',
},
runtime: {
onRemoteFailure: 'null-fallback',
},
diagnostics: {
level: 'warn',
},
});
5 changes: 3 additions & 2 deletions apps/3000-home/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"@ant-design/cssinjs": "^1.21.0",
"antd": "5.19.1",
"lodash": "4.17.23",
"next": "14.2.35",
"next": "16.1.5",
"react": "18.3.1",
"react-dom": "18.3.1"
},
Expand All @@ -19,6 +19,7 @@
},
"scripts": {
"start": "next start",
"build": "pnpm exec next telemetry disable && NEXT_PRIVATE_LOCAL_WEBPACK=true next build"
"build": "pnpm exec next telemetry disable && NEXT_PRIVATE_LOCAL_WEBPACK=true next build --webpack",
"dev": "NEXT_PRIVATE_LOCAL_WEBPACK=true next dev --webpack -p 3000"
}
}
65 changes: 39 additions & 26 deletions apps/3000-home/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,67 @@
import * as React from 'react';
import { useState } from 'react';
import { init } from '@module-federation/runtime';
console.log('logging init', typeof init);
import App from 'next/app';
import { Layout, version, ConfigProvider } from 'antd';
import { StyleProvider } from '@ant-design/cssinjs';

import Router, { useRouter } from 'next/router';
const SharedNav = React.lazy(() => import('../components/SharedNav'));
import { useRouter } from 'next/compat/router';
import SharedNav from '../components/SharedNav';
import HostAppMenu from '../components/menu';

function MyApp(props) {
const { Component, pageProps } = props;
const { asPath } = useRouter();
const [MenuComponent, setMenuComponent] = useState(() => HostAppMenu);
const handleRouteChange = async (url) => {
const router = useRouter();
const resolvedPath =
router?.asPath ||
router?.pathname ||
(typeof window !== 'undefined'
? `${window.location.pathname}${window.location.search}${window.location.hash}`
: '/');
const [MenuComponent, setMenuComponent] = React.useState(() => HostAppMenu);

const handleRouteChange = React.useCallback(async (url: string) => {
if (url.startsWith('/shop')) {
// @ts-ignore
const RemoteAppMenu = (await import('shop/menu')).default;
setMenuComponent(() => RemoteAppMenu);
} else if (url.startsWith('/checkout')) {
// @ts-ignore
return;
}

if (url.startsWith('/checkout')) {
const RemoteAppMenu = (await import('checkout/menu')).default;
setMenuComponent(() => RemoteAppMenu);
} else {
setMenuComponent(() => HostAppMenu);
return;
}
};
// handle first route hit.

setMenuComponent(() => HostAppMenu);
}, []);

React.useEffect(() => {
handleRouteChange(asPath);
}, [asPath]);
const initialPath =
router?.asPath ||
(typeof window !== 'undefined'
? `${window.location.pathname}${window.location.search}${window.location.hash}`
: '/');
void handleRouteChange(initialPath);
}, [handleRouteChange, router?.asPath]);

//handle route change
React.useEffect(() => {
// Step 3: Subscribe on events
Router.events.on('routeChangeStart', handleRouteChange);
if (!router?.events) {
return;
}

router.events.on('routeChangeStart', handleRouteChange);
return () => {
Router.events.off('routeChangeStart', handleRouteChange);
router.events.off('routeChangeStart', handleRouteChange);
};
}, []);
}, [handleRouteChange, router?.events]);

return (
<StyleProvider layer>
<ConfigProvider theme={{ hashed: false }}>
<Layout style={{ minHeight: '100vh' }} prefixCls={'dd'}>
<React.Suspense>
<SharedNav />
</React.Suspense>
<SharedNav currentPath={resolvedPath} />
<Layout>
<Layout.Sider width={200}>
<MenuComponent />
<MenuComponent currentPath={resolvedPath} />
</Layout.Sider>
<Layout>
<Layout.Content style={{ background: '#fff', padding: 20 }}>
Expand Down
Loading