create-interview-cockpit 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1551,6 +1551,1212 @@ export const ISOLATED_MODULE_FEDERATION_LAB: FrontendLabWorkspace = {
1551
1551
  files: MODULE_FEDERATION_ISOLATED_FILES,
1552
1552
  };
1553
1553
 
1554
+ // ─── Next.js MF Option A — @module-federation/nextjs-mf plugin ──────────────
1555
+ const NEXTJS_MF_PLUGIN_FILES: Record<string, string> = {
1556
+ "README.md": `# Next.js Module Federation — Option A: built-in webpack ModuleFederationPlugin
1557
+
1558
+ ## What is this
1559
+
1560
+ A minimal Next.js shell that loads a remote Next.js app using webpack's built-in
1561
+ \`ModuleFederationPlugin\` — no extra npm packages needed.
1562
+ Both shell and remote are Next.js apps that configure the plugin inside their \`next.config.js\`.
1563
+
1564
+ ## Structure
1565
+
1566
+ - \`apps/shell/\` — Next.js shell app
1567
+ - \`apps/remote/\` — Next.js remote app that exposes a widget
1568
+
1569
+ Ports are assigned automatically by the lab runner (HOST_PORT for shell, REMOTE_PORT for remote).
1570
+
1571
+ ## Key idea
1572
+
1573
+ The shell's \`next.config.js\` registers \`webpack.container.ModuleFederationPlugin\` with no
1574
+ static remotes — the remote URL is resolved at runtime via the \`RemoteSlot\` component.
1575
+ No \`@module-federation/nextjs-mf\` package is required.
1576
+
1577
+ ## Things to try
1578
+
1579
+ 1. Stop the remote and reload the shell — observe the fallback UI.
1580
+ 2. Add a second exposed component in the remote's \`next.config.js\` and load it from the shell.
1581
+ 3. Change the entry URL to point to a CDN-hosted remoteEntry.js.
1582
+ `,
1583
+ "package.json": `{
1584
+ "name": "nextjs-mf-plugin-lab",
1585
+ "private": true,
1586
+ "workspaces": [
1587
+ "apps/shell",
1588
+ "apps/remote"
1589
+ ],
1590
+ "scripts": {
1591
+ "dev": "concurrently -k -n remote,shell -c magenta,cyan 'npm run dev --workspace=nextjs-mf-remote' 'npm run dev --workspace=nextjs-mf-shell'"
1592
+ },
1593
+ "devDependencies": {
1594
+ "concurrently": "^9.2.1"
1595
+ },
1596
+ "overrides": {
1597
+ "undici": "^7"
1598
+ }
1599
+ }
1600
+ `,
1601
+ "apps/shell/package.json": `{
1602
+ "name": "nextjs-mf-shell",
1603
+ "private": true,
1604
+ "scripts": {
1605
+ "dev": "next dev -p $HOST_PORT",
1606
+ "build": "next build",
1607
+ "start": "next start"
1608
+ },
1609
+ "dependencies": {
1610
+ "next": "latest",
1611
+ "react": "^19.0.0",
1612
+ "react-dom": "^19.0.0"
1613
+ }
1614
+ }
1615
+ `,
1616
+ "apps/shell/next.config.js": `/** @type {import('next').NextConfig} */
1617
+ const nextConfig = {
1618
+ reactStrictMode: true,
1619
+ // Use webpack's built-in ModuleFederationPlugin — no extra npm package needed.
1620
+ webpack(config, { webpack }) {
1621
+ config.plugins.push(
1622
+ new webpack.container.ModuleFederationPlugin({
1623
+ name: 'shell',
1624
+ // Shell exposes nothing, just consumes remotes at runtime.
1625
+ remotes: {},
1626
+ shared: {
1627
+ react: { singleton: true, requiredVersion: false },
1628
+ 'react-dom': { singleton: true, requiredVersion: false },
1629
+ },
1630
+ })
1631
+ );
1632
+ return config;
1633
+ },
1634
+ };
1635
+
1636
+ module.exports = nextConfig;
1637
+ `,
1638
+ "apps/shell/src/app/layout.tsx": `import type { Metadata } from 'next';
1639
+ import React from 'react';
1640
+
1641
+ export const metadata: Metadata = { title: 'Shell — Next.js MF' };
1642
+
1643
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
1644
+ return (
1645
+ <html lang="en">
1646
+ <body>{children}</body>
1647
+ </html>
1648
+ );
1649
+ }
1650
+ `,
1651
+ "apps/shell/src/app/page.tsx": `'use client';
1652
+
1653
+ import React from 'react';
1654
+ import { RemoteSlot } from '@/components/RemoteSlot';
1655
+
1656
+ export default function Home() {
1657
+ return (
1658
+ <main style={{ padding: '2rem', fontFamily: 'system-ui, sans-serif' }}>
1659
+ <h1>Next.js Shell — Option A</h1>
1660
+ <p style={{ color: '#64748b' }}>
1661
+ The widget below is loaded from the Next.js remote at runtime.
1662
+ Shell and remote both use Next.js with webpack's built-in ModuleFederationPlugin.
1663
+ </p>
1664
+
1665
+ {/* NEXT_PUBLIC_REMOTE_URL is injected by the lab runner */}
1666
+ <RemoteSlot
1667
+ scope="myRemote"
1668
+ module="./Widget"
1669
+ remoteUrl={
1670
+ process.env.NEXT_PUBLIC_REMOTE_URL ||
1671
+ 'http://localhost:3001/_next/static/chunks/remoteEntry.js'
1672
+ }
1673
+ fallback={<p style={{ color: '#ef4444' }}>Remote unavailable.</p>}
1674
+ />
1675
+ </main>
1676
+ );
1677
+ }
1678
+ `,
1679
+ "apps/shell/src/components/RemoteSlot.tsx": `'use client';
1680
+
1681
+ import React from 'react';
1682
+
1683
+ type Props = {
1684
+ scope: string;
1685
+ module: string;
1686
+ remoteUrl: string;
1687
+ fallback?: React.ReactNode;
1688
+ };
1689
+
1690
+ // Webpack federation globals — available because shell's next.config.js
1691
+ // registers ModuleFederationPlugin which injects them at runtime.
1692
+ declare const __webpack_init_sharing__: (scope: 'default') => Promise<void>;
1693
+ declare const __webpack_share_scopes__: { default: unknown };
1694
+
1695
+ async function loadRemoteModule(scope: string, module: string, remoteUrl: string) {
1696
+ // 1. Inject the remote's remoteEntry.js once.
1697
+ if (!(window as Record<string, unknown>)[scope]) {
1698
+ await new Promise<void>((resolve, reject) => {
1699
+ const script = document.createElement('script');
1700
+ script.src = remoteUrl;
1701
+ script.async = true;
1702
+ script.onload = () => resolve();
1703
+ script.onerror = () => reject(new Error(\`Failed to load remote: \${scope}\`));
1704
+ document.head.appendChild(script);
1705
+ });
1706
+ }
1707
+
1708
+ // 2. Init the shared scope so singleton packages (react) are negotiated.
1709
+ await __webpack_init_sharing__('default');
1710
+
1711
+ const container = (window as Record<string, unknown>)[scope] as {
1712
+ init(s: unknown): Promise<void>;
1713
+ get(m: string): Promise<() => unknown>;
1714
+ };
1715
+
1716
+ await container.init(__webpack_share_scopes__.default);
1717
+
1718
+ // 3. Get the exposed module factory and invoke it.
1719
+ const factory = await container.get(module);
1720
+ return factory() as { default: React.ComponentType };
1721
+ }
1722
+
1723
+ export function RemoteSlot({ scope, module, remoteUrl, fallback }: Props) {
1724
+ const LazyComponent = React.useMemo(
1725
+ () =>
1726
+ React.lazy(() =>
1727
+ loadRemoteModule(scope, module, remoteUrl).then((mod) => ({
1728
+ default: mod.default,
1729
+ }))
1730
+ ),
1731
+ [scope, module, remoteUrl]
1732
+ );
1733
+
1734
+ return (
1735
+ <React.Suspense fallback={<p>Loading remote...</p>}>
1736
+ <LazyComponent />
1737
+ </React.Suspense>
1738
+ );
1739
+ }
1740
+ `,
1741
+ "apps/remote/package.json": `{
1742
+ "name": "nextjs-mf-remote",
1743
+ "private": true,
1744
+ "scripts": {
1745
+ "dev": "next dev -p $REMOTE_PORT",
1746
+ "build": "next build",
1747
+ "start": "next start"
1748
+ },
1749
+ "dependencies": {
1750
+ "next": "latest",
1751
+ "react": "^19.0.0",
1752
+ "react-dom": "^19.0.0"
1753
+ }
1754
+ }
1755
+ `,
1756
+ "apps/remote/next.config.js": `/** @type {import('next').NextConfig} */
1757
+ const nextConfig = {
1758
+ reactStrictMode: true,
1759
+ webpack(config, { webpack }) {
1760
+ config.plugins.push(
1761
+ new webpack.container.ModuleFederationPlugin({
1762
+ name: 'myRemote',
1763
+ filename: 'static/chunks/remoteEntry.js',
1764
+ exposes: {
1765
+ // The shell loads this module at runtime via RemoteSlot.
1766
+ './Widget': './src/exposes/Widget',
1767
+ },
1768
+ shared: {
1769
+ react: { singleton: true, requiredVersion: false },
1770
+ 'react-dom': { singleton: true, requiredVersion: false },
1771
+ },
1772
+ })
1773
+ );
1774
+ return config;
1775
+ },
1776
+ };
1777
+
1778
+ module.exports = nextConfig;
1779
+ `,
1780
+ "apps/remote/src/app/layout.tsx": `import type { Metadata } from 'next';
1781
+ import React from 'react';
1782
+
1783
+ export const metadata: Metadata = { title: 'Remote — Next.js MF' };
1784
+
1785
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
1786
+ return (
1787
+ <html lang="en">
1788
+ <body>{children}</body>
1789
+ </html>
1790
+ );
1791
+ }
1792
+ `,
1793
+ "apps/remote/src/app/page.tsx": `export default function Home() {
1794
+ return (
1795
+ <main style={{ padding: '2rem', fontFamily: 'system-ui, sans-serif' }}>
1796
+ <h1>Remote App</h1>
1797
+ <p>
1798
+ This app exposes <code>myRemote/Widget</code> via Module Federation.
1799
+ The shell loads it at runtime — open the shell URL to see it in action.
1800
+ </p>
1801
+ </main>
1802
+ );
1803
+ }
1804
+ `,
1805
+ "apps/remote/src/exposes/Widget.tsx": `'use client';
1806
+
1807
+ // This component is exposed at ./Widget via ModuleFederationPlugin.
1808
+ // The shell dynamically loads it at runtime via RemoteSlot — zero build-time coupling.
1809
+ export default function Widget() {
1810
+ return (
1811
+ <section
1812
+ style={{
1813
+ padding: '1rem',
1814
+ border: '1px solid #e2e8f0',
1815
+ borderRadius: '8px',
1816
+ background: '#f8fafc',
1817
+ maxWidth: '400px',
1818
+ }}
1819
+ >
1820
+ <h2 style={{ margin: '0 0 0.5rem', fontSize: '1rem', color: '#0f172a' }}>
1821
+ Remote Widget
1822
+ </h2>
1823
+ <p style={{ margin: 0, color: '#475569', fontSize: '0.875rem' }}>
1824
+ Loaded from{' '}
1825
+ <code style={{ background: '#e2e8f0', padding: '0 4px', borderRadius: '4px' }}>
1826
+ myRemote/Widget
1827
+ </code>{' '}
1828
+ at runtime. Both shell and remote are Next.js apps.
1829
+ </p>
1830
+ </section>
1831
+ );
1832
+ }
1833
+ `,
1834
+ };
1835
+
1836
+ export const NEXTJS_MF_PLUGIN_LAB: FrontendLabWorkspace = {
1837
+ version: 1,
1838
+ label: "Next.js MF — Plugin (Option A)",
1839
+ type: "module-federation",
1840
+ activeFile: "apps/shell/src/components/RemoteSlot.tsx",
1841
+ files: NEXTJS_MF_PLUGIN_FILES,
1842
+ };
1843
+
1844
+ // ─── Next.js MF Option B — plain runtime script loading (no plugin) ──────────
1845
+ const NEXTJS_MF_RUNTIME_FILES: Record<string, string> = {
1846
+ "README.md": `# Next.js Module Federation — Option B: plain runtime script loading
1847
+
1848
+ ## What is this
1849
+
1850
+ The shell is a plain Next.js app with NO federation webpack plugin.
1851
+ The remote is a standard webpack 5 app that produces a \`remoteEntry.js\` federation container.
1852
+ The shell loads that container at runtime using plain DOM script injection + webpack federation globals.
1853
+
1854
+ ## Why this approach
1855
+
1856
+ - No Next.js-specific federation plugin required on the shell
1857
+ - Full runtime control — manifest URL can come from an API at page-load time
1858
+ - Easier to reason about for client-side-only composition
1859
+
1860
+ ## Structure
1861
+
1862
+ - \`apps/shell/\` — plain Next.js app
1863
+ - \`apps/remote/\` — webpack 5 React app that exposes a widget
1864
+
1865
+ Ports are assigned automatically by the lab runner (HOST_PORT for shell, REMOTE_PORT for remote).
1866
+
1867
+ ## Things to try
1868
+
1869
+ 1. Change the entry URL to point to a CDN-hosted remoteEntry.js.
1870
+ 2. Add a second exposed module in apps/remote/webpack.config.js and load it in the shell.
1871
+ 3. Move the remote URL to an API route (\`/api/remote-config\`) to simulate a manifest service.
1872
+ `,
1873
+ "package.json": `{
1874
+ "name": "nextjs-runtime-mf-lab",
1875
+ "private": true,
1876
+ "workspaces": [
1877
+ "apps/shell",
1878
+ "apps/remote"
1879
+ ],
1880
+ "scripts": {
1881
+ "dev": "concurrently -k -n remote,shell -c magenta,cyan 'npm run dev --workspace=nextjs-runtime-remote' 'npm run dev --workspace=nextjs-runtime-shell'"
1882
+ },
1883
+ "devDependencies": {
1884
+ "concurrently": "^9.2.1"
1885
+ },
1886
+ "overrides": {
1887
+ "undici": "^7"
1888
+ }
1889
+ }
1890
+ `,
1891
+ "apps/shell/package.json": `{
1892
+ "name": "nextjs-runtime-shell",
1893
+ "private": true,
1894
+ "scripts": {
1895
+ "dev": "next dev -p $HOST_PORT",
1896
+ "build": "next build",
1897
+ "start": "next start"
1898
+ },
1899
+ "dependencies": {
1900
+ "next": "latest",
1901
+ "react": "^19.0.0",
1902
+ "react-dom": "^19.0.0"
1903
+ }
1904
+ }
1905
+ `,
1906
+ "apps/shell/next.config.js": `/** @type {import('next').NextConfig} */
1907
+ const nextConfig = {
1908
+ reactStrictMode: true,
1909
+ // No federation plugin — the shell loads remotes entirely at runtime.
1910
+ };
1911
+
1912
+ module.exports = nextConfig;
1913
+ `,
1914
+ "apps/shell/src/lib/loadRemoteModule.ts": `// Utility: load a webpack 5 Module Federation container at runtime.
1915
+ // Works in any browser environment — no webpack plugin needed on the shell side.
1916
+
1917
+ declare const __webpack_init_sharing__: (scope: 'default') => Promise<void>;
1918
+ declare const __webpack_share_scopes__: { default: unknown };
1919
+
1920
+ type FederatedContainer = {
1921
+ init(shareScope: unknown): Promise<void>;
1922
+ get(module: string): Promise<() => unknown>;
1923
+ };
1924
+
1925
+ // Avoid injecting the same script twice.
1926
+ const loaded = new Set<string>();
1927
+
1928
+ async function injectScript(url: string, scope: string): Promise<void> {
1929
+ if (loaded.has(scope)) return;
1930
+ loaded.add(scope);
1931
+
1932
+ return new Promise((resolve, reject) => {
1933
+ const script = document.createElement('script');
1934
+ script.src = url;
1935
+ script.async = true;
1936
+ script.crossOrigin = 'anonymous';
1937
+ script.onload = () => resolve();
1938
+ script.onerror = () => {
1939
+ loaded.delete(scope); // allow retry
1940
+ reject(new Error(\`Failed to load remote entry: \${url}\`));
1941
+ };
1942
+ document.head.appendChild(script);
1943
+ });
1944
+ }
1945
+
1946
+ export async function loadRemoteModule<T = { default: unknown }>(
1947
+ scope: string,
1948
+ module: string,
1949
+ remoteUrl: string
1950
+ ): Promise<T> {
1951
+ await injectScript(remoteUrl, scope);
1952
+
1953
+ // Init the shared scope so singleton packages are negotiated.
1954
+ await __webpack_init_sharing__('default');
1955
+
1956
+ const container = (window as Record<string, unknown>)[scope] as
1957
+ | FederatedContainer
1958
+ | undefined;
1959
+
1960
+ if (!container) {
1961
+ throw new Error(\`Remote container "\${scope}" not found on window after script load.\`);
1962
+ }
1963
+
1964
+ await container.init(__webpack_share_scopes__.default);
1965
+
1966
+ const factory = await container.get(module);
1967
+ return factory() as T;
1968
+ }
1969
+ `,
1970
+ "apps/shell/src/app/page.tsx": `'use client';
1971
+
1972
+ import React from 'react';
1973
+ import { loadRemoteModule } from '@/lib/loadRemoteModule';
1974
+
1975
+ // The remote URL would normally come from an API call / manifest service.
1976
+ const REMOTE_URL =
1977
+ process.env.NEXT_PUBLIC_REMOTE_URL ||
1978
+ 'http://localhost:3001/remoteEntry.js';
1979
+
1980
+ // React.lazy wraps our runtime loader — Suspense handles the loading state.
1981
+ const RemoteWidget = React.lazy(() =>
1982
+ loadRemoteModule<{ default: React.ComponentType }>(
1983
+ 'myRemote',
1984
+ './Widget',
1985
+ REMOTE_URL
1986
+ ).then((mod) => ({ default: mod.default }))
1987
+ );
1988
+
1989
+ export default function Home() {
1990
+ return (
1991
+ <main style={{ padding: '2rem', fontFamily: 'system-ui, sans-serif' }}>
1992
+ <h1>Next.js Shell (runtime loader)</h1>
1993
+ <p style={{ color: '#64748b' }}>
1994
+ No federation plugin on the shell — the widget is loaded via plain
1995
+ script injection and webpack federation globals.
1996
+ </p>
1997
+
1998
+ <React.Suspense fallback={<p>Loading remote widget...</p>}>
1999
+ <RemoteWidget />
2000
+ </React.Suspense>
2001
+ </main>
2002
+ );
2003
+ }
2004
+ `,
2005
+ "apps/remote/package.json": `{
2006
+ "name": "nextjs-runtime-remote",
2007
+ "private": true,
2008
+ "scripts": {
2009
+ "dev": "webpack serve --config webpack.config.js",
2010
+ "build": "webpack --config webpack.config.js"
2011
+ },
2012
+ "dependencies": {
2013
+ "react": "18.2.0",
2014
+ "react-dom": "18.2.0"
2015
+ },
2016
+ "devDependencies": {
2017
+ "esbuild": "^0.28.0",
2018
+ "esbuild-loader": "^4.4.3",
2019
+ "html-webpack-plugin": "^5.6.7",
2020
+ "webpack": "^5.106.2",
2021
+ "webpack-cli": "^7.0.2",
2022
+ "webpack-dev-server": "^5.2.3"
2023
+ }
2024
+ }
2025
+ `,
2026
+ "apps/remote/webpack.config.js": `const path = require('path');
2027
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
2028
+ const { ModuleFederationPlugin } = require('webpack').container;
2029
+
2030
+ const REMOTE_PORT = Number(process.env.REMOTE_PORT || 3001);
2031
+
2032
+ module.exports = {
2033
+ mode: 'development',
2034
+ entry: './src/index.js',
2035
+ output: {
2036
+ path: path.resolve(__dirname, 'dist'),
2037
+ publicPath: \`http://localhost:\${REMOTE_PORT}/\`,
2038
+ clean: true,
2039
+ },
2040
+ resolve: { extensions: ['.js', '.jsx', '.ts', '.tsx'] },
2041
+ module: {
2042
+ rules: [
2043
+ {
2044
+ test: /\\.(js|jsx|ts|tsx)$/,
2045
+ exclude: /node_modules/,
2046
+ use: {
2047
+ loader: 'esbuild-loader',
2048
+ options: { loader: 'jsx', jsx: 'automatic', target: 'es2020' },
2049
+ },
2050
+ },
2051
+ ],
2052
+ },
2053
+ plugins: [
2054
+ new ModuleFederationPlugin({
2055
+ name: 'myRemote',
2056
+ filename: 'remoteEntry.js',
2057
+ exposes: {
2058
+ './Widget': './src/Widget.jsx',
2059
+ },
2060
+ shared: {
2061
+ react: { singleton: true, requiredVersion: false },
2062
+ 'react-dom': { singleton: true, requiredVersion: false },
2063
+ },
2064
+ }),
2065
+ new HtmlWebpackPlugin({ template: './public/index.html' }),
2066
+ ],
2067
+ devServer: {
2068
+ port: REMOTE_PORT,
2069
+ headers: { 'Access-Control-Allow-Origin': '*' },
2070
+ },
2071
+ };
2072
+ `,
2073
+ "apps/remote/public/index.html": `<!doctype html>
2074
+ <html lang="en">
2075
+ <head><meta charset="UTF-8" /><title>Remote (standalone)</title></head>
2076
+ <body><div id="root"></div></body>
2077
+ </html>
2078
+ `,
2079
+ "apps/remote/src/index.js": `// Async boundary required for Module Federation
2080
+ import('./bootstrap');
2081
+ `,
2082
+ "apps/remote/src/bootstrap.jsx": `import React from 'react';
2083
+ import { createRoot } from 'react-dom/client';
2084
+ import Widget from './Widget';
2085
+
2086
+ createRoot(document.getElementById('root')).render(<Widget />);
2087
+ `,
2088
+ "apps/remote/src/Widget.jsx": `import React from 'react';
2089
+
2090
+ // This component is exposed at ./Widget via ModuleFederationPlugin.
2091
+ // The shell loads it with loadRemoteModule('myRemote', './Widget', url).
2092
+ export default function Widget() {
2093
+ return (
2094
+ <section
2095
+ style={{
2096
+ padding: '1rem',
2097
+ border: '1px solid #e2e8f0',
2098
+ borderRadius: '8px',
2099
+ background: '#f8fafc',
2100
+ maxWidth: '400px',
2101
+ }}
2102
+ >
2103
+ <h2 style={{ margin: '0 0 0.5rem', fontSize: '1rem', color: '#0f172a' }}>
2104
+ Remote Widget
2105
+ </h2>
2106
+ <p style={{ margin: 0, color: '#475569', fontSize: '0.875rem' }}>
2107
+ Built by webpack 5. Loaded by the Next.js shell at runtime via plain
2108
+ script injection — no federation plugin on the shell required.
2109
+ </p>
2110
+ </section>
2111
+ );
2112
+ }
2113
+ `,
2114
+ };
2115
+
2116
+ export const NEXTJS_MF_RUNTIME_LAB: FrontendLabWorkspace = {
2117
+ version: 1,
2118
+ label: "Next.js MF — Runtime Loader (Option B)",
2119
+ type: "module-federation",
2120
+ activeFile: "apps/shell/src/lib/loadRemoteModule.ts",
2121
+ files: NEXTJS_MF_RUNTIME_FILES,
2122
+ };
2123
+
2124
+ // ─── Next.js Multi-Zones ─────────────────────────────────────────────────────
2125
+ const NEXTJS_MULTI_ZONES_FILES: Record<string, string> = {
2126
+ "README.md": `# Next.js Multi-Zones
2127
+
2128
+ ## What this shows
2129
+ Two separate Next.js apps served under one domain via URL-path splitting.
2130
+ The shell owns \`/\` and proxies \`/store\` to Zone B using Next.js rewrites.
2131
+ Each zone deploys independently — the shell never rebuilds when Zone B changes.
2132
+
2133
+ ## Key files
2134
+ - \`apps/shell/next.config.js\` — rewrites \`/store/*\` → Zone B
2135
+ - \`apps/zone-b/next.config.js\` — \`basePath: '/store'\` aligns Zone B's routes with the rewrite
2136
+
2137
+ ## Things to try
2138
+ 1. Click the Store link — URL stays on the shell's origin but Zone B renders.
2139
+ 2. Deploy Zone B to a CDN; update only the rewrite destination.
2140
+ 3. Add a \`/checkout\` zone as a third independent Next.js app.
2141
+ `,
2142
+ "package.json": `{
2143
+ "name": "nextjs-multi-zones-lab",
2144
+ "private": true,
2145
+ "workspaces": ["apps/shell", "apps/zone-b"],
2146
+ "scripts": {
2147
+ "dev": "concurrently -k -n zone-b,shell -c magenta,cyan 'npm run dev --workspace=nextjs-zone-b' 'npm run dev --workspace=nextjs-shell'"
2148
+ },
2149
+ "devDependencies": { "concurrently": "^9.2.1" },
2150
+ "overrides": { "undici": "^7" }
2151
+ }
2152
+ `,
2153
+ "apps/shell/package.json": `{
2154
+ "name": "nextjs-shell",
2155
+ "private": true,
2156
+ "scripts": { "dev": "next dev -p $HOST_PORT" },
2157
+ "dependencies": {
2158
+ "next": "latest",
2159
+ "react": "^19.0.0",
2160
+ "react-dom": "^19.0.0"
2161
+ },
2162
+ "devDependencies": {
2163
+ "typescript": "latest",
2164
+ "@types/react": "latest",
2165
+ "@types/react-dom": "latest",
2166
+ "@types/node": "latest"
2167
+ }
2168
+ }
2169
+ `,
2170
+ "apps/shell/next.config.js": `/** @type {import('next').NextConfig} */
2171
+ // Shell acts as the routing layer.
2172
+ // /store and /store/* are proxied to Zone B running at REMOTE_PORT.
2173
+ const nextConfig = {
2174
+ async rewrites() {
2175
+ const zoneB = 'http://localhost:' + process.env.REMOTE_PORT;
2176
+ return [
2177
+ { source: '/store', destination: zoneB + '/store' },
2178
+ { source: '/store/:path*', destination: zoneB + '/store/:path*' },
2179
+ ];
2180
+ },
2181
+ };
2182
+ module.exports = nextConfig;
2183
+ `,
2184
+ "apps/shell/src/app/layout.tsx": `import type { Metadata } from 'next';
2185
+ import React from 'react';
2186
+
2187
+ export const metadata: Metadata = { title: 'Shell \u2014 Multi-Zones' };
2188
+
2189
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
2190
+ return (
2191
+ <html lang="en">
2192
+ <body style={{ margin: 0, fontFamily: 'system-ui, sans-serif' }}>{children}</body>
2193
+ </html>
2194
+ );
2195
+ }
2196
+ `,
2197
+ "apps/shell/src/app/page.tsx": `// Cross-zone links must be plain <a> tags — Next.js <Link> routes within
2198
+ // the same app via client-side JS, so the server-side rewrite never fires.
2199
+ export default function Home() {
2200
+ return (
2201
+ <main style={{ padding: '2rem' }}>
2202
+ <h1>Shell App \u2014 Zone A</h1>
2203
+ <p style={{ color: '#64748b' }}>
2204
+ This zone owns <code>/</code>. Clicking Store proxies the request to a
2205
+ separate Next.js app (Zone B) \u2014 transparent to the browser.
2206
+ </p>
2207
+ <nav style={{ display: 'flex', gap: '1rem', marginTop: '1.5rem' }}>
2208
+ <a href="/" style={{ color: '#3b82f6' }}>Home (Zone A)</a>
2209
+ <a href="/store" style={{ color: '#3b82f6' }}>Store (Zone B \u2014 proxied)</a>
2210
+ </nav>
2211
+ </main>
2212
+ );
2213
+ }
2214
+ `,
2215
+ "apps/zone-b/package.json": `{
2216
+ "name": "nextjs-zone-b",
2217
+ "private": true,
2218
+ "scripts": { "dev": "next dev -p $REMOTE_PORT" },
2219
+ "dependencies": {
2220
+ "next": "latest",
2221
+ "react": "^19.0.0",
2222
+ "react-dom": "^19.0.0"
2223
+ },
2224
+ "devDependencies": {
2225
+ "typescript": "latest",
2226
+ "@types/react": "latest",
2227
+ "@types/react-dom": "latest",
2228
+ "@types/node": "latest"
2229
+ }
2230
+ }
2231
+ `,
2232
+ "apps/zone-b/next.config.js": `/** @type {import('next').NextConfig} */
2233
+ // basePath makes Zone B serve its pages under /store,
2234
+ // matching the path prefix the shell forwards here.
2235
+ const nextConfig = {
2236
+ basePath: '/store',
2237
+ };
2238
+ module.exports = nextConfig;
2239
+ `,
2240
+ "apps/zone-b/src/app/layout.tsx": `import type { Metadata } from 'next';
2241
+ import React from 'react';
2242
+
2243
+ export const metadata: Metadata = { title: 'Store \u2014 Zone B' };
2244
+
2245
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
2246
+ return (
2247
+ <html lang="en">
2248
+ <body style={{ margin: 0, fontFamily: 'system-ui, sans-serif' }}>{children}</body>
2249
+ </html>
2250
+ );
2251
+ }
2252
+ `,
2253
+ "apps/zone-b/src/app/page.tsx": `// Cross-zone back-link must be a plain <a> — client-side Next.js routing
2254
+ // stays within this app and would 404. A full navigation triggers the shell's rewrite.
2255
+ export default function StorePage() {
2256
+ return (
2257
+ <main style={{ padding: '2rem', background: '#f0fdf4', minHeight: '100vh' }}>
2258
+ <h1>Store \u2014 Zone B</h1>
2259
+ <p style={{ color: '#64748b' }}>
2260
+ Rendered by a <strong>separate</strong> Next.js app. The shell proxied
2261
+ your request via <code>next.config.js</code> rewrites. No code is shared
2262
+ \u2014 both zones are fully independent.
2263
+ </p>
2264
+ <a href="/" style={{ color: '#3b82f6' }}>\u2190 Back to Shell (Zone A)</a>
2265
+ </main>
2266
+ );
2267
+ }
2268
+ `,
2269
+ };
2270
+
2271
+ export const NEXTJS_MULTI_ZONES_LAB: FrontendLabWorkspace = {
2272
+ version: 1,
2273
+ label: "Next.js \u2014 Multi-Zones",
2274
+ type: "module-federation",
2275
+ activeFile: "apps/shell/next.config.js",
2276
+ files: NEXTJS_MULTI_ZONES_FILES,
2277
+ };
2278
+
2279
+ // ─── Next.js MF Runtime API ──────────────────────────────────────────────────
2280
+ const NEXTJS_MF_RUNTIME_API_FILES: Record<string, string> = {
2281
+ "README.md": `# Next.js \u2014 Module Federation Runtime API
2282
+
2283
+ ## What this shows
2284
+ Consuming a federated remote from Next.js App Router without touching \`next.config.js\`.
2285
+ All federation work is isolated inside a \`'use client'\` component using
2286
+ \`@module-federation/enhanced/runtime\`'s \`init()\` + \`loadRemote()\` API.
2287
+
2288
+ ## Key files
2289
+ - \`apps/shell/src/components/FederatedWidget.tsx\` \u2014 the only federation code in the shell
2290
+ - \`apps/mf-remote/webpack.config.js\` \u2014 webpack remote exposing \`./Widget\`
2291
+
2292
+ ## Trade-off
2293
+ The component is strictly client-side (SPA). There is no SSR for the remote content.
2294
+ This is the recommended safe pattern for consuming federation in the App Router.
2295
+ `,
2296
+ "package.json": `{
2297
+ "name": "nextjs-mf-runtime-api-lab",
2298
+ "private": true,
2299
+ "workspaces": ["apps/shell", "apps/mf-remote"],
2300
+ "scripts": {
2301
+ "dev": "concurrently -k -n remote,shell -c magenta,cyan 'npm run dev --workspace=mf-runtime-remote' 'npm run dev --workspace=mf-runtime-shell'"
2302
+ },
2303
+ "devDependencies": { "concurrently": "^9.2.1" },
2304
+ "overrides": { "undici": "^7" }
2305
+ }
2306
+ `,
2307
+ "apps/shell/package.json": `{
2308
+ "name": "mf-runtime-shell",
2309
+ "private": true,
2310
+ "scripts": { "dev": "next dev -p $HOST_PORT" },
2311
+ "dependencies": {
2312
+ "next": "latest",
2313
+ "react": "^19.0.0",
2314
+ "react-dom": "^19.0.0",
2315
+ "@module-federation/enhanced": "latest"
2316
+ },
2317
+ "devDependencies": {
2318
+ "typescript": "latest",
2319
+ "@types/react": "latest",
2320
+ "@types/react-dom": "latest",
2321
+ "@types/node": "latest"
2322
+ }
2323
+ }
2324
+ `,
2325
+ "apps/shell/next.config.js": `/** @type {import('next').NextConfig} */
2326
+ // No webpack hooks needed — federation is handled entirely at runtime
2327
+ // inside FederatedWidget.tsx via @module-federation/enhanced/runtime.
2328
+ module.exports = {};
2329
+ `,
2330
+ "apps/shell/tsconfig.json": `{
2331
+ "compilerOptions": {
2332
+ "target": "ES2017",
2333
+ "lib": ["dom", "dom.iterable", "esnext"],
2334
+ "allowJs": true,
2335
+ "skipLibCheck": true,
2336
+ "strict": true,
2337
+ "noEmit": true,
2338
+ "esModuleInterop": true,
2339
+ "module": "esnext",
2340
+ "moduleResolution": "bundler",
2341
+ "resolveJsonModule": true,
2342
+ "isolatedModules": true,
2343
+ "jsx": "preserve",
2344
+ "incremental": true,
2345
+ "plugins": [{ "name": "next" }],
2346
+ "paths": {
2347
+ "@/*": ["./src/*"]
2348
+ }
2349
+ },
2350
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
2351
+ "exclude": ["node_modules"]
2352
+ }
2353
+ `,
2354
+ "apps/shell/src/app/layout.tsx": `import type { Metadata } from 'next';
2355
+ import React from 'react';
2356
+
2357
+ export const metadata: Metadata = { title: 'Shell \u2014 MF Runtime API' };
2358
+
2359
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
2360
+ return (
2361
+ <html lang="en">
2362
+ <body style={{ margin: 0, fontFamily: 'system-ui, sans-serif' }}>{children}</body>
2363
+ </html>
2364
+ );
2365
+ }
2366
+ `,
2367
+ "apps/shell/src/app/page.tsx": `import FederatedWidget from '@/components/FederatedWidget';
2368
+
2369
+ export default function Home() {
2370
+ return (
2371
+ <main style={{ padding: '2rem' }}>
2372
+ <h1>Next.js Shell \u2014 MF Runtime API</h1>
2373
+ <p style={{ color: '#64748b' }}>
2374
+ The widget below is loaded at runtime using{' '}
2375
+ <code>@module-federation/enhanced/runtime</code>. No webpack config changes needed.
2376
+ </p>
2377
+ <FederatedWidget />
2378
+ </main>
2379
+ );
2380
+ }
2381
+ `,
2382
+ "apps/shell/src/components/FederatedWidget.tsx": `'use client';
2383
+
2384
+ import { useEffect, useState } from 'react';
2385
+ import type { ComponentType } from 'react';
2386
+
2387
+ // All federation work is isolated here in a client component.
2388
+ // init() + loadRemote() run only in the browser, never on the server.
2389
+ export default function FederatedWidget() {
2390
+ const [Component, setComponent] = useState<ComponentType | null>(null);
2391
+ const [error, setError] = useState<string | null>(null);
2392
+
2393
+ useEffect(() => {
2394
+ async function load() {
2395
+ const { init, loadRemote } = await import('@module-federation/enhanced/runtime');
2396
+
2397
+ // NEXT_PUBLIC_REMOTE_URL is injected by the lab runner.
2398
+ const entry =
2399
+ process.env.NEXT_PUBLIC_REMOTE_URL || 'http://localhost:3001/remoteEntry.js';
2400
+
2401
+ // shared tells the runtime which packages to negotiate as singletons.
2402
+ // Without this, both shell and remote load their own React copy → version mismatch crash.
2403
+ init({
2404
+ name: 'shell',
2405
+ remotes: [{ name: 'mfRemote', entry }],
2406
+ shared: {
2407
+ react: { singleton: true, version: '19.0.0', lib: () => require('react') },
2408
+ 'react-dom': { singleton: true, version: '19.0.0', lib: () => require('react-dom') },
2409
+ },
2410
+ });
2411
+
2412
+ // loadRemote resolves ./Widget from the remote container at runtime.
2413
+ const mod = await loadRemote<{ default: ComponentType }>('mfRemote/Widget');
2414
+ if (mod) setComponent(() => mod.default);
2415
+ }
2416
+ load().catch((e) => setError(String(e)));
2417
+ }, []);
2418
+
2419
+ if (error) return <p style={{ color: '#ef4444' }}>Remote failed: {error}</p>;
2420
+ if (!Component) return <p>Loading remote widget\u2026</p>;
2421
+ return <Component />;
2422
+ }
2423
+ `,
2424
+ "apps/mf-remote/package.json": `{
2425
+ "name": "mf-runtime-remote",
2426
+ "private": true,
2427
+ "scripts": { "dev": "webpack serve" },
2428
+ "dependencies": {
2429
+ "react": "^19.0.0",
2430
+ "react-dom": "^19.0.0"
2431
+ },
2432
+ "devDependencies": {
2433
+ "webpack": "^5",
2434
+ "webpack-cli": "^5",
2435
+ "webpack-dev-server": "^5",
2436
+ "html-webpack-plugin": "^5",
2437
+ "@babel/core": "^7",
2438
+ "@babel/preset-env": "^7",
2439
+ "@babel/preset-react": "^7",
2440
+ "babel-loader": "^9"
2441
+ }
2442
+ }
2443
+ `,
2444
+ "apps/mf-remote/webpack.config.js": `const path = require('path');
2445
+ const webpack = require('webpack');
2446
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
2447
+
2448
+ module.exports = {
2449
+ entry: './src/index.js',
2450
+ mode: 'development',
2451
+ output: {
2452
+ path: path.resolve(__dirname, 'dist'),
2453
+ publicPath: 'auto',
2454
+ },
2455
+ devServer: {
2456
+ port: parseInt(process.env.REMOTE_PORT, 10) || 3001,
2457
+ hot: true,
2458
+ headers: { 'Access-Control-Allow-Origin': '*' },
2459
+ },
2460
+ module: {
2461
+ rules: [
2462
+ {
2463
+ test: /\\.(js|jsx)$/,
2464
+ exclude: /node_modules/,
2465
+ use: {
2466
+ loader: 'babel-loader',
2467
+ options: { presets: ['@babel/preset-env', '@babel/preset-react'] },
2468
+ },
2469
+ },
2470
+ ],
2471
+ },
2472
+ resolve: { extensions: ['.js', '.jsx'] },
2473
+ plugins: [
2474
+ new webpack.container.ModuleFederationPlugin({
2475
+ name: 'mfRemote',
2476
+ filename: 'remoteEntry.js',
2477
+ exposes: { './Widget': './src/exposes/Widget' },
2478
+ shared: {
2479
+ react: { singleton: true, eager: true, requiredVersion: '^19.0.0' },
2480
+ 'react-dom': { singleton: true, eager: true, requiredVersion: '^19.0.0' },
2481
+ },
2482
+ }),
2483
+ new HtmlWebpackPlugin({ template: './public/index.html' }),
2484
+ ],
2485
+ };
2486
+ `,
2487
+ "apps/mf-remote/src/index.js": `// Remote bootstrap page. MF consumers load ./Widget via remoteEntry.js.
2488
+ document.getElementById('root').innerHTML =
2489
+ '<div style="padding:2rem;font-family:system-ui">' +
2490
+ '<h2>mfRemote \u2014 running</h2>' +
2491
+ '<p>Exposes <code>mfRemote/Widget</code> via Module Federation.</p>' +
2492
+ '</div>';
2493
+ `,
2494
+ "apps/mf-remote/src/exposes/Widget.jsx": `import React from 'react';
2495
+
2496
+ // Exposed as mfRemote/Widget via ModuleFederationPlugin.
2497
+ // The shell loads this at runtime via @module-federation/enhanced/runtime.
2498
+ export default function Widget() {
2499
+ return (
2500
+ <section style={{
2501
+ padding: '1rem', border: '1px solid #e2e8f0',
2502
+ borderRadius: '8px', background: '#f8fafc', maxWidth: '400px',
2503
+ }}>
2504
+ <h2 style={{ margin: '0 0 0.5rem', fontSize: '1rem' }}>Remote Widget</h2>
2505
+ <p style={{ margin: 0, color: '#475569', fontSize: '0.875rem' }}>
2506
+ Loaded via <code>@module-federation/enhanced/runtime</code> \u2014 client-side only.
2507
+ </p>
2508
+ </section>
2509
+ );
2510
+ }
2511
+ `,
2512
+ "apps/mf-remote/public/index.html": `<!DOCTYPE html>
2513
+ <html lang="en">
2514
+ <head><meta charset="UTF-8" /><title>MF Remote</title></head>
2515
+ <body><div id="root"></div></body>
2516
+ </html>
2517
+ `,
2518
+ };
2519
+
2520
+ export const NEXTJS_MF_RUNTIME_API_LAB: FrontendLabWorkspace = {
2521
+ version: 1,
2522
+ label: "Next.js \u2014 MF Runtime API",
2523
+ type: "module-federation",
2524
+ activeFile: "apps/shell/src/components/FederatedWidget.tsx",
2525
+ files: NEXTJS_MF_RUNTIME_API_FILES,
2526
+ };
2527
+
2528
+ // ─── Rspack Shell + Webpack Remote ───────────────────────────────────────────
2529
+ const RSPACK_SHELL_FILES: Record<string, string> = {
2530
+ "README.md": `# Rspack Shell \u2014 Native Module Federation 2.0
2531
+
2532
+ ## What this shows
2533
+ An Rspack-bundled React shell consuming a webpack remote using native Module
2534
+ Federation support. Rspack mirrors webpack's API and ships MF support out of the
2535
+ box \u2014 no extra plugins or workarounds needed.
2536
+
2537
+ ## Key files
2538
+ - \`apps/rspack-shell/rspack.config.js\` \u2014 shell config with MF plugin + built-in SWC loader
2539
+ - \`apps/webpack-remote/webpack.config.js\` \u2014 remote exposing \`./Widget\`
2540
+
2541
+ ## Why Rspack for the shell
2542
+ If Module Federation is a hard requirement, the community recommends running the
2543
+ host/orchestrator on Rspack (or Modern.js) rather than Next.js. Next.js remotes
2544
+ still work \u2014 here a plain webpack remote is used for simplicity.
2545
+ `,
2546
+ "package.json": `{
2547
+ "name": "rspack-shell-mf-lab",
2548
+ "private": true,
2549
+ "workspaces": ["apps/rspack-shell", "apps/webpack-remote"],
2550
+ "scripts": {
2551
+ "dev": "concurrently -k -n remote,shell -c magenta,cyan 'npm run dev --workspace=rspack-mf-remote' 'npm run dev --workspace=rspack-mf-shell'"
2552
+ },
2553
+ "devDependencies": { "concurrently": "^9.2.1" }
2554
+ }
2555
+ `,
2556
+ "apps/rspack-shell/package.json": `{
2557
+ "name": "rspack-mf-shell",
2558
+ "private": true,
2559
+ "scripts": { "dev": "rspack serve" },
2560
+ "dependencies": {
2561
+ "react": "^18.0.0",
2562
+ "react-dom": "^18.0.0"
2563
+ },
2564
+ "devDependencies": {
2565
+ "@rspack/core": "latest",
2566
+ "@rspack/cli": "latest"
2567
+ }
2568
+ }
2569
+ `,
2570
+ "apps/rspack-shell/rspack.config.js": `const rspack = require('@rspack/core');
2571
+
2572
+ module.exports = {
2573
+ entry: './src/index.jsx',
2574
+ mode: 'development',
2575
+ output: { publicPath: 'auto' },
2576
+ devServer: {
2577
+ // PORT is read from env — injected by the lab runner.
2578
+ port: parseInt(process.env.HOST_PORT, 10) || 3000,
2579
+ hot: true,
2580
+ },
2581
+ resolve: { extensions: ['.js', '.jsx'] },
2582
+ module: {
2583
+ rules: [
2584
+ {
2585
+ test: /\\.(js|jsx)$/,
2586
+ // Rspack ships a built-in SWC loader — no Babel or extra packages needed.
2587
+ loader: 'builtin:swc-loader',
2588
+ options: {
2589
+ jsc: {
2590
+ parser: { syntax: 'ecmascript', jsx: true },
2591
+ transform: { react: { runtime: 'automatic' } },
2592
+ },
2593
+ },
2594
+ type: 'javascript/auto',
2595
+ },
2596
+ ],
2597
+ },
2598
+ plugins: [
2599
+ // Rspack has first-class Module Federation support via rspack.container.
2600
+ new rspack.container.ModuleFederationPlugin({
2601
+ name: 'rspackShell',
2602
+ remotes: {
2603
+ // mfRemote is the webpack remote at REMOTE_PORT.
2604
+ mfRemote:
2605
+ 'mfRemote@http://localhost:' +
2606
+ (process.env.REMOTE_PORT || '3001') +
2607
+ '/remoteEntry.js',
2608
+ },
2609
+ shared: {
2610
+ react: { singleton: true },
2611
+ 'react-dom': { singleton: true },
2612
+ },
2613
+ }),
2614
+ new rspack.HtmlRspackPlugin({ template: './index.html' }),
2615
+ ],
2616
+ };
2617
+ `,
2618
+ "apps/rspack-shell/index.html": `<!DOCTYPE html>
2619
+ <html lang="en">
2620
+ <head>
2621
+ <meta charset="UTF-8" />
2622
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
2623
+ <title>Rspack Shell</title>
2624
+ </head>
2625
+ <body>
2626
+ <div id="root"></div>
2627
+ </body>
2628
+ </html>
2629
+ `,
2630
+ "apps/rspack-shell/src/index.jsx": `import { createRoot } from 'react-dom/client';
2631
+ import App from './App';
2632
+
2633
+ // Rspack's SWC loader handles JSX via builtin:swc-loader (no Babel needed).
2634
+ createRoot(document.getElementById('root')).render(<App />);
2635
+ `,
2636
+ "apps/rspack-shell/src/App.jsx": `import React, { Suspense, lazy } from 'react';
2637
+
2638
+ // mfRemote is declared in rspack.config.js under remotes.
2639
+ // Rspack resolves this lazy import to the federated container at runtime.
2640
+ const RemoteWidget = lazy(() => import('mfRemote/Widget'));
2641
+
2642
+ export default function App() {
2643
+ return (
2644
+ <main style={{ padding: '2rem', fontFamily: 'system-ui, sans-serif' }}>
2645
+ <h1>Rspack Shell</h1>
2646
+ <p style={{ color: '#64748b' }}>
2647
+ Bundled with Rspack \u2014 native Module Federation, no plugins or workarounds.
2648
+ The widget below is loaded from a separate webpack remote.
2649
+ </p>
2650
+ <Suspense fallback={<p>Loading remote widget\u2026</p>}>
2651
+ <RemoteWidget />
2652
+ </Suspense>
2653
+ </main>
2654
+ );
2655
+ }
2656
+ `,
2657
+ "apps/webpack-remote/package.json": `{
2658
+ "name": "rspack-mf-remote",
2659
+ "private": true,
2660
+ "scripts": { "dev": "webpack serve" },
2661
+ "dependencies": {
2662
+ "react": "^18.0.0",
2663
+ "react-dom": "^18.0.0"
2664
+ },
2665
+ "devDependencies": {
2666
+ "webpack": "^5",
2667
+ "webpack-cli": "^5",
2668
+ "webpack-dev-server": "^5",
2669
+ "html-webpack-plugin": "^5",
2670
+ "@babel/core": "^7",
2671
+ "@babel/preset-env": "^7",
2672
+ "@babel/preset-react": "^7",
2673
+ "babel-loader": "^9"
2674
+ }
2675
+ }
2676
+ `,
2677
+ "apps/webpack-remote/webpack.config.js": `const path = require('path');
2678
+ const webpack = require('webpack');
2679
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
2680
+
2681
+ module.exports = {
2682
+ entry: './src/index.js',
2683
+ mode: 'development',
2684
+ output: {
2685
+ path: path.resolve(__dirname, 'dist'),
2686
+ publicPath: 'auto',
2687
+ },
2688
+ devServer: {
2689
+ port: parseInt(process.env.REMOTE_PORT, 10) || 3001,
2690
+ hot: true,
2691
+ headers: { 'Access-Control-Allow-Origin': '*' },
2692
+ },
2693
+ module: {
2694
+ rules: [
2695
+ {
2696
+ test: /\\.(js|jsx)$/,
2697
+ exclude: /node_modules/,
2698
+ use: {
2699
+ loader: 'babel-loader',
2700
+ options: { presets: ['@babel/preset-env', '@babel/preset-react'] },
2701
+ },
2702
+ },
2703
+ ],
2704
+ },
2705
+ resolve: { extensions: ['.js', '.jsx'] },
2706
+ plugins: [
2707
+ new webpack.container.ModuleFederationPlugin({
2708
+ name: 'mfRemote',
2709
+ filename: 'remoteEntry.js',
2710
+ exposes: { './Widget': './src/exposes/Widget' },
2711
+ shared: {
2712
+ react: { singleton: true, eager: true, requiredVersion: '^18.0.0' },
2713
+ 'react-dom': { singleton: true, eager: true, requiredVersion: '^18.0.0' },
2714
+ },
2715
+ }),
2716
+ new HtmlWebpackPlugin({ template: './public/index.html' }),
2717
+ ],
2718
+ };
2719
+ `,
2720
+ "apps/webpack-remote/src/index.js": `// Remote bootstrap page. MF consumers load ./Widget via remoteEntry.js.
2721
+ document.getElementById('root').innerHTML =
2722
+ '<div style="padding:2rem;font-family:system-ui">' +
2723
+ '<h2>webpack-remote \u2014 running</h2>' +
2724
+ '<p>Exposes <code>mfRemote/Widget</code> via Module Federation.</p>' +
2725
+ '</div>';
2726
+ `,
2727
+ "apps/webpack-remote/src/exposes/Widget.jsx": `import React from 'react';
2728
+
2729
+ // Exposed as mfRemote/Widget. The Rspack shell loads this via lazy import.
2730
+ export default function Widget() {
2731
+ return (
2732
+ <section style={{
2733
+ padding: '1rem', border: '1px solid #e2e8f0',
2734
+ borderRadius: '8px', background: '#f8fafc', maxWidth: '400px',
2735
+ }}>
2736
+ <h2 style={{ margin: '0 0 0.5rem', fontSize: '1rem' }}>Remote Widget</h2>
2737
+ <p style={{ margin: 0, color: '#475569', fontSize: '0.875rem' }}>
2738
+ Served by a webpack remote, consumed by the Rspack shell via native MF.
2739
+ </p>
2740
+ </section>
2741
+ );
2742
+ }
2743
+ `,
2744
+ "apps/webpack-remote/public/index.html": `<!DOCTYPE html>
2745
+ <html lang="en">
2746
+ <head><meta charset="UTF-8" /><title>Webpack Remote</title></head>
2747
+ <body><div id="root"></div></body>
2748
+ </html>
2749
+ `,
2750
+ };
2751
+
2752
+ export const RSPACK_SHELL_LAB: FrontendLabWorkspace = {
2753
+ version: 1,
2754
+ label: "Rspack Shell \u2014 Native MF 2.0",
2755
+ type: "module-federation",
2756
+ activeFile: "apps/rspack-shell/rspack.config.js",
2757
+ files: RSPACK_SHELL_FILES,
2758
+ };
2759
+
1554
2760
  export function defaultForType(type: FrontendLabType): FrontendLabWorkspace {
1555
2761
  if (type === "nextjs") return DEFAULT_NEXTJS_LAB;
1556
2762
  if (type === "module-federation") return DEFAULT_MODULE_FEDERATION_LAB;