diff --git a/.changeset/ai-eager-wolf.md b/.changeset/ai-eager-wolf.md new file mode 100644 index 00000000000..ab5fe276767 --- /dev/null +++ b/.changeset/ai-eager-wolf.md @@ -0,0 +1,12 @@ +--- +"@module-federation/runtime-core": minor +--- + +Added support for OR ranges in semantic version satisfaction logic with comprehensive unit tests. + +- Implemented parsing for OR (||) conditions in version ranges. + - Split input ranges by || to evaluate alternatives individually. + - Ensured logical handling of wildcards '*' and 'x' within ranges. +- Refactored internal parsing to support more complex range constructs. +- Added comprehensive test cases to cover diverse scenarios for OR range support. +- Introduced error handling during range processing, with console logging for tracking issues. diff --git a/.changeset/ai-happy-fox.md b/.changeset/ai-happy-fox.md new file mode 100644 index 00000000000..51821121aff --- /dev/null +++ b/.changeset/ai-happy-fox.md @@ -0,0 +1,12 @@ +--- +"@module-federation/nextjs-mf": minor +--- + +Refactor and enhance module federation support for Next.js. + +- Introduced `getShareScope` function to dynamically generate the default share scope based on the client or server environment, replacing static DEFAULT_SHARE_SCOPE declarations. +- Implemented `RscManifestInterceptPlugin` to intercept and modify client reference manifests, ensuring proper prefix handling. +- Refined server-side externals handling to ensure shared federation modules are bundled. +- Simplified and modularized sharing logic by creating distinct functions for React, React DOM, React JSX Runtime, and React JSX Dev Runtime package configurations. +- Captured the original webpack public path for potential use in plugins and adjustments. +- Enhanced logging for debug tracing of shared module resolution processes in runtimePlugin. diff --git a/.changeset/ai-hungry-bear.md b/.changeset/ai-hungry-bear.md new file mode 100644 index 00000000000..444a92082bf --- /dev/null +++ b/.changeset/ai-hungry-bear.md @@ -0,0 +1,9 @@ +"@module-federation/enhanced": minor +--- + +Enhancements to layer handling in module federation tests and configuration. + +- Improved handling of `shareKey` for layers within `ConsumeSharedPlugin` and `ProvideSharedPlugin`. + - Conditionally prepend the `shareKey` with the `layer` if applicable. +- Introduced new layer configurations to support more nuanced federation scenarios that consider multiple layers of dependency. +``` diff --git a/.changeset/ai-sleepy-fox.md b/.changeset/ai-sleepy-fox.md new file mode 100644 index 00000000000..36d84f71503 --- /dev/null +++ b/.changeset/ai-sleepy-fox.md @@ -0,0 +1,9 @@ +--- +"@module-federation/enhanced": patch +--- + +Refactored module sharing configuration handling. + +- Simplified plugin schema for better maintainability +- Improved layer-based module sharing test coverage +- Removed redundant plugin exports diff --git a/.changeset/ai-sleepy-tiger.md b/.changeset/ai-sleepy-tiger.md new file mode 100644 index 00000000000..6c11ece998e --- /dev/null +++ b/.changeset/ai-sleepy-tiger.md @@ -0,0 +1,6 @@ +--- +"@module-federation/runtime": minor +--- + +- Added a new property 'layer' of type string or null to SharedConfig. +``` diff --git a/.changeset/brown-badgers-fetch.md b/.changeset/brown-badgers-fetch.md new file mode 100644 index 00000000000..00d28f1f096 --- /dev/null +++ b/.changeset/brown-badgers-fetch.md @@ -0,0 +1,5 @@ +--- +'@module-federation/enhanced': minor +--- + +support request option on ConsumeSharePlugin. Allows matching requests like the object key of shared does diff --git a/.changeset/shy-snails-battle.md b/.changeset/shy-snails-battle.md new file mode 100644 index 00000000000..8d4fb5ec2f1 --- /dev/null +++ b/.changeset/shy-snails-battle.md @@ -0,0 +1,5 @@ +--- +'@module-federation/enhanced': minor +--- + +Layer support for Provide Share Plugin diff --git a/apps/next-app-router/next-app-router-4000/app/layout.tsx b/apps/next-app-router/next-app-router-4000/app/layout.tsx index ed79b53661a..d85b4370922 100644 --- a/apps/next-app-router/next-app-router-4000/app/layout.tsx +++ b/apps/next-app-router/next-app-router-4000/app/layout.tsx @@ -3,7 +3,7 @@ import { AddressBar } from '#/ui/address-bar'; import Byline from '#/ui/byline'; // import { GlobalNav } from 'remote_4001/GlobalNav(rsc)'; import { Metadata } from 'next'; - +console.log(require('remote_4001/Button')); export const metadata: Metadata = { title: { default: 'Next.js App Router', diff --git a/apps/next-app-router/next-app-router-4000/app/page.tsx b/apps/next-app-router/next-app-router-4000/app/page.tsx index 0c8b03c0c0e..b7ef75430ee 100644 --- a/apps/next-app-router/next-app-router-4000/app/page.tsx +++ b/apps/next-app-router/next-app-router-4000/app/page.tsx @@ -1,7 +1,8 @@ import { demos } from '#/lib/demos'; import Link from 'next/link'; +import { lazy } from 'react'; import dynamic from 'next/dynamic'; -const Button = dynamic(() => import('remote_4001/Button'), { ssr: true }); +const Button = lazy(() => import('remote_4001/Button')); export default function Page() { return ( diff --git a/apps/next-app-router/next-app-router-4000/next.config.js b/apps/next-app-router/next-app-router-4000/next.config.js index da2ee672d59..e5e47e6d24f 100644 --- a/apps/next-app-router/next-app-router-4000/next.config.js +++ b/apps/next-app-router/next-app-router-4000/next.config.js @@ -38,8 +38,8 @@ const nextConfig = { filename: 'static/chunks/remoteEntry.js', remotes: { remote_4001: remotes.remote_4001, - shop: remotes.shop, - checkout: remotes.checkout, + // shop: remotes.shop, + // checkout: remotes.checkout, }, shared: { // 'react': { diff --git a/apps/next-app-router/next-app-router-4000/project.json b/apps/next-app-router/next-app-router-4000/project.json index 63f23344afd..31ae3b8365f 100644 --- a/apps/next-app-router/next-app-router-4000/project.json +++ b/apps/next-app-router/next-app-router-4000/project.json @@ -27,8 +27,8 @@ "serve": { "executor": "nx:run-commands", "options": { - "command": "pnpm dev", - "cwd": "apps/next-app-router-4000" + "command": "npm run dev", + "cwd": "apps/next-app-router/next-app-router-4000" }, "dependsOn": [ { diff --git a/apps/next-app-router/next-app-router-4001/app/layout.tsx b/apps/next-app-router/next-app-router-4001/app/layout.tsx index ba6662a2c6a..0a34ae994e1 100644 --- a/apps/next-app-router/next-app-router-4001/app/layout.tsx +++ b/apps/next-app-router/next-app-router-4001/app/layout.tsx @@ -1,8 +1,8 @@ import '#/styles/globals.css'; -import { AddressBar } from '#/ui/address-bar'; -import Byline from '#/ui/byline'; +// import { AddressBar } from '#/ui/address-bar'; +// import Byline from '#/ui/byline'; import { GlobalNav } from '#/ui/global-nav'; -import { Metadata } from 'next'; +// import { Metadata } from 'next'; export const metadata: Metadata = { title: { @@ -36,15 +36,13 @@ export default function RootLayout({
-
- -
+
{/**/}
{children}
- + {/**/}
diff --git a/apps/next-app-router/next-app-router-4001/app/page.tsx b/apps/next-app-router/next-app-router-4001/app/page.tsx index da496ca4858..925e8ce10ef 100644 --- a/apps/next-app-router/next-app-router-4001/app/page.tsx +++ b/apps/next-app-router/next-app-router-4001/app/page.tsx @@ -1,6 +1,7 @@ import { demos } from '#/lib/demos'; import Link from 'next/link'; - +import React from 'react'; +console.log(React); export default function Page() { return (
diff --git a/apps/next-app-router/next-app-router-4001/next-env.d.ts b/apps/next-app-router/next-app-router-4001/next-env.d.ts index 40c3d68096c..725dd6f2451 100644 --- a/apps/next-app-router/next-app-router-4001/next-env.d.ts +++ b/apps/next-app-router/next-app-router-4001/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/apps/next-app-router/next-app-router-4001/next.config.js b/apps/next-app-router/next-app-router-4001/next.config.js index 714745c649c..7c5ff89e450 100644 --- a/apps/next-app-router/next-app-router-4001/next.config.js +++ b/apps/next-app-router/next-app-router-4001/next.config.js @@ -23,10 +23,10 @@ const nextConfig = { // Core UI Components './Button': './ui/button', // './Header': isServer ? './ui/header?rsc' : './ui/header?shared', - './Footer': './ui/footer', + // './Footer': './ui/footer', // './GlobalNav(rsc)': isServer ? './ui/global-nav?rsc' : './ui/global-nav', // './GlobalNav(ssr)': isServer ? './ui/global-nav?ssr' : './ui/global-nav', - './GlobalNav': './ui/global-nav', + // './GlobalNav': './ui/global-nav', // // // Product Related Components // './ProductCard': './ui/product-card', diff --git a/apps/next-app-router/next-app-router-4001/pages/router-test.tsx b/apps/next-app-router/next-app-router-4001/pages/router-test.tsx new file mode 100644 index 00000000000..a7df7a44b71 --- /dev/null +++ b/apps/next-app-router/next-app-router-4001/pages/router-test.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import Link from 'next/link'; + +const RouterTestPage = () => { + return ( +
+

Router Test Page (in Pages Directory)

+

+ This page exists in the 'pages' directory of an app that primarily uses + the App Router ('app' directory). +

+

+ Below are links demonstrating navigation potentially involving different + router types: +

+
    +
  • + {/* Link within the current app (likely handled by App Router if root exists there) */} + Link to App Root +
  • +
  • + {/* Link to an external app known to use Pages Router */} + + Link to Home App (Pages Router via full URL) + +
  • +
  • Placeholder for other routing/federation examples.
  • +
+

+ Note: The instruction "sets both routers" was interpreted as + demonstrating links to potentially different router contexts. +

+
+ ); +}; + +export default RouterTestPage; diff --git a/apps/next-app-router/next-app-router-4001/project.json b/apps/next-app-router/next-app-router-4001/project.json index cba17d562e1..cea74e5f6f6 100644 --- a/apps/next-app-router/next-app-router-4001/project.json +++ b/apps/next-app-router/next-app-router-4001/project.json @@ -1,7 +1,7 @@ { "name": "next-app-router-4001", "$schema": "../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "apps/next-app-router-4001", + "sourceRoot": "apps/next-app-router/next-app-router-4001", "projectType": "application", "tags": [], "targets": { @@ -27,8 +27,8 @@ "serve": { "executor": "nx:run-commands", "options": { - "command": "pnpm dev", - "cwd": "apps/next-app-router-4001" + "command": "npm run dev", + "cwd": "apps/next-app-router/next-app-router-4001" }, "dependsOn": [ { diff --git a/package.json b/package.json index 10f9154706e..cccb64ee98d 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "prepare": "husky install", "changeset": "changeset", "build:packages": "npx nx affected -t build --parallel=10 --exclude='*,!tag:type:pkg'", - "changegen": "./changeset-gen.js --path ./packages/enhanced --staged &&./changeset-gen.js --path ./packages/cli --staged && ./changeset-gen.js --path ./packages/node --staged && ./changeset-gen.js --path ./packages/runtime --staged && ./changeset-gen.js --path ./packages/data-prefetch --staged && ./changeset-gen.js --path ./packages/nextjs-mf --staged && ./changeset-gen.js --path ./packages/dts-plugin --staged", + "changegen": "node ./changeset-gen.js --path ./packages/runtime-core && ./changeset-gen.js --path ./packages/enhanced --staged &&./changeset-gen.js --path ./packages/cli --staged && ./changeset-gen.js --path ./packages/node --staged && ./changeset-gen.js --path ./packages/runtime --staged && ./changeset-gen.js --path ./packages/data-prefetch --staged && ./changeset-gen.js --path ./packages/nextjs-mf --staged && ./changeset-gen.js --path ./packages/dts-plugin --staged", "commitgen:staged": "./commit-gen.js --path ./packages --staged", "commitgen:main": "./commit-gen.js --path ./packages", "changeset:status": "changeset status", diff --git a/packages/nextjs-mf/src/constants.ts b/packages/nextjs-mf/src/constants.ts new file mode 100644 index 00000000000..cfabb664e66 --- /dev/null +++ b/packages/nextjs-mf/src/constants.ts @@ -0,0 +1,207 @@ +export const NEXT_QUERY_PARAM_PREFIX = 'nxtP'; +export const NEXT_INTERCEPTION_MARKER_PREFIX = 'nxtI'; + +export const MATCHED_PATH_HEADER = 'x-matched-path'; +export const PRERENDER_REVALIDATE_HEADER = 'x-prerender-revalidate'; +export const PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER = + 'x-prerender-revalidate-if-generated'; + +export const RSC_PREFETCH_SUFFIX = '.prefetch.rsc'; +export const RSC_SEGMENTS_DIR_SUFFIX = '.segments'; +export const RSC_SEGMENT_SUFFIX = '.segment.rsc'; +export const RSC_SUFFIX = '.rsc'; +export const ACTION_SUFFIX = '.action'; +export const NEXT_DATA_SUFFIX = '.json'; +export const NEXT_META_SUFFIX = '.meta'; +export const NEXT_BODY_SUFFIX = '.body'; + +export const NEXT_CACHE_TAGS_HEADER = 'x-next-cache-tags'; +export const NEXT_CACHE_REVALIDATED_TAGS_HEADER = 'x-next-revalidated-tags'; +export const NEXT_CACHE_REVALIDATE_TAG_TOKEN_HEADER = + 'x-next-revalidate-tag-token'; + +export const NEXT_RESUME_HEADER = 'next-resume'; + +// if these change make sure we update the related +// documentation as well +export const NEXT_CACHE_TAG_MAX_ITEMS = 128; +export const NEXT_CACHE_TAG_MAX_LENGTH = 256; +export const NEXT_CACHE_SOFT_TAG_MAX_LENGTH = 1024; +export const NEXT_CACHE_IMPLICIT_TAG_ID = '_N_T_'; + +// in seconds +export const CACHE_ONE_YEAR = 31536000; + +// in seconds, represents revalidate=false. I.e. never revaliate. +// We use this value since it can be represented as a V8 SMI for optimal performance. +// It can also be serialized as JSON if it ever leaks accidentally as an actual value. +export const INFINITE_CACHE = 0xfffffffe; + +// Patterns to detect middleware files +export const MIDDLEWARE_FILENAME = 'middleware'; +export const MIDDLEWARE_LOCATION_REGEXP = `(?:src/)?${MIDDLEWARE_FILENAME}`; + +// Pattern to detect instrumentation hooks file +export const INSTRUMENTATION_HOOK_FILENAME = 'instrumentation'; + +// Because on Windows absolute paths in the generated code can break because of numbers, eg 1 in the path, +// we have to use a private alias +export const PAGES_DIR_ALIAS = 'private-next-pages'; +export const DOT_NEXT_ALIAS = 'private-dot-next'; +export const ROOT_DIR_ALIAS = 'private-next-root-dir'; +export const APP_DIR_ALIAS = 'private-next-app-dir'; +export const RSC_MOD_REF_PROXY_ALIAS = 'private-next-rsc-mod-ref-proxy'; +export const RSC_ACTION_VALIDATE_ALIAS = 'private-next-rsc-action-validate'; +export const RSC_ACTION_PROXY_ALIAS = 'private-next-rsc-server-reference'; +export const RSC_CACHE_WRAPPER_ALIAS = 'private-next-rsc-cache-wrapper'; +export const RSC_ACTION_ENCRYPTION_ALIAS = 'private-next-rsc-action-encryption'; +export const RSC_ACTION_CLIENT_WRAPPER_ALIAS = + 'private-next-rsc-action-client-wrapper'; + +export const PUBLIC_DIR_MIDDLEWARE_CONFLICT = `You can not have a '_next' folder inside of your public folder. This conflicts with the internal '/_next' route. https://nextjs.org/docs/messages/public-next-folder-conflict`; + +export const SSG_GET_INITIAL_PROPS_CONFLICT = `You can not use getInitialProps with getStaticProps. To use SSG, please remove your getInitialProps`; + +export const SERVER_PROPS_GET_INIT_PROPS_CONFLICT = `You can not use getInitialProps with getServerSideProps. Please remove getInitialProps.`; + +export const SERVER_PROPS_SSG_CONFLICT = `You can not use getStaticProps or getStaticPaths with getServerSideProps. To use SSG, please remove getServerSideProps`; + +export const STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR = `can not have getInitialProps/getServerSideProps, https://nextjs.org/docs/messages/404-get-initial-props`; + +export const SERVER_PROPS_EXPORT_ERROR = `pages with \`getServerSideProps\` can not be exported. See more info here: https://nextjs.org/docs/messages/gssp-export`; + +export const GSP_NO_RETURNED_VALUE = + 'Your `getStaticProps` function did not return an object. Did you forget to add a `return`?'; +export const GSSP_NO_RETURNED_VALUE = + 'Your `getServerSideProps` function did not return an object. Did you forget to add a `return`?'; + +export const UNSTABLE_REVALIDATE_RENAME_ERROR = + 'The `unstable_revalidate` property is available for general use.\n' + + 'Please use `revalidate` instead.'; + +export const GSSP_COMPONENT_MEMBER_ERROR = `can not be attached to a page's component and must be exported from the page. See more info here: https://nextjs.org/docs/messages/gssp-component-member`; + +export const NON_STANDARD_NODE_ENV = `You are using a non-standard "NODE_ENV" value in your environment. This creates inconsistencies in the project and is strongly advised against. Read more: https://nextjs.org/docs/messages/non-standard-node-env`; + +export const SSG_FALLBACK_EXPORT_ERROR = `Pages with \`fallback\` enabled in \`getStaticPaths\` can not be exported. See more info here: https://nextjs.org/docs/messages/ssg-fallback-true-export`; + +export const ESLINT_DEFAULT_DIRS = ['app', 'pages', 'components', 'lib', 'src']; + +export const SERVER_RUNTIME: Record = { + edge: 'edge', + experimentalEdge: 'experimental-edge', + nodejs: 'nodejs', +}; + +/** + * The names of the webpack layers. These layers are the primitives for the + * webpack chunks. + */ +export const WEBPACK_LAYERS_NAMES = { + /** + * The layer for the shared code between the client and server bundles. + */ + shared: 'shared', + /** + * The layer for server-only runtime and picking up `react-server` export conditions. + * Including app router RSC pages and app router custom routes and metadata routes. + */ + reactServerComponents: 'rsc', + /** + * Server Side Rendering layer for app (ssr). + */ + serverSideRendering: 'ssr', + /** + * The browser client bundle layer for actions. + */ + actionBrowser: 'action-browser', + /** + * The Node.js bundle layer for the API routes. + */ + apiNode: 'api-node', + /** + * The Edge Lite bundle layer for the API routes. + */ + apiEdge: 'api-edge', + /** + * The layer for the middleware code. + */ + middleware: 'middleware', + /** + * The layer for the instrumentation hooks. + */ + instrument: 'instrument', + /** + * The layer for assets on the edge. + */ + edgeAsset: 'edge-asset', + /** + * The browser client bundle layer for App directory. + */ + appPagesBrowser: 'app-pages-browser', + /** + * The browser client bundle layer for Pages directory. + */ + pagesDirBrowser: 'pages-dir-browser', + /** + * The Edge Lite bundle layer for Pages directory. + */ + pagesDirEdge: 'pages-dir-edge', + /** + * The Node.js bundle layer for Pages directory. + */ + pagesDirNode: 'pages-dir-node', +} as const; + +export type WebpackLayerName = + (typeof WEBPACK_LAYERS_NAMES)[keyof typeof WEBPACK_LAYERS_NAMES]; + +const WEBPACK_LAYERS = { + ...WEBPACK_LAYERS_NAMES, + GROUP: { + builtinReact: [ + WEBPACK_LAYERS_NAMES.reactServerComponents, + WEBPACK_LAYERS_NAMES.actionBrowser, + ], + serverOnly: [ + WEBPACK_LAYERS_NAMES.reactServerComponents, + WEBPACK_LAYERS_NAMES.actionBrowser, + WEBPACK_LAYERS_NAMES.instrument, + WEBPACK_LAYERS_NAMES.middleware, + ], + neutralTarget: [ + // pages api + WEBPACK_LAYERS_NAMES.apiNode, + WEBPACK_LAYERS_NAMES.apiEdge, + ], + clientOnly: [ + WEBPACK_LAYERS_NAMES.serverSideRendering, + WEBPACK_LAYERS_NAMES.appPagesBrowser, + ], + bundled: [ + WEBPACK_LAYERS_NAMES.reactServerComponents, + WEBPACK_LAYERS_NAMES.actionBrowser, + WEBPACK_LAYERS_NAMES.serverSideRendering, + WEBPACK_LAYERS_NAMES.appPagesBrowser, + WEBPACK_LAYERS_NAMES.shared, + WEBPACK_LAYERS_NAMES.instrument, + WEBPACK_LAYERS_NAMES.middleware, + ], + appPages: [ + // app router pages and layouts + WEBPACK_LAYERS_NAMES.reactServerComponents, + WEBPACK_LAYERS_NAMES.serverSideRendering, + WEBPACK_LAYERS_NAMES.appPagesBrowser, + WEBPACK_LAYERS_NAMES.actionBrowser, + ], + }, +}; + +const WEBPACK_RESOURCE_QUERIES = { + edgeSSREntry: '__next_edge_ssr_entry__', + metadata: '__next_metadata__', + metadataRoute: '__next_metadata_route__', + metadataImageMeta: '__next_metadata_image_meta__', +}; + +export { WEBPACK_LAYERS, WEBPACK_RESOURCE_QUERIES }; diff --git a/packages/nextjs-mf/src/internal-helpers.ts b/packages/nextjs-mf/src/internal-helpers.ts new file mode 100644 index 00000000000..888cb312506 --- /dev/null +++ b/packages/nextjs-mf/src/internal-helpers.ts @@ -0,0 +1,135 @@ +import type { Compiler } from 'webpack'; +import { WEBPACK_LAYERS, WebpackLayerName } from './constants'; +import path from 'path'; + +export const defaultOverrides = { + 'styled-jsx': path.dirname(require.resolve('styled-jsx/package.json')), + 'styled-jsx/style': require.resolve('styled-jsx/style'), + 'styled-jsx/style.js': require.resolve('styled-jsx/style'), +}; +/** + * Safely resolves a module path using require.resolve. + * Logs a warning and returns undefined if resolution fails. + */ +export const safeRequireResolve = ( + id: string, + options?: { + paths?: string[]; + mainFields?: string[]; + conditionNames?: string[]; + }, +): string | undefined => { + try { + return require.resolve(id, options); + } catch (e) { + console.warn( + `[nextjs-mf] Warning: Could not resolve '${id}'. Falling back.`, + e, + ); + return id; + } +}; + +/** + * Safely resolves a module path and attempts to require it to get its version. + * Logs warnings and returns undefined if any step fails. + */ +export function getReactVersionSafely( + aliasPath: string, + context: string, +): string | undefined { + const resolvedPath = safeRequireResolve(aliasPath, { paths: [context] }); + if (!resolvedPath || resolvedPath === aliasPath) { + // Check if fallback was used + // Warning potentially logged by safeRequireResolve or resolution failed + return undefined; + } + try { + // Attempt to require the *resolved* path + const requiredModule = require(resolvedPath); + const version = requiredModule.version; + if (!version) { + console.warn( + `[nextjs-mf] Warning: Resolved '${aliasPath}' at '${resolvedPath}' but it has no 'version' property.`, + ); + return undefined; + } + return version; + } catch (error: any) { + console.warn( + `[nextjs-mf] Warning: Could not require resolved path '${resolvedPath}' for alias '${aliasPath}'. Error: ${error.message}`, + ); + return undefined; + } +} + +/** + * Gets the alias for a given name from the compiler's alias configuration. + * If the alias doesn't exist, it returns the fallback value. + */ +export function getAlias( + compiler: Compiler, + aliasName: string, + fallback: string, +): string { + if ( + !compiler || + !compiler.options || + !compiler.options.resolve || + !compiler.options.resolve.alias + ) { + return fallback; + } + const aliasConfig = compiler.options.resolve.alias as Record< + string, + string | string[] | false + >; + return ( + (aliasConfig[aliasName] as string) || + (aliasConfig[aliasName.replace('$', '')] as string) || + fallback + ); +} + +// Consider also moving createSharedConfig here if it makes sense +// For now, keeping it minimal with only the direct dependencies of the group functions + +export function isWebpackServerOnlyLayer( + layer: WebpackLayerName | null | undefined, +): boolean { + return Boolean( + layer && WEBPACK_LAYERS.GROUP.serverOnly.includes(layer as any), + ); +} + +export function isWebpackClientOnlyLayer( + layer: WebpackLayerName | null | undefined, +): boolean { + return Boolean( + layer && WEBPACK_LAYERS.GROUP.clientOnly.includes(layer as any), + ); +} + +export function isWebpackDefaultLayer( + layer: WebpackLayerName | null | undefined, +): boolean { + return ( + layer === null || + layer === undefined || + layer === WEBPACK_LAYERS.pagesDirBrowser || + layer === WEBPACK_LAYERS.pagesDirEdge || + layer === WEBPACK_LAYERS.pagesDirNode + ); +} + +export function isWebpackBundledLayer( + layer: WebpackLayerName | null | undefined, +): boolean { + return Boolean(layer && WEBPACK_LAYERS.GROUP.bundled.includes(layer as any)); +} + +export function isWebpackAppPagesLayer( + layer: WebpackLayerName | null | undefined, +): boolean { + return Boolean(layer && WEBPACK_LAYERS.GROUP.appPages.includes(layer as any)); +} diff --git a/packages/nextjs-mf/src/internal.ts b/packages/nextjs-mf/src/internal.ts index f25ced295bb..c9793582600 100644 --- a/packages/nextjs-mf/src/internal.ts +++ b/packages/nextjs-mf/src/internal.ts @@ -2,16 +2,9 @@ import type { moduleFederationPlugin, sharePlugin, } from '@module-federation/sdk'; +import type { Compiler } from 'webpack'; -// Extend the SharedConfig type to include layer properties -type ExtendedSharedConfig = sharePlugin.SharedConfig & { - layer?: string; - issuerLayer?: string | string[]; - request?: string; - shareKey?: string; -}; - -const WEBPACK_LAYERS_NAMES = { +export const WEBPACK_LAYERS_NAMES = { /** * The layer for the shared code between the client and server bundles. */ @@ -51,70 +44,8 @@ const WEBPACK_LAYERS_NAMES = { appPagesBrowser: 'app-pages-browser', } as const; -const createSharedConfig = ( - name: string, - layers: (string | undefined)[], - options: { request?: string; import?: false | undefined } = {}, -) => { - return layers.reduce( - (acc, layer) => { - const key = layer ? `${name}-${layer}` : name; - acc[key] = { - singleton: true, - requiredVersion: false, - import: layer ? undefined : (options.import ?? false), - shareKey: options.request ?? name, - request: options.request ?? name, - layer, - issuerLayer: layer, - }; - return acc; - }, - {} as Record, - ); -}; - -const defaultLayers = [ - WEBPACK_LAYERS_NAMES.reactServerComponents, - WEBPACK_LAYERS_NAMES.serverSideRendering, - undefined, -]; - -const navigationLayers = [ - WEBPACK_LAYERS_NAMES.reactServerComponents, - WEBPACK_LAYERS_NAMES.serverSideRendering, -]; - -const reactShares = createSharedConfig('react', defaultLayers); -const reactDomShares = createSharedConfig('react', defaultLayers, { - request: 'react-dom', -}); -const jsxRuntimeShares = createSharedConfig('react/', navigationLayers, { - request: 'react/', - import: undefined, -}); -const nextNavigationShares = createSharedConfig( - 'next-navigation', - navigationLayers, - { request: 'next/navigation' }, -); - -/** - * @typedef SharedObject - * @type {object} - * @property {object} [key] - The key representing the shared object's package name. - * @property {boolean} key.singleton - Whether the shared object should be a singleton. - * @property {boolean} key.requiredVersion - Whether a specific version of the shared object is required. - * @property {boolean} key.eager - Whether the shared object should be eagerly loaded. - * @property {boolean} key.import - Whether the shared object should be imported or not. - * @property {string} key.layer - The webpack layer this shared module belongs to. - * @property {string|string[]} key.issuerLayer - The webpack layer that can import this shared module. - */ -export const DEFAULT_SHARE_SCOPE: moduleFederationPlugin.SharedObject = { - // ...reactShares, - // ...reactDomShares, - // ...nextNavigationShares, - // ...jsxRuntimeShares, +// Group Next.js related packages +const nextGroup = { 'next/dynamic': { requiredVersion: undefined, singleton: true, @@ -131,7 +62,7 @@ export const DEFAULT_SHARE_SCOPE: moduleFederationPlugin.SharedObject = { import: undefined, }, 'next/router': { - requiredVersion: false, + requiredVersion: undefined, singleton: true, import: undefined, }, @@ -145,34 +76,10 @@ export const DEFAULT_SHARE_SCOPE: moduleFederationPlugin.SharedObject = { 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, - }, +}; + +// Group styled-jsx related packages +const styledJsxGroup = { 'styled-jsx': { singleton: true, import: undefined, @@ -181,7 +88,7 @@ export const DEFAULT_SHARE_SCOPE: moduleFederationPlugin.SharedObject = { }, 'styled-jsx/style': { singleton: true, - import: false, + import: undefined, version: require('styled-jsx/package.json').version, requiredVersion: '^' + require('styled-jsx/package.json').version, }, @@ -193,25 +100,48 @@ export const DEFAULT_SHARE_SCOPE: moduleFederationPlugin.SharedObject = { }, }; +// --- New getShareScope Function --- + /** - * Defines a default share scope for the browser environment. - * This function takes the DEFAULT_SHARE_SCOPE and sets eager to undefined and import to undefined for all entries. - * For 'react', 'react-dom', 'next/router', and 'next/link', it sets eager to true. - * The module hoisting system relocates these modules into the right runtime and out of the remote. - * - * @type {SharedObject} - * @returns {SharedObject} - The modified share scope for the browser environment. + * Generates the appropriate default share scope based on the compiler context. + * @param {Compiler} compiler - The webpack compiler instance. + * @returns {moduleFederationPlugin.SharedObject} - The generated share scope. */ +export const getShareScope = ( + compiler: Compiler, +): moduleFederationPlugin.SharedObject => { + const isClient = compiler.options.name === 'client'; + + // Combine the groups manually + let combinedScope: moduleFederationPlugin.SharedObject = { + ...nextGroup, + ...styledJsxGroup, + }; + + // Apply browser-specific modifications + if (isClient) { + combinedScope = Object.entries(combinedScope).reduce( + (acc, [key, value]) => { + // Ensure value is treated correctly if it's a simple string (though unlikely with current groups) + const configValue = + typeof value === 'string' ? { import: value } : value; + + // ONLY change `import: false` to `import: undefined` for client builds. + // Keep other import values (strings, undefined) as they are. + // if (configValue.import === false) { + // acc[key] = { ...configValue, import: undefined }; + // } else { + // // Otherwise, keep the original value entirely + acc[key] = value; + // } + return acc; + }, + {} as moduleFederationPlugin.SharedObject, + ); + } -export const DEFAULT_SHARE_SCOPE_BROWSER: moduleFederationPlugin.SharedObject = - Object.entries(DEFAULT_SHARE_SCOPE).reduce((acc, item) => { - const [key, value] = item as [string, moduleFederationPlugin.SharedConfig]; - - // Set eager and import to undefined for all entries, except for the ones specified above - acc[key] = { ...value, import: undefined }; - - return acc; - }, {} as moduleFederationPlugin.SharedObject); + return combinedScope; +}; /** * Checks if the remote value is an internal or promise delegate module reference. diff --git a/packages/nextjs-mf/src/plugins/NextFederationPlugin/RscManifestInterceptPlugin.ts b/packages/nextjs-mf/src/plugins/NextFederationPlugin/RscManifestInterceptPlugin.ts new file mode 100644 index 00000000000..6b60292d71a --- /dev/null +++ b/packages/nextjs-mf/src/plugins/NextFederationPlugin/RscManifestInterceptPlugin.ts @@ -0,0 +1,109 @@ +import type { Compiler, Compilation } from 'webpack'; +import * as vm from 'vm'; +import { NextFederationPlugin } from './index'; + +const PLUGIN_NAME = 'RscManifestInterceptPlugin'; +const CLIENT_REFERENCE_MANIFEST = 'client-reference-manifest'; + +// Define types for the manifest structure +interface ModuleLoading { + prefix?: string; + [key: string]: any; +} + +interface ManifestEntry { + moduleLoading?: ModuleLoading; + [key: string]: any; +} + +interface RscManifest { + [key: string]: ManifestEntry; +} + +export class RscManifestInterceptPlugin { + apply(compiler: Compiler) { + const { sources, Compilation } = compiler.webpack; + compiler.hooks.afterPlugins.tap(PLUGIN_NAME, (compiler: Compiler) => { + compiler.hooks.compilation.tap( + PLUGIN_NAME, + (compilation: Compilation) => { + compilation.hooks.processAssets.tapPromise( + { + name: `${PLUGIN_NAME}Modify`, + // Run at a later stage to ensure manifest files are available + stage: + // @ts-expect-error use runtime variable in case peer dep not installed + compilation.constructor.PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER, + }, + async (assets) => { + // Get the original public path from NextFederationPlugin + const originalPublicPath = + NextFederationPlugin.originalPublicPath || '/_next/'; + + for (const assetName in assets) { + if (assetName.includes('client-reference-manifest.js')) { + const asset = assets[assetName]; + if (!asset) continue; + const originalSource = asset.source(); + const content = Buffer.isBuffer(originalSource) + ? originalSource.toString() + : String(originalSource); + + try { + // Create a sandbox with globalThis + const sandbox = { + globalThis: { + __RSC_MANIFEST: {} as RscManifest, + }, + }; + + // Create a new VM context + vm.createContext(sandbox); + + // Run the file content in the VM context + vm.runInContext(content, sandbox); + + // Get the manifest object from the sandbox + const manifest = sandbox.globalThis.__RSC_MANIFEST; + + // Check if we need to modify the prefix + let modified = false; + for (const key in manifest) { + if (manifest[key]?.moduleLoading?.prefix === 'auto') { + manifest[key].moduleLoading.prefix = originalPublicPath; + modified = true; + } + } + + if (modified) { + // Serialize the modified manifest back to a string + const newContent = `globalThis.__RSC_MANIFEST=${JSON.stringify(manifest)};`; + + // Create a new source + const newSource = new sources.RawSource(newContent); + + // Update the asset using the compilation API + compilation.updateAsset(assetName, newSource); + } + } catch (e: any) { + console.error( + `[${PLUGIN_NAME}] Error processing manifest ${assetName}:`, + e.message, + ); + compilation.errors.push( + new compiler.webpack.WebpackError( + `${PLUGIN_NAME}: Failed to process ${assetName}: ${e.message}`, + ), + ); + } + } + } + }, + ); + }, + ); + }); + } +} + +export default RscManifestInterceptPlugin; diff --git a/packages/nextjs-mf/src/plugins/NextFederationPlugin/apply-server-plugins.ts b/packages/nextjs-mf/src/plugins/NextFederationPlugin/apply-server-plugins.ts index a61a193fa12..d6aa17a9a34 100644 --- a/packages/nextjs-mf/src/plugins/NextFederationPlugin/apply-server-plugins.ts +++ b/packages/nextjs-mf/src/plugins/NextFederationPlugin/apply-server-plugins.ts @@ -2,9 +2,11 @@ import type { WebpackOptionsNormalized, Compiler, ExternalItemFunctionData, + ResolveData, } from 'webpack'; import type { moduleFederationPlugin } from '@module-federation/sdk'; import path from 'path'; +import fs from 'fs'; import InvertedContainerPlugin from '../container/InvertedContainerPlugin'; import UniverseEntryChunkTrackerPlugin from '@module-federation/node/universe-entry-chunk-tracker-plugin'; @@ -97,11 +99,37 @@ export function configureServerLibraryAndFilename( options.filename = path.basename(options.filename as string); } +// Define a more specific type for the context object passed to the external function +// based on the stringified output and common webpack externals function arguments +interface CustomExternalContext { + context?: string; + request?: string; + dependencyType?: string; + contextInfo?: { + issuer?: string; + issuerLayer?: string | null; // Layer can be null + compiler?: string; + }; + getResolve?: ( + options?: any, + ) => ( + resolveContext: string, + requestToResolve: string, + callback: ( + err?: Error | null, + result?: string | false, + resolveData?: ResolveData, + ) => void, + ) => void; // A basic signature for getResolve + layer?: string | null; // Include layer if available directly on ctx +} + /** * Patches Next.js' default externals function to ensure shared modules are bundled and not treated as external. + * (Updated to use Promise-based signature) * * @param {Compiler} compiler - The Webpack compiler instance. - * @param {ModuleFederationPluginOptions} options - The ModuleFederationPluginOptions instance. + * @param {moduleFederationPlugin.ModuleFederationPluginOptions} options - The ModuleFederationPluginOptions instance. */ export function handleServerExternals( compiler: Compiler, @@ -114,48 +142,71 @@ export function handleServerExternals( if (functionIndex !== -1) { const originalExternals = compiler.options.externals[functionIndex] as ( - data: ExternalItemFunctionData, - callback: any, - ) => undefined | string; - - compiler.options.externals[functionIndex] = async function ( - ctx: ExternalItemFunctionData, - callback: any, - ) { - const fromNext = await originalExternals(ctx, callback); + data: CustomExternalContext, + ) => Promise; + + compiler.options.externals[functionIndex] = async ({ + context, + request, + dependencyType, + contextInfo, + getResolve, + layer, + }: CustomExternalContext): Promise => { + let fromNext: string | boolean | undefined; + + try { + fromNext = await originalExternals({ + context, + request, + dependencyType, + contextInfo, + getResolve, + layer, + }); + } catch (e) { + fromNext = undefined; + } + if (!fromNext) { - return; + return undefined; } - const req = fromNext.split(' ')[1]; - if ( - ctx.request && - (ctx.request.includes('@module-federation/utilities') || + + const req = + typeof fromNext === 'string' ? fromNext.split(' ')[1] : undefined; + if (!req) { + return undefined; + } + + const shouldBundleFederation = + request && + (request.includes('@module-federation/') || Object.keys(options.shared || {}).some((key) => { const sharedOptions = options.shared as Record< string, { import: boolean } >; - return ( - sharedOptions[key]?.import !== false && - (key.endsWith('/') ? req.includes(key) : req === key) - ); - }) || - ctx.request.includes('@module-federation/')) - ) { - return; - } + const match = key.endsWith('/') ? req.includes(key) : req === key; + return sharedOptions[key]?.import !== false && match; + })); - if ( + const shouldExternalizeCore = req.startsWith('next') || req.startsWith('react/') || req.startsWith('react-dom/') || req === 'react' || req === 'styled-jsx/style' || - req === 'react-dom' - ) { + req === 'react-dom'; + + if (shouldExternalizeCore) { return fromNext; } - return; + + if (shouldBundleFederation) { + return undefined; + } + + return undefined; }; } } diff --git a/packages/nextjs-mf/src/plugins/NextFederationPlugin/index.ts b/packages/nextjs-mf/src/plugins/NextFederationPlugin/index.ts index 416476409b3..c1665a0c0de 100644 --- a/packages/nextjs-mf/src/plugins/NextFederationPlugin/index.ts +++ b/packages/nextjs-mf/src/plugins/NextFederationPlugin/index.ts @@ -13,7 +13,9 @@ import type { Compiler, WebpackPluginInstance } from 'webpack'; import { getWebpackPath } from '@module-federation/sdk/normalize-webpack-path'; import CopyFederationPlugin from '../CopyFederationPlugin'; import { exposeNextjsPages } from '../../loaders/nextPageMapLoader'; -import { retrieveDefaultShared, applyPathFixes } from './next-fragments'; +import { getShareScope } from '../../internal'; +import { getNextInternalsShareScopeClient } from '../../share-internals-client'; +import { getNextInternalsShareScopeServer } from '../../share-internals-server'; import { setOptions } from './set-options'; import { validateCompilerOptions, @@ -28,8 +30,11 @@ import { import { applyClientPlugins } from './apply-client-plugins'; import { ModuleFederationPlugin } from '@module-federation/enhanced/webpack'; import type { moduleFederationPlugin } from '@module-federation/sdk'; +import RscManifestInterceptPlugin from './RscManifestInterceptPlugin'; +import { applyPathFixes } from './next-fragments'; import path from 'path'; +import { WEBPACK_LAYERS_NAMES } from '../../constants'; /** * NextFederationPlugin is a webpack plugin that handles Next.js application federation using Module Federation. */ @@ -37,6 +42,9 @@ export class NextFederationPlugin { private _options: moduleFederationPlugin.ModuleFederationPluginOptions; private _extraOptions: NextFederationPluginExtraOptions; public name: string; + // Store the original public path for use by other plugins + public static originalPublicPath = ''; + /** * Constructs the NextFederationPlugin with the provided options. * @@ -59,16 +67,27 @@ export class NextFederationPlugin { getWebpackPath(compiler, { framework: 'nextjs' }); if (!this.validateOptions(compiler)) return; const isServer = this.isServerCompiler(compiler); + + // Capture the original public path before any modifications + const publicPath = compiler.options.output.publicPath; + NextFederationPlugin.originalPublicPath = + typeof publicPath === 'string' ? publicPath : ''; + new CopyFederationPlugin(isServer).apply(compiler); const normalFederationPluginOptions = this.getNormalFederationPluginOptions( compiler, isServer, ); + this._options = normalFederationPluginOptions; this.applyConditionalPlugins(compiler, isServer); new ModuleFederationPlugin(normalFederationPluginOptions).apply(compiler); + // Apply the RSC Manifest Intercept Plugin after the main ModuleFederationPlugin + // This ensures it runs on assets potentially modified or generated by MF processes + new RscManifestInterceptPlugin().apply(compiler); + const noop = this.getNoopPath(); if (!this._extraOptions.skipSharingNextInternals) { @@ -108,14 +127,14 @@ export class NextFederationPlugin { p?.constructor?.name === 'BuildManifestPlugin', ); - if (manifestPlugin) { - //@ts-ignore - if (manifestPlugin?.appDirEnabled) { - throw new Error( - 'App Directory is not supported by nextjs-mf. Use only pages directory, do not open git issues about this', - ); - } - } + // if (manifestPlugin) { + // //@ts-ignore + // if (manifestPlugin?.appDirEnabled) { + // throw new Error( + // 'App Directory is not supported by nextjs-mf. Use only pages directory, do not open git issues about this', + // ); + // } + // } const compilerValid = validateCompilerOptions(compiler); const pluginValid = validatePluginOptions(this._options); @@ -179,7 +198,10 @@ export class NextFederationPlugin { applyServerPlugins(compiler, this._options); handleServerExternals(compiler, { ...this._options, - shared: { ...retrieveDefaultShared(isServer), ...this._options.shared }, + shared: { + ...getNextInternalsShareScopeServer(compiler), + ...this._options.shared, + }, }); } else { applyClientPlugins(compiler, this._options, this._extraOptions); @@ -192,7 +214,9 @@ export class NextFederationPlugin { ): moduleFederationPlugin.ModuleFederationPluginOptions { const defaultShared = this._extraOptions.skipSharingNextInternals ? {} - : retrieveDefaultShared(isServer); + : compiler.options.name === 'client' + ? getNextInternalsShareScopeClient(compiler) + : getNextInternalsShareScopeServer(compiler); return { ...this._options, @@ -215,6 +239,10 @@ export class NextFederationPlugin { remotes: { ...this._options.remotes, }, + shareScope: Object.values({ + ...WEBPACK_LAYERS_NAMES, + default: 'default', + }), shared: { ...defaultShared, ...this._options.shared, diff --git a/packages/nextjs-mf/src/plugins/NextFederationPlugin/next-fragments.ts b/packages/nextjs-mf/src/plugins/NextFederationPlugin/next-fragments.ts index 7327edba53e..7afe52aac5b 100644 --- a/packages/nextjs-mf/src/plugins/NextFederationPlugin/next-fragments.ts +++ b/packages/nextjs-mf/src/plugins/NextFederationPlugin/next-fragments.ts @@ -3,32 +3,13 @@ import type { moduleFederationPlugin, sharePlugin, } from '@module-federation/sdk'; -import { - DEFAULT_SHARE_SCOPE, - DEFAULT_SHARE_SCOPE_BROWSER, -} from '../../internal'; import { hasLoader, injectRuleLoader, findLoaderForResource, } from '../../loaders/helpers'; import path from 'path'; -/** - * Set up default shared values based on the environment. - * @param {boolean} isServer - Boolean indicating if the code is running on the server. - * @returns {SharedObject} The default share scope based on the environment. - */ -export const retrieveDefaultShared = ( - isServer: boolean, -): moduleFederationPlugin.SharedObject => { - // If the code is running on the server, treat some Next.js internals as import false to make them external - // This is because they will be provided by the server environment and not by the remote container - if (isServer) { - return DEFAULT_SHARE_SCOPE; - } - // If the code is running on the client/browser, always bundle Next.js internals - return DEFAULT_SHARE_SCOPE_BROWSER; -}; + export const applyPathFixes = ( compiler: Compiler, pluginOptions: moduleFederationPlugin.ModuleFederationPluginOptions, diff --git a/packages/nextjs-mf/src/plugins/NextFederationPlugin/remove-unnecessary-shared-keys.test.ts b/packages/nextjs-mf/src/plugins/NextFederationPlugin/remove-unnecessary-shared-keys.test.ts index cf398032679..e873ab7c0d6 100644 --- a/packages/nextjs-mf/src/plugins/NextFederationPlugin/remove-unnecessary-shared-keys.test.ts +++ b/packages/nextjs-mf/src/plugins/NextFederationPlugin/remove-unnecessary-shared-keys.test.ts @@ -1,4 +1,14 @@ import { removeUnnecessarySharedKeys } from './remove-unnecessary-shared-keys'; +import type { Compiler } from 'webpack'; + +// Basic mock compiler +const mockCompiler = { + options: { + name: 'server', + // Add minimal resolve structure to prevent crash in writeCompilerResolveConfig + resolve: { alias: {} }, + }, +} as Compiler; describe('removeUnnecessarySharedKeys', () => { beforeEach(() => { @@ -16,7 +26,7 @@ describe('removeUnnecessarySharedKeys', () => { lodash: '4.17.21', }; - removeUnnecessarySharedKeys(shared); + removeUnnecessarySharedKeys(shared, mockCompiler); expect(shared).toEqual({ lodash: '4.17.21' }); expect(console.warn).toHaveBeenCalled(); @@ -28,18 +38,20 @@ describe('removeUnnecessarySharedKeys', () => { axios: '0.21.1', }; - removeUnnecessarySharedKeys(shared); + (console.warn as jest.Mock).mockClear(); + + removeUnnecessarySharedKeys(shared, mockCompiler); expect(shared).toEqual({ lodash: '4.17.21', axios: '0.21.1' }); - expect(console.warn).not.toHaveBeenCalled(); }); it('should not remove keys from an empty object', () => { const shared: Record = {}; - removeUnnecessarySharedKeys(shared); + (console.warn as jest.Mock).mockClear(); + + removeUnnecessarySharedKeys(shared, mockCompiler); expect(shared).toEqual({}); - expect(console.warn).not.toHaveBeenCalled(); }); }); diff --git a/packages/nextjs-mf/src/plugins/NextFederationPlugin/remove-unnecessary-shared-keys.ts b/packages/nextjs-mf/src/plugins/NextFederationPlugin/remove-unnecessary-shared-keys.ts index bb5f5522ee2..dce72400a6d 100644 --- a/packages/nextjs-mf/src/plugins/NextFederationPlugin/remove-unnecessary-shared-keys.ts +++ b/packages/nextjs-mf/src/plugins/NextFederationPlugin/remove-unnecessary-shared-keys.ts @@ -5,23 +5,29 @@ * * @param {Record} shared - The shared object to be checked. */ -import { DEFAULT_SHARE_SCOPE } from '../../internal'; +import type { Compiler } from 'webpack'; +import { getShareScope } from '../../internal'; /** * Function to remove unnecessary shared keys from the default share scope. - * It iterates over each key in the shared object and checks against the default share scope. - * If a key is found in the default share scope, a warning is logged and the key is removed from the shared object. + * It iterates over each key in the shared object and checks against the default share scope + * generated based on the compiler context. + * If a key is found in the default share scope, a warning is logged and the key is removed. * * @param {Record} shared - The shared object to be checked. + * @param {Compiler} compiler - The webpack compiler instance. */ export function removeUnnecessarySharedKeys( shared: Record, + compiler: Compiler, ): void { + const defaultScope = getShareScope(compiler); + Object.keys(shared).forEach((key: string) => { /** * If the key is found in the default share scope, log a warning and remove the key from the shared object. */ - if (DEFAULT_SHARE_SCOPE[key]) { + if (defaultScope[key]) { console.warn( `%c[nextjs-mf] You are sharing ${key} from the default share scope. This is not necessary and can be removed.`, 'color: red', diff --git a/packages/nextjs-mf/src/plugins/container/runtimePlugin.ts b/packages/nextjs-mf/src/plugins/container/runtimePlugin.ts index 558dbcc0bb1..1386a526a20 100644 --- a/packages/nextjs-mf/src/plugins/container/runtimePlugin.ts +++ b/packages/nextjs-mf/src/plugins/container/runtimePlugin.ts @@ -217,6 +217,7 @@ export default function (): FederationRuntimePlugin { return args; }, resolveShare: function (args: any) { + console.log('Resolving share for package:', args.pkgName); if ( args.pkgName !== 'react' && args.pkgName !== 'react-dom' && @@ -231,17 +232,18 @@ export default function (): FederationRuntimePlugin { const GlobalFederation = args.GlobalFederation; const host = GlobalFederation['__INSTANCES__'][0]; if (!host) { + console.log('No host instance found'); return args; } if (!host.options.shared[pkgName]) { return args; } - args.resolver = function () { - shareScopeMap[scope][pkgName][version] = - host.options.shared[pkgName][0]; - return shareScopeMap[scope][pkgName][version]; - }; + // args.resolver = function () { + // shareScopeMap[scope][pkgName][version] = + // host.options.shared[pkgName][0]; + // return shareScopeMap[scope][pkgName][version]; + // }; return args; }, beforeLoadShare: async function (args: any) { diff --git a/packages/nextjs-mf/src/share-internals-client.ts b/packages/nextjs-mf/src/share-internals-client.ts new file mode 100644 index 00000000000..10531a0137c --- /dev/null +++ b/packages/nextjs-mf/src/share-internals-client.ts @@ -0,0 +1,652 @@ +import type { + moduleFederationPlugin, + sharePlugin, +} from '@module-federation/sdk'; +import type { Compiler, RuleSetRule, Configuration } from 'webpack'; +import { + WEBPACK_LAYERS as WL, + type WebpackLayerName, + WEBPACK_LAYERS_NAMES, +} from './constants'; +import { safeRequireResolve, getReactVersionSafely } from './internal-helpers'; + +// Extend the SharedConfig type to include layer properties +export type ExtendedSharedConfig = sharePlugin.SharedConfig & { + layer?: string; + issuerLayer?: string | string[]; + request?: string; + shareKey?: string; +}; + +/** + * Extracts aliases from webpack rules + */ +const extractRuleAliases = (rules: Configuration['module']['rules']): any[] => { + const collectedAliases: any[] = []; + + const traverse = (rule: RuleSetRule) => { + if (!rule || typeof rule !== 'object') return; + + const ruleInfo: any = { + conditions: {}, + resolve: {}, + }; + + let hasResolveConfig = false; + + // Collect all rule conditions + if (rule.test) ruleInfo.conditions.test = rule.test.toString(); + if (rule.include) ruleInfo.conditions.include = rule.include; + if (rule.exclude) ruleInfo.conditions.exclude = rule.exclude; + if (rule.issuer) ruleInfo.conditions.issuer = rule.issuer; + if (rule.issuerLayer) { + ruleInfo.conditions.issuerLayer = rule.issuerLayer; + } + if (rule.layer) { + ruleInfo.conditions.layer = rule.layer; + } + if (rule.resourceQuery) { + ruleInfo.conditions.resourceQuery = rule.resourceQuery.toString(); + } + + // Collect resolve configuration + if (rule.resolve) { + if (rule.resolve.alias) { + ruleInfo.resolve.alias = rule.resolve.alias; + hasResolveConfig = true; + } + if (rule.resolve.fallback) { + ruleInfo.resolve.fallback = rule.resolve.fallback; + hasResolveConfig = true; + } + if (rule.resolve.mainFields) { + ruleInfo.resolve.mainFields = rule.resolve.mainFields; + hasResolveConfig = true; + } + if (rule.resolve.conditionNames) { + ruleInfo.resolve.conditionNames = rule.resolve.conditionNames; + hasResolveConfig = true; + } + } + + if (hasResolveConfig) { + collectedAliases.push(ruleInfo); + } + + // Traverse nested rules + if ('oneOf' in rule && Array.isArray(rule.oneOf)) { + rule.oneOf.forEach((r) => { + if (isRuleSetRule(r)) { + traverse(r); + } + }); + } + if ('rules' in rule && Array.isArray(rule.rules)) { + rule.rules.forEach((r) => { + if (isRuleSetRule(r)) { + traverse(r); + } + }); + } + }; + + if (rules) { + rules.forEach((rule: unknown) => { + if (isRuleSetRule(rule)) { + traverse(rule); + } + }); + } + return collectedAliases; +}; + +// Type guard to check if a value is a RuleSetRule +export const isRuleSetRule = (rule: unknown): rule is RuleSetRule => { + if (rule === null || rule === undefined) return false; + if (typeof rule !== 'object') return false; + return true; +}; + +/** + * Function defining the React related packages group for client side + */ +export const getReactGroupClient = ( + compiler: Compiler, +): Record => { + const aliases = { + ssr: 'next/dist/server/route-modules/app-page/vendored/ssr/react.js', + rsc: 'next/dist/server/route-modules/app-page/vendored/rsc/react.js', + browser: 'next/dist/compiled/react', + original: 'react', + }; + + const reactVersion = getReactVersionSafely(aliases.browser, compiler.context); + + // Client-side configuration + return { + 'react-original': { + request: aliases.original, + singleton: true, + shareScope: 'default', + shareKey: 'react', + }, + // Direct import of the browser alias path + 'react-direct': { + request: aliases.browser, + singleton: true, + layer: WEBPACK_LAYERS_NAMES.appPagesBrowser, + issuerLayer: WEBPACK_LAYERS_NAMES.appPagesBrowser, + shareScope: WEBPACK_LAYERS_NAMES.appPagesBrowser, + import: + safeRequireResolve(aliases.browser, { paths: [compiler.context] }) || + false, + version: reactVersion, + shareKey: 'react', + }, + // User requests 'react' + 'react-user': { + request: 'react', + singleton: true, + layer: WEBPACK_LAYERS_NAMES.appPagesBrowser, + issuerLayer: WEBPACK_LAYERS_NAMES.appPagesBrowser, + shareScope: WEBPACK_LAYERS_NAMES.appPagesBrowser, + import: + safeRequireResolve(aliases.browser, { paths: [compiler.context] }) || + false, + version: reactVersion, + shareKey: 'react', + }, + // SSR layer - direct import + 'react-ssr-direct': { + request: aliases.ssr, + singleton: true, + layer: WEBPACK_LAYERS_NAMES.serverSideRendering, + issuerLayer: WEBPACK_LAYERS_NAMES.serverSideRendering, + shareScope: WEBPACK_LAYERS_NAMES.serverSideRendering, + import: + safeRequireResolve(aliases.ssr, { paths: [compiler.context] }) || false, + version: reactVersion, + shareKey: 'react', + }, + // SSR layer - user request + 'react-ssr-user': { + request: 'react', + singleton: true, + layer: WEBPACK_LAYERS_NAMES.serverSideRendering, + issuerLayer: WEBPACK_LAYERS_NAMES.serverSideRendering, + shareScope: WEBPACK_LAYERS_NAMES.serverSideRendering, + import: + safeRequireResolve(aliases.ssr, { paths: [compiler.context] }) || false, + version: reactVersion, + shareKey: 'react', + }, + // RSC layer - direct import + 'react-rsc-direct': { + request: aliases.rsc, + singleton: true, + layer: WEBPACK_LAYERS_NAMES.reactServerComponents, + issuerLayer: WEBPACK_LAYERS_NAMES.reactServerComponents, + shareScope: WEBPACK_LAYERS_NAMES.reactServerComponents, + import: + safeRequireResolve(aliases.rsc, { paths: [compiler.context] }) || false, + version: reactVersion, + shareKey: 'react', + }, + // RSC layer - user request + 'react-rsc-user': { + request: 'react', + singleton: true, + layer: WEBPACK_LAYERS_NAMES.reactServerComponents, + issuerLayer: WEBPACK_LAYERS_NAMES.reactServerComponents, + shareScope: WEBPACK_LAYERS_NAMES.reactServerComponents, + import: + safeRequireResolve(aliases.rsc, { paths: [compiler.context] }) || false, + version: reactVersion, + shareKey: 'react', + }, + }; +}; + +/** + * Function defining the React-JSX-Runtime related packages group for client side + */ +export const getReactJsxRuntimeGroupClient = ( + compiler: Compiler, +): Record => { + const aliases = { + ssr: 'next/dist/server/route-modules/app-page/vendored/ssr/react-jsx-runtime.js', + rsc: 'next/dist/server/route-modules/app-page/vendored/rsc/react-jsx-runtime.js', + browser: 'next/dist/compiled/react/jsx-runtime', + original: 'react/jsx-runtime', + }; + + // Use React's version since jsx-runtime is part of React + const reactVersion = getReactVersionSafely( + 'next/dist/compiled/react', + compiler.context, + ); + + // Client-side configuration + return { + 'react/jsx-runtime-original': { + request: aliases.original, + singleton: true, + shareScope: 'default', + shareKey: 'react/jsx-runtime', + }, + // Direct import of the browser alias path + 'react/jsx-runtime-direct': { + request: aliases.browser, + singleton: true, + layer: WEBPACK_LAYERS_NAMES.appPagesBrowser, + issuerLayer: WEBPACK_LAYERS_NAMES.appPagesBrowser, + shareScope: WEBPACK_LAYERS_NAMES.appPagesBrowser, + import: + safeRequireResolve(aliases.browser, { paths: [compiler.context] }) || + false, + version: reactVersion, + shareKey: 'react/jsx-runtime', + }, + // User requests 'react/jsx-runtime' + 'react/jsx-runtime-user': { + request: 'react/jsx-runtime', + singleton: true, + layer: WEBPACK_LAYERS_NAMES.appPagesBrowser, + issuerLayer: WEBPACK_LAYERS_NAMES.appPagesBrowser, + shareScope: WEBPACK_LAYERS_NAMES.appPagesBrowser, + import: + safeRequireResolve(aliases.browser, { paths: [compiler.context] }) || + false, + version: reactVersion, + shareKey: 'react/jsx-runtime', + }, + // SSR layer - direct import + 'react/jsx-runtime-ssr-direct': { + request: aliases.ssr, + singleton: true, + layer: WEBPACK_LAYERS_NAMES.serverSideRendering, + issuerLayer: WEBPACK_LAYERS_NAMES.serverSideRendering, + shareScope: WEBPACK_LAYERS_NAMES.serverSideRendering, + import: + safeRequireResolve(aliases.ssr, { paths: [compiler.context] }) || false, + version: reactVersion, + shareKey: 'react/jsx-runtime', + }, + // SSR layer - user request + 'react/jsx-runtime-ssr-user': { + request: 'react/jsx-runtime', + singleton: true, + layer: WEBPACK_LAYERS_NAMES.serverSideRendering, + issuerLayer: WEBPACK_LAYERS_NAMES.serverSideRendering, + shareScope: WEBPACK_LAYERS_NAMES.serverSideRendering, + import: + safeRequireResolve(aliases.ssr, { paths: [compiler.context] }) || false, + version: reactVersion, + shareKey: 'react/jsx-runtime', + }, + // RSC layer - direct import + 'react/jsx-runtime-rsc-direct': { + request: aliases.rsc, + singleton: true, + layer: WEBPACK_LAYERS_NAMES.reactServerComponents, + issuerLayer: WEBPACK_LAYERS_NAMES.reactServerComponents, + shareScope: WEBPACK_LAYERS_NAMES.reactServerComponents, + import: + safeRequireResolve(aliases.rsc, { paths: [compiler.context] }) || false, + version: reactVersion, + shareKey: 'react/jsx-runtime', + }, + // RSC layer - user request + 'react/jsx-runtime-rsc-user': { + request: 'react/jsx-runtime', + singleton: true, + layer: WEBPACK_LAYERS_NAMES.reactServerComponents, + issuerLayer: WEBPACK_LAYERS_NAMES.reactServerComponents, + shareScope: WEBPACK_LAYERS_NAMES.reactServerComponents, + import: + safeRequireResolve(aliases.rsc, { paths: [compiler.context] }) || false, + version: reactVersion, + shareKey: 'react/jsx-runtime', + }, + }; +}; + +/** + * Function defining the React-DOM related packages group for client side + */ +export const getReactDomGroupClient = ( + compiler: Compiler, +): Record => { + const aliases = { + ssr: 'next/dist/server/route-modules/app-page/vendored/ssr/react-dom.js', + rsc: 'next/dist/server/route-modules/app-page/vendored/rsc/react-dom.js', + browser: 'next/dist/compiled/react-dom', + original: 'react-dom', + }; + + const reactDomVersion = getReactVersionSafely( + aliases.browser, + compiler.context, + ); + + // Client-side configuration + return { + 'react-dom-original': { + request: aliases.original, + singleton: true, + shareScope: 'default', + shareKey: 'react-dom', + }, + // Direct import of the browser alias path + 'react-dom-direct': { + request: aliases.browser, + singleton: true, + layer: WEBPACK_LAYERS_NAMES.appPagesBrowser, + issuerLayer: WEBPACK_LAYERS_NAMES.appPagesBrowser, + shareScope: WEBPACK_LAYERS_NAMES.appPagesBrowser, + import: + safeRequireResolve(aliases.browser, { paths: [compiler.context] }) || + false, + version: reactDomVersion, + shareKey: 'react-dom', + }, + // User requests 'react-dom' + 'react-dom-user': { + request: 'react-dom', + singleton: true, + layer: WEBPACK_LAYERS_NAMES.appPagesBrowser, + issuerLayer: WEBPACK_LAYERS_NAMES.appPagesBrowser, + shareScope: WEBPACK_LAYERS_NAMES.appPagesBrowser, + import: + safeRequireResolve(aliases.browser, { paths: [compiler.context] }) || + false, + version: reactDomVersion, + shareKey: 'react-dom', + }, + // SSR layer - direct import + 'react-dom-ssr-direct': { + request: aliases.ssr, + singleton: true, + layer: WEBPACK_LAYERS_NAMES.serverSideRendering, + issuerLayer: WEBPACK_LAYERS_NAMES.serverSideRendering, + shareScope: WEBPACK_LAYERS_NAMES.serverSideRendering, + import: + safeRequireResolve(aliases.ssr, { paths: [compiler.context] }) || false, + version: reactDomVersion, + shareKey: 'react-dom', + }, + // SSR layer - user request + 'react-dom-ssr-user': { + request: 'react-dom', + singleton: true, + layer: WEBPACK_LAYERS_NAMES.serverSideRendering, + issuerLayer: WEBPACK_LAYERS_NAMES.serverSideRendering, + shareScope: WEBPACK_LAYERS_NAMES.serverSideRendering, + import: + safeRequireResolve(aliases.ssr, { paths: [compiler.context] }) || false, + version: reactDomVersion, + shareKey: 'react-dom', + }, + // RSC layer - direct import + 'react-dom-rsc-direct': { + request: aliases.rsc, + singleton: true, + layer: WEBPACK_LAYERS_NAMES.reactServerComponents, + issuerLayer: WEBPACK_LAYERS_NAMES.reactServerComponents, + shareScope: WEBPACK_LAYERS_NAMES.reactServerComponents, + import: + safeRequireResolve(aliases.rsc, { paths: [compiler.context] }) || false, + version: reactDomVersion, + shareKey: 'react-dom', + }, + // RSC layer - user request + 'react-dom-rsc-user': { + request: 'react-dom', + singleton: true, + layer: WEBPACK_LAYERS_NAMES.reactServerComponents, + issuerLayer: WEBPACK_LAYERS_NAMES.reactServerComponents, + shareScope: WEBPACK_LAYERS_NAMES.reactServerComponents, + import: + safeRequireResolve(aliases.rsc, { paths: [compiler.context] }) || false, + version: reactDomVersion, + shareKey: 'react-dom', + }, + }; +}; + +/** + * Function defining the React-DOM/Client related packages group for client side + */ +export const getReactDomClientGroupClient = ( + compiler: Compiler, +): Record => { + const aliases = { + ssr: 'next/dist/server/route-modules/app-page/vendored/ssr/react-dom-client.js', + rsc: 'next/dist/server/route-modules/app-page/vendored/rsc/react-dom-client.js', + browser: 'next/dist/compiled/react-dom/client', + original: 'react-dom/client', + }; + + const reactDomVersion = getReactVersionSafely( + aliases.browser, + compiler.context, + ); + + // Client-side configuration + return { + 'react-dom/client-original': { + request: aliases.original, + singleton: true, + shareScope: 'default', + shareKey: 'react-dom/client', + }, + // Direct import of the browser alias path + 'react-dom/client-direct': { + request: aliases.browser, + singleton: true, + layer: WEBPACK_LAYERS_NAMES.appPagesBrowser, + issuerLayer: WEBPACK_LAYERS_NAMES.appPagesBrowser, + shareScope: WEBPACK_LAYERS_NAMES.appPagesBrowser, + import: + safeRequireResolve(aliases.browser, { paths: [compiler.context] }) || + false, + version: reactDomVersion, + shareKey: 'react-dom/client', + }, + // User requests 'react-dom/client' + 'react-dom/client-user': { + request: 'react-dom/client', + singleton: true, + layer: WEBPACK_LAYERS_NAMES.appPagesBrowser, + issuerLayer: WEBPACK_LAYERS_NAMES.appPagesBrowser, + shareScope: WEBPACK_LAYERS_NAMES.appPagesBrowser, + import: + safeRequireResolve(aliases.browser, { paths: [compiler.context] }) || + false, + version: reactDomVersion, + shareKey: 'react-dom/client', + }, + // SSR layer - direct import + 'react-dom/client-ssr-direct': { + request: aliases.ssr, + singleton: true, + layer: WEBPACK_LAYERS_NAMES.serverSideRendering, + issuerLayer: WEBPACK_LAYERS_NAMES.serverSideRendering, + shareScope: WEBPACK_LAYERS_NAMES.serverSideRendering, + import: + safeRequireResolve(aliases.ssr, { paths: [compiler.context] }) || false, + version: reactDomVersion, + shareKey: 'react-dom/client', + }, + // SSR layer - user request + 'react-dom/client-ssr-user': { + request: 'react-dom/client', + singleton: true, + layer: WEBPACK_LAYERS_NAMES.serverSideRendering, + issuerLayer: WEBPACK_LAYERS_NAMES.serverSideRendering, + shareScope: WEBPACK_LAYERS_NAMES.serverSideRendering, + import: + safeRequireResolve(aliases.ssr, { paths: [compiler.context] }) || false, + version: reactDomVersion, + shareKey: 'react-dom/client', + }, + // RSC layer - direct import + 'react-dom/client-rsc-direct': { + request: aliases.rsc, + singleton: true, + layer: WEBPACK_LAYERS_NAMES.reactServerComponents, + issuerLayer: WEBPACK_LAYERS_NAMES.reactServerComponents, + shareScope: WEBPACK_LAYERS_NAMES.reactServerComponents, + import: + safeRequireResolve(aliases.rsc, { paths: [compiler.context] }) || false, + version: reactDomVersion, + shareKey: 'react-dom/client', + }, + // RSC layer - user request + 'react-dom/client-rsc-user': { + request: 'react-dom/client', + singleton: true, + layer: WEBPACK_LAYERS_NAMES.reactServerComponents, + issuerLayer: WEBPACK_LAYERS_NAMES.reactServerComponents, + shareScope: WEBPACK_LAYERS_NAMES.reactServerComponents, + import: + safeRequireResolve(aliases.rsc, { paths: [compiler.context] }) || false, + version: reactDomVersion, + shareKey: 'react-dom/client', + }, + }; +}; + +/** + * Function defining the React-JSX-Dev-Runtime related packages group for client side + */ +export const getReactJsxDevRuntimeGroupClient = ( + compiler: Compiler, +): Record => { + const aliases = { + ssr: 'next/dist/server/route-modules/app-page/vendored/ssr/react-jsx-dev-runtime.js', + rsc: 'next/dist/server/route-modules/app-page/vendored/rsc/react-jsx-dev-runtime.js', + browser: 'next/dist/compiled/react/jsx-dev-runtime', + original: 'react/jsx-dev-runtime', + }; + + // Use React's version since jsx-dev-runtime is part of React + const reactVersion = getReactVersionSafely( + 'next/dist/compiled/react', + compiler.context, + ); + + // Client-side configuration + return { + 'react/jsx-dev-runtime-original': { + request: aliases.original, + singleton: true, + shareScope: 'default', + shareKey: 'react/jsx-dev-runtime', + }, + // Direct import of the browser alias path + 'react/jsx-dev-runtime-direct': { + request: aliases.browser, + singleton: true, + layer: WEBPACK_LAYERS_NAMES.appPagesBrowser, + issuerLayer: WEBPACK_LAYERS_NAMES.appPagesBrowser, + shareScope: WEBPACK_LAYERS_NAMES.appPagesBrowser, + import: + safeRequireResolve(aliases.browser, { paths: [compiler.context] }) || + false, + version: reactVersion, + shareKey: 'react/jsx-dev-runtime', + }, + // User requests 'react/jsx-dev-runtime' + 'react/jsx-dev-runtime-user': { + request: 'react/jsx-dev-runtime', + singleton: true, + layer: WEBPACK_LAYERS_NAMES.appPagesBrowser, + issuerLayer: WEBPACK_LAYERS_NAMES.appPagesBrowser, + shareScope: WEBPACK_LAYERS_NAMES.appPagesBrowser, + import: + safeRequireResolve(aliases.browser, { paths: [compiler.context] }) || + false, + version: reactVersion, + shareKey: 'react/jsx-dev-runtime', + }, + // SSR layer - direct import + 'react/jsx-dev-runtime-ssr-direct': { + request: aliases.ssr, + singleton: true, + layer: WEBPACK_LAYERS_NAMES.serverSideRendering, + issuerLayer: WEBPACK_LAYERS_NAMES.serverSideRendering, + shareScope: WEBPACK_LAYERS_NAMES.serverSideRendering, + import: + safeRequireResolve(aliases.ssr, { paths: [compiler.context] }) || false, + version: reactVersion, + shareKey: 'react/jsx-dev-runtime', + }, + // SSR layer - user request + 'react/jsx-dev-runtime-ssr-user': { + request: 'react/jsx-dev-runtime', + singleton: true, + layer: WEBPACK_LAYERS_NAMES.serverSideRendering, + issuerLayer: WEBPACK_LAYERS_NAMES.serverSideRendering, + shareScope: WEBPACK_LAYERS_NAMES.serverSideRendering, + import: + safeRequireResolve(aliases.ssr, { paths: [compiler.context] }) || false, + version: reactVersion, + shareKey: 'react/jsx-dev-runtime', + }, + // RSC layer - direct import + 'react/jsx-dev-runtime-rsc-direct': { + request: aliases.rsc, + singleton: true, + layer: WEBPACK_LAYERS_NAMES.reactServerComponents, + issuerLayer: WEBPACK_LAYERS_NAMES.reactServerComponents, + shareScope: WEBPACK_LAYERS_NAMES.reactServerComponents, + import: + safeRequireResolve(aliases.rsc, { paths: [compiler.context] }) || false, + version: reactVersion, + shareKey: 'react/jsx-dev-runtime', + }, + // RSC layer - user request + 'react/jsx-dev-runtime-rsc-user': { + request: 'react/jsx-dev-runtime', + singleton: true, + layer: WEBPACK_LAYERS_NAMES.reactServerComponents, + issuerLayer: WEBPACK_LAYERS_NAMES.reactServerComponents, + shareScope: WEBPACK_LAYERS_NAMES.reactServerComponents, + import: + safeRequireResolve(aliases.rsc, { paths: [compiler.context] }) || false, + version: reactVersion, + shareKey: 'react/jsx-dev-runtime', + }, + }; +}; + +/** + * Generates the appropriate share scope for Next.js internals based on the compiler context. + * @param {Compiler} compiler - The webpack compiler instance. + * @returns {moduleFederationPlugin.SharedObject} - The generated share scope. + */ +export const getNextInternalsShareScopeClient = ( + compiler: Compiler, +): moduleFederationPlugin.SharedObject => { + // Only proceed if this is a client compiler + if (compiler.options.name !== 'client') { + return {}; + } + + // Generate the base groups + const reactGroup = getReactGroupClient(compiler); + const reactDomGroup = getReactDomGroupClient(compiler); + const reactDomClientGroup = getReactDomClientGroupClient(compiler); + const reactJsxDevRuntimeGroup = getReactJsxDevRuntimeGroupClient(compiler); + const reactJsxRuntimeGroup = getReactJsxRuntimeGroupClient(compiler); + + // Combine all groups + return { + ...reactGroup, + ...reactDomGroup, + ...reactDomClientGroup, + ...reactJsxDevRuntimeGroup, + ...reactJsxRuntimeGroup, + }; +}; diff --git a/packages/nextjs-mf/src/share-internals-server.ts b/packages/nextjs-mf/src/share-internals-server.ts new file mode 100644 index 00000000000..0280eed5ce1 --- /dev/null +++ b/packages/nextjs-mf/src/share-internals-server.ts @@ -0,0 +1,480 @@ +import type { + moduleFederationPlugin, + sharePlugin, +} from '@module-federation/sdk'; +import type { Compiler } from 'webpack'; +import { + WEBPACK_LAYERS as WL, + type WebpackLayerName, + WEBPACK_LAYERS_NAMES, +} from './constants'; +import { safeRequireResolve, getReactVersionSafely } from './internal-helpers'; + +// Extend the SharedConfig type to include layer properties +export type ExtendedSharedConfig = sharePlugin.SharedConfig & { + layer?: string; + issuerLayer?: string | string[]; + request?: string; + shareKey?: string; +}; + +/** + * Gets the appropriate React alias based on the layer + */ +const getReactAliasForLayer = (layer: WebpackLayerName): string => { + switch (layer) { + case WL.reactServerComponents: + return 'next/dist/server/route-modules/app-page/vendored/rsc/react'; + case WL.serverSideRendering: + return 'next/dist/server/route-modules/app-page/vendored/ssr/react'; + case WL.appPagesBrowser: + return 'next/dist/compiled/react'; + default: + return 'next/dist/server/route-modules/app-page/vendored/rsc/react'; + } +}; + +/** + * Gets the appropriate React DOM alias based on the layer + */ +const getReactDomAliasForLayer = (layer: WebpackLayerName): string => { + switch (layer) { + case WL.reactServerComponents: + return 'next/dist/server/route-modules/app-page/vendored/rsc/react-dom'; + case WL.serverSideRendering: + return 'next/dist/server/route-modules/app-page/vendored/ssr/react-dom'; + case WL.appPagesBrowser: + return 'next/dist/compiled/react-dom'; + default: + return 'next/dist/server/route-modules/app-page/vendored/rsc/react-dom'; + } +}; + +/** + * Gets the appropriate React JSX Runtime alias based on the layer + */ +const getReactJsxRuntimeAliasForLayer = (layer: WebpackLayerName): string => { + switch (layer) { + case WL.reactServerComponents: + return 'next/dist/server/route-modules/app-page/vendored/rsc/react-jsx-runtime'; + case WL.serverSideRendering: + return 'next/dist/server/route-modules/app-page/vendored/ssr/react-jsx-runtime'; + case WL.appPagesBrowser: + return 'next/dist/compiled/react/jsx-runtime'; + default: + return 'next/dist/server/route-modules/app-page/vendored/rsc/react-jsx-runtime'; + } +}; + +/** + * Gets the appropriate React JSX Dev Runtime alias based on the layer + */ +const getReactJsxDevRuntimeAliasForLayer = ( + layer: WebpackLayerName, +): string => { + switch (layer) { + case WL.reactServerComponents: + return 'next/dist/server/route-modules/app-page/vendored/rsc/react-jsx-dev-runtime'; + case WL.serverSideRendering: + return 'next/dist/server/route-modules/app-page/vendored/ssr/react-jsx-dev-runtime'; + case WL.appPagesBrowser: + return 'next/dist/compiled/react/jsx-dev-runtime'; + default: + return 'next/dist/server/route-modules/app-page/vendored/rsc/react-jsx-dev-runtime'; + } +}; + +/** + * Gets the appropriate React Server DOM Webpack alias based on the layer + */ +const getReactServerDomWebpackAliasForLayer = ( + layer: WebpackLayerName, +): { request: string } => { + switch (layer) { + case WL.reactServerComponents: + return { + request: + 'next/dist/server/route-modules/app-page/vendored/rsc/react-server-dom-webpack-server-edge', + }; + case WL.serverSideRendering: + return { + request: + 'next/dist/server/route-modules/app-page/vendored/ssr/react-server-dom-webpack-client-edge', + }; + default: + return { + request: 'next/dist/compiled/react-server-dom-webpack/server.edge', + }; + } +}; + +/** + * Function defining the React related packages group for server side + */ +export const getReactGroupServer = ( + compiler: Compiler, +): Record => { + const rscAlias = getReactAliasForLayer(WL.reactServerComponents); + const ssrAlias = getReactAliasForLayer(WL.serverSideRendering); + + const reactVersion = getReactVersionSafely(rscAlias, compiler.context); + + return { + // RSC layer entries + 'react-rsc': { + request: rscAlias, + singleton: true, + layer: WEBPACK_LAYERS_NAMES.reactServerComponents, + issuerLayer: WEBPACK_LAYERS_NAMES.reactServerComponents, + shareScope: WEBPACK_LAYERS_NAMES.reactServerComponents, + import: + safeRequireResolve(rscAlias, { paths: [compiler.context] }) || false, + version: reactVersion, + shareKey: 'react', + }, + 'react-rsc-user': { + request: 'react', + singleton: true, + layer: WEBPACK_LAYERS_NAMES.reactServerComponents, + issuerLayer: WEBPACK_LAYERS_NAMES.reactServerComponents, + shareScope: WEBPACK_LAYERS_NAMES.reactServerComponents, + import: + safeRequireResolve(rscAlias, { paths: [compiler.context] }) || false, + version: reactVersion, + shareKey: 'react', + }, + // SSR layer entries + 'react-ssr': { + request: ssrAlias, + singleton: true, + layer: WEBPACK_LAYERS_NAMES.serverSideRendering, + issuerLayer: WEBPACK_LAYERS_NAMES.serverSideRendering, + shareScope: WEBPACK_LAYERS_NAMES.serverSideRendering, + import: + safeRequireResolve(ssrAlias, { paths: [compiler.context] }) || false, + version: reactVersion, + shareKey: 'react', + }, + 'react-ssr-user': { + request: 'react', + singleton: true, + layer: WEBPACK_LAYERS_NAMES.serverSideRendering, + issuerLayer: WEBPACK_LAYERS_NAMES.serverSideRendering, + shareScope: WEBPACK_LAYERS_NAMES.serverSideRendering, + import: + safeRequireResolve(ssrAlias, { paths: [compiler.context] }) || false, + version: reactVersion, + shareKey: 'react', + }, + }; +}; + +/** + * Function defining the React-DOM related packages group for server side + */ +export const getReactDomGroupServer = ( + compiler: Compiler, +): Record => { + const rscAlias = getReactDomAliasForLayer(WL.reactServerComponents); + const ssrAlias = getReactDomAliasForLayer(WL.serverSideRendering); + + const reactDomVersion = getReactVersionSafely(rscAlias, compiler.context); + + return { + // RSC layer entries + 'react-dom-rsc': { + request: rscAlias, + singleton: true, + layer: WEBPACK_LAYERS_NAMES.reactServerComponents, + issuerLayer: WEBPACK_LAYERS_NAMES.reactServerComponents, + shareScope: WEBPACK_LAYERS_NAMES.reactServerComponents, + import: + safeRequireResolve(rscAlias, { paths: [compiler.context] }) || false, + version: reactDomVersion, + shareKey: 'react-dom', + }, + 'react-dom-rsc-user': { + request: 'react-dom', + singleton: true, + layer: WEBPACK_LAYERS_NAMES.reactServerComponents, + issuerLayer: WEBPACK_LAYERS_NAMES.reactServerComponents, + shareScope: WEBPACK_LAYERS_NAMES.reactServerComponents, + import: + safeRequireResolve(rscAlias, { paths: [compiler.context] }) || false, + version: reactDomVersion, + shareKey: 'react-dom', + }, + // SSR layer entries + 'react-dom-ssr': { + request: ssrAlias, + singleton: true, + layer: WEBPACK_LAYERS_NAMES.serverSideRendering, + issuerLayer: WEBPACK_LAYERS_NAMES.serverSideRendering, + shareScope: WEBPACK_LAYERS_NAMES.serverSideRendering, + import: + safeRequireResolve(ssrAlias, { paths: [compiler.context] }) || false, + version: reactDomVersion, + shareKey: 'react-dom', + }, + 'react-dom-ssr-user': { + request: 'react-dom', + singleton: true, + layer: WEBPACK_LAYERS_NAMES.serverSideRendering, + issuerLayer: WEBPACK_LAYERS_NAMES.serverSideRendering, + shareScope: WEBPACK_LAYERS_NAMES.serverSideRendering, + import: + safeRequireResolve(ssrAlias, { paths: [compiler.context] }) || false, + version: reactDomVersion, + shareKey: 'react-dom', + }, + // Server-specific entries + 'react-dom/server': { + request: 'next/dist/compiled/react-dom/server', + singleton: true, + layer: WEBPACK_LAYERS_NAMES.serverSideRendering, + shareScope: WEBPACK_LAYERS_NAMES.serverSideRendering, + import: + safeRequireResolve('next/dist/compiled/react-dom/server', { + paths: [compiler.context], + }) || false, + version: reactDomVersion, + shareKey: 'react-dom/server', + }, + 'react-dom/server.edge': { + request: 'next/dist/build/webpack/alias/react-dom-server-edge.js', + singleton: true, + layer: WEBPACK_LAYERS_NAMES.serverSideRendering, + shareScope: WEBPACK_LAYERS_NAMES.serverSideRendering, + import: + safeRequireResolve( + 'next/dist/build/webpack/alias/react-dom-server-edge.js', + { paths: [compiler.context] }, + ) || false, + version: reactDomVersion, + shareKey: 'react-dom/server.edge', + }, + }; +}; + +/** + * Function defining the React-Server-DOM-Webpack related packages group for server side + */ +export const getReactServerDomWebpackGroupServer = ( + compiler: Compiler, +): Record => { + const rscConfig = getReactServerDomWebpackAliasForLayer( + WL.reactServerComponents, + ); + const ssrConfig = getReactServerDomWebpackAliasForLayer( + WL.serverSideRendering, + ); + + const reactVersion = getReactVersionSafely( + 'next/dist/compiled/react-server-dom-webpack/server.edge', + compiler.context, + ); + + return { + 'react-server-dom-webpack/server.edge': { + request: rscConfig.request, + singleton: true, + layer: WEBPACK_LAYERS_NAMES.reactServerComponents, + shareScope: WEBPACK_LAYERS_NAMES.reactServerComponents, + import: + safeRequireResolve(rscConfig.request, { + paths: [compiler.context], + }) || false, + version: reactVersion, + shareKey: 'react-server-dom-webpack/server.edge', + }, + 'react-server-dom-webpack/server.node': { + request: + 'next/dist/server/route-modules/app-page/vendored/rsc/react-server-dom-webpack-server-node', + singleton: true, + layer: WEBPACK_LAYERS_NAMES.reactServerComponents, + shareScope: WEBPACK_LAYERS_NAMES.reactServerComponents, + import: + safeRequireResolve( + 'next/dist/server/route-modules/app-page/vendored/rsc/react-server-dom-webpack-server-node', + { + paths: [compiler.context], + }, + ) || false, + version: reactVersion, + shareKey: 'react-server-dom-webpack/server.node', + }, + 'react-server-dom-webpack/client.edge': { + request: ssrConfig.request, + singleton: true, + layer: WEBPACK_LAYERS_NAMES.serverSideRendering, + shareScope: WEBPACK_LAYERS_NAMES.serverSideRendering, + import: + safeRequireResolve(ssrConfig.request, { + paths: [compiler.context], + }) || false, + version: reactVersion, + shareKey: 'react-server-dom-webpack/client.edge', + }, + }; +}; + +/** + * Function defining the React-JSX-Runtime related packages group for server side + */ +export const getReactJsxRuntimeGroupServer = ( + compiler: Compiler, +): Record => { + const rscAlias = getReactJsxRuntimeAliasForLayer(WL.reactServerComponents); + const ssrAlias = getReactJsxRuntimeAliasForLayer(WL.serverSideRendering); + + // Use React's version since jsx-runtime is part of React + const reactVersion = getReactVersionSafely( + 'next/dist/compiled/react', + compiler.context, + ); + + return { + // RSC layer entries + 'react/jsx-runtime-rsc': { + request: rscAlias, + singleton: true, + layer: WEBPACK_LAYERS_NAMES.reactServerComponents, + issuerLayer: WEBPACK_LAYERS_NAMES.reactServerComponents, + shareScope: WEBPACK_LAYERS_NAMES.reactServerComponents, + import: + safeRequireResolve(rscAlias, { paths: [compiler.context] }) || false, + version: reactVersion, + shareKey: 'react/jsx-runtime', + }, + 'react/jsx-runtime-rsc-user': { + request: 'react/jsx-runtime', + singleton: true, + layer: WEBPACK_LAYERS_NAMES.reactServerComponents, + issuerLayer: WEBPACK_LAYERS_NAMES.reactServerComponents, + shareScope: WEBPACK_LAYERS_NAMES.reactServerComponents, + import: + safeRequireResolve(rscAlias, { paths: [compiler.context] }) || false, + version: reactVersion, + shareKey: 'react/jsx-runtime', + }, + // SSR layer entries + 'react/jsx-runtime-ssr': { + request: ssrAlias, + singleton: true, + layer: WEBPACK_LAYERS_NAMES.serverSideRendering, + issuerLayer: WEBPACK_LAYERS_NAMES.serverSideRendering, + shareScope: WEBPACK_LAYERS_NAMES.serverSideRendering, + import: + safeRequireResolve(ssrAlias, { paths: [compiler.context] }) || false, + version: reactVersion, + shareKey: 'react/jsx-runtime', + }, + 'react/jsx-runtime-ssr-user': { + request: 'react/jsx-runtime', + singleton: true, + layer: WEBPACK_LAYERS_NAMES.serverSideRendering, + issuerLayer: WEBPACK_LAYERS_NAMES.serverSideRendering, + shareScope: WEBPACK_LAYERS_NAMES.serverSideRendering, + import: + safeRequireResolve(ssrAlias, { paths: [compiler.context] }) || false, + version: reactVersion, + shareKey: 'react/jsx-runtime', + }, + }; +}; + +/** + * Function defining the React-JSX-Dev-Runtime related packages group for server side + */ +export const getReactJsxDevRuntimeGroupServer = ( + compiler: Compiler, +): Record => { + const rscAlias = getReactJsxDevRuntimeAliasForLayer(WL.reactServerComponents); + const ssrAlias = getReactJsxDevRuntimeAliasForLayer(WL.serverSideRendering); + + // Use React's version since jsx-dev-runtime is part of React + const reactVersion = getReactVersionSafely( + 'next/dist/compiled/react', + compiler.context, + ); + + return { + // RSC layer entries + 'react/jsx-dev-runtime-rsc': { + request: rscAlias, + singleton: true, + layer: WEBPACK_LAYERS_NAMES.reactServerComponents, + issuerLayer: WEBPACK_LAYERS_NAMES.reactServerComponents, + shareScope: WEBPACK_LAYERS_NAMES.reactServerComponents, + import: + safeRequireResolve(rscAlias, { paths: [compiler.context] }) || false, + version: reactVersion, + shareKey: 'react/jsx-dev-runtime', + }, + 'react/jsx-dev-runtime-rsc-user': { + request: 'react/jsx-dev-runtime', + singleton: true, + layer: WEBPACK_LAYERS_NAMES.reactServerComponents, + issuerLayer: WEBPACK_LAYERS_NAMES.reactServerComponents, + shareScope: WEBPACK_LAYERS_NAMES.reactServerComponents, + import: + safeRequireResolve(rscAlias, { paths: [compiler.context] }) || false, + version: reactVersion, + shareKey: 'react/jsx-dev-runtime', + }, + // SSR layer entries + 'react/jsx-dev-runtime-ssr': { + request: ssrAlias, + singleton: true, + layer: WEBPACK_LAYERS_NAMES.serverSideRendering, + issuerLayer: WEBPACK_LAYERS_NAMES.serverSideRendering, + shareScope: WEBPACK_LAYERS_NAMES.serverSideRendering, + import: + safeRequireResolve(ssrAlias, { paths: [compiler.context] }) || false, + version: reactVersion, + shareKey: 'react/jsx-dev-runtime', + }, + 'react/jsx-dev-runtime-ssr-user': { + request: 'react/jsx-dev-runtime', + singleton: true, + layer: WEBPACK_LAYERS_NAMES.serverSideRendering, + issuerLayer: WEBPACK_LAYERS_NAMES.serverSideRendering, + shareScope: WEBPACK_LAYERS_NAMES.serverSideRendering, + import: + safeRequireResolve(ssrAlias, { paths: [compiler.context] }) || false, + version: reactVersion, + shareKey: 'react/jsx-dev-runtime', + }, + }; +}; + +/** + * Generates the appropriate share scope for Next.js internals based on the server compiler context. + * @param {Compiler} compiler - The webpack compiler instance. + * @returns {moduleFederationPlugin.SharedObject} - The generated share scope. + */ +export const getNextInternalsShareScopeServer = ( + compiler: Compiler, +): moduleFederationPlugin.SharedObject => { + // Only proceed if this is a server compiler + if (compiler.options.name !== 'server') { + return {}; + } + + // Generate all the server-side sharing groups + const reactGroup = getReactGroupServer(compiler); + const reactDomGroup = getReactDomGroupServer(compiler); + const reactServerDomWebpackGroup = + getReactServerDomWebpackGroupServer(compiler); + const reactJsxRuntimeGroup = getReactJsxRuntimeGroupServer(compiler); + const reactJsxDevRuntimeGroup = getReactJsxDevRuntimeGroupServer(compiler); + + // Combine all groups + return { + ...reactGroup, + ...reactDomGroup, + ...reactServerDomWebpackGroup, + ...reactJsxRuntimeGroup, + ...reactJsxDevRuntimeGroup, + }; +}; diff --git a/packages/runtime-core/__tests__/semver.spec.ts b/packages/runtime-core/__tests__/semver.spec.ts index 8bd41e475df..5c5515187a9 100644 --- a/packages/runtime-core/__tests__/semver.spec.ts +++ b/packages/runtime-core/__tests__/semver.spec.ts @@ -226,3 +226,34 @@ describe('pre-release', () => { expect(satisfy('4.0.0-alpha.58', '^4.0.0-beta.57')).toBe(false); }); }); + +describe('OR ranges (Unsupported)', () => { + test('should pass with || support', () => { + const version = '19.0.0-rc-cd22717c-20241013'; + const range = '^18.2.0 || 19.0.0-rc-cd22717c-20241013'; + // This should now return true as the second part of the OR matches the version. + expect(satisfy(version, range)).toBe(true); + }); + + test('should pass if first part matches', () => { + const version = '18.5.0'; + const range = '^18.2.0 || 19.0.0'; + // This should pass as the first part matches. + expect(satisfy(version, range)).toBe(true); + }); + + test('should fail if neither part matches', () => { + const version = '17.0.0'; + const range = '^18.2.0 || 19.0.0'; + expect(satisfy(version, range)).toBe(false); + }); + + test('should handle complex OR parts', () => { + const version = '1.2.4'; + // Range expands to: (>=1.2.3 <1.3.0) || (>=1.3.1 <1.4.0) + const range = '~1.2.3 || ~1.3.1'; + expect(satisfy(version, range)).toBe(true); // Matches first part + expect(satisfy('1.3.2', range)).toBe(true); // Matches second part + expect(satisfy('1.3.0', range)).toBe(false); // Matches neither + }); +}); diff --git a/packages/runtime-core/src/utils/semver/index.ts b/packages/runtime-core/src/utils/semver/index.ts index 6a93200ebfe..f974cfa45ae 100644 --- a/packages/runtime-core/src/utils/semver/index.ts +++ b/packages/runtime-core/src/utils/semver/index.ts @@ -69,20 +69,12 @@ export function satisfy(version: string, range: string): boolean { return false; } - const parsedRange = parseRange(range); - const parsedComparator = parsedRange - .split(' ') - .map((rangeVersion) => parseComparatorString(rangeVersion)) - .join(' '); - const comparators = parsedComparator - .split(/\s+/) - .map((comparator) => parseGTE0(comparator)); + // Extract version details once const extractedVersion = extractComparator(version); - if (!extractedVersion) { + // If the version string is invalid, it can't satisfy any range return false; } - const [ , versionOperator, @@ -106,42 +98,113 @@ export function satisfy(version: string, range: string): boolean { preRelease: versionPreRelease?.split('.'), }; - for (const comparator of comparators) { - const extractedComparator = extractComparator(comparator); + // Split the range by || to handle OR conditions + const orRanges = range.split('||'); - if (!extractedComparator) { - return false; + for (const orRange of orRanges) { + const trimmedOrRange = orRange.trim(); + if (!trimmedOrRange) { + // An empty range string signifies wildcard *, satisfy any valid version + // (We already checked if the version itself is valid) + return true; } - const [ - , - rangeOperator, - , - rangeMajor, - rangeMinor, - rangePatch, - rangePreRelease, - ] = extractedComparator; - const rangeAtom: CompareAtom = { - operator: rangeOperator, - version: combineVersion( - rangeMajor, - rangeMinor, - rangePatch, - rangePreRelease, - ), // exclude build atom - major: rangeMajor, - minor: rangeMinor, - patch: rangePatch, - preRelease: rangePreRelease?.split('.'), - }; - - if (!compare(rangeAtom, versionAtom)) { - return false; // early return + // Handle simple wildcards explicitly before complex parsing + if (trimmedOrRange === '*' || trimmedOrRange === 'x') { + return true; + } + + try { + // Apply existing parsing logic to the current OR sub-range + const parsedSubRange = parseRange(trimmedOrRange); // Handles hyphens, trims etc. + + // Check if the result of initial parsing is empty, which can happen + // for some wildcard cases handled by parseRange/parseComparatorString. + // E.g. `parseStar` used in `parseComparatorString` returns ''. + if (!parsedSubRange.trim()) { + // If parsing results in empty string, treat as wildcard match + return true; + } + + const parsedComparatorString = parsedSubRange + .split(' ') + .map((rangeVersion) => parseComparatorString(rangeVersion)) // Expands ^, ~ + .join(' '); + + // Check again if the comparator string became empty after specific parsing like ^ or ~ + if (!parsedComparatorString.trim()) { + return true; + } + + // Split the sub-range by space for implicit AND conditions + const comparators = parsedComparatorString + .split(/\s+/) + .map((comparator) => parseGTE0(comparator)) + // Filter out empty strings that might result from multiple spaces + .filter(Boolean); + + // If a sub-range becomes empty after parsing (e.g., invalid characters), + // it cannot be satisfied. This check might be redundant now but kept for safety. + if (comparators.length === 0) { + continue; + } + + let subRangeSatisfied = true; + for (const comparator of comparators) { + const extractedComparator = extractComparator(comparator); + + // If any part of the AND sub-range is invalid, the sub-range is not satisfied + if (!extractedComparator) { + subRangeSatisfied = false; + break; + } + + const [ + , + rangeOperator, + , + rangeMajor, + rangeMinor, + rangePatch, + rangePreRelease, + ] = extractedComparator; + const rangeAtom: CompareAtom = { + operator: rangeOperator, + version: combineVersion( + rangeMajor, + rangeMinor, + rangePatch, + rangePreRelease, + ), + major: rangeMajor, + minor: rangeMinor, + patch: rangePatch, + preRelease: rangePreRelease?.split('.'), + }; + + // Check if the version satisfies this specific comparator in the AND chain + if (!compare(rangeAtom, versionAtom)) { + subRangeSatisfied = false; // This part of the AND condition failed + break; // No need to check further comparators in this sub-range + } + } + + // If all AND conditions within this OR sub-range were met, the overall range is satisfied + if (subRangeSatisfied) { + return true; + } + } catch (e) { + // Log error and treat this sub-range as unsatisfied + console.error( + `[semver] Error processing range part "${trimmedOrRange}":`, + e, + ); + continue; } } - return true; + // If none of the OR sub-ranges were satisfied + return false; } export function isLegallyVersion(version: string): boolean {