diff --git a/e2e/react-core/src/react-module-federation.test.ts b/e2e/react-core/src/react-module-federation.test.ts index 600dd5473edd78..0027fec4be3cdf 100644 --- a/e2e/react-core/src/react-module-federation.test.ts +++ b/e2e/react-core/src/react-module-federation.test.ts @@ -282,6 +282,84 @@ describe('React Module Federation', () => { } }, 500_000); + it('should support host and remote with library type var', async () => { + const shell = uniq('shell'); + const remote = uniq('remote'); + + runCLI( + `generate @nx/react:host ${shell} --project-name-and-root-format=as-provided --no-interactive` + ); + runCLI( + `generate @nx/react:remote ${remote} --host=${shell} --project-name-and-root-format=as-provided --no-interactive` + ); + + // update host and remote to use library type var + updateFile( + `${shell}/module-federation.config.js`, + stripIndents` + module.exports = { + name: '${shell}', + library: { type: 'var', name: '${shell}' }, + remotes: ['${remote}'], + }; + ` + ); + + updateFile( + `${shell}/webpack.config.prod.js`, + `module.exports = require('./webpack.config');` + ); + + updateFile( + `${remote}/module-federation.config.js`, + stripIndents` + module.exports = { + name: '${remote}', + library: { type: 'var', name: '${remote}' }, + exposes: { + './Module': './src/remote-entry.ts', + } + } + ` + ); + + // Update host e2e test to check that the remote works with library type var via navigation + updateFile( + `${shell}-e2e/src/e2e/app.cy.ts`, + ` + describe('${shell}', () => { + beforeEach(() => cy.visit('/')); + + it('should display welcome message', () => { + getGreeting().contains('Welcome ${shell}'); + + }); + + it('should navigate to /about from /', () => { + cy.get('a').contains('${remote}').click(); + cy.url().should('include', '/${remote}'); + getGreeting().contains('Welcome ${remote}'); + }); + }); + ` + ); + + // Build host and remote + const buildOutput = runCLI(`build ${shell}`); + const remoteOutput = runCLI(`build ${remote}`); + + expect(buildOutput).toContain('Successfully ran target build'); + expect(remoteOutput).toContain('Successfully ran target build'); + + if (runE2ETests()) { + const hostE2eResults = runCLI(`e2e ${shell}-e2e --no-watch --verbose`); + const remoteE2eResults = runCLI(`e2e ${remote}-e2e --no-watch --verbose`); + + expect(hostE2eResults).toContain('All specs passed!'); + expect(remoteE2eResults).toContain('All specs passed!'); + } + }, 500_000); + function readPort(appName: string): number { const config = readJson(join('apps', appName, 'project.json')); return config.targets.serve.options.port; diff --git a/packages/react/src/module-federation/utils.ts b/packages/react/src/module-federation/utils.ts index 7775123b1dce6b..6be83708b62a5a 100644 --- a/packages/react/src/module-federation/utils.ts +++ b/packages/react/src/module-federation/utils.ts @@ -95,14 +95,26 @@ export async function getModuleFederationConfig( projectGraph ); + // Choose the correct mapRemotes function based on the server state. const mapRemotesFunction = options.isServer ? mapRemotesForSSR : mapRemotes; - const determineRemoteUrlFn = - options.determineRemoteUrl || - getFunctionDeterminateRemoteUrl(options.isServer); - const mappedRemotes = - !mfConfig.remotes || mfConfig.remotes.length === 0 - ? {} - : mapRemotesFunction(mfConfig.remotes, 'js', determineRemoteUrlFn); + + // Determine the URL function, either from provided options or by using a default. + const determineRemoteUrlFunction = options.determineRemoteUrl + ? options.determineRemoteUrl + : getFunctionDeterminateRemoteUrl(options.isServer); + + // Map the remotes if they exist, otherwise default to an empty object. + let mappedRemotes = {}; + + if (mfConfig.remotes && mfConfig.remotes.length > 0) { + const isLibraryTypeVar = mfConfig.library.type === 'var'; + mappedRemotes = mapRemotesFunction( + mfConfig.remotes, + 'js', + determineRemoteUrlFunction, + isLibraryTypeVar + ); + } return { sharedLibraries, sharedDependencies, mappedRemotes }; } diff --git a/packages/react/src/module-federation/with-module-federation.ts b/packages/react/src/module-federation/with-module-federation.ts index 8e30f947ed049c..c58b05d1220a1a 100644 --- a/packages/react/src/module-federation/with-module-federation.ts +++ b/packages/react/src/module-federation/with-module-federation.ts @@ -17,6 +17,11 @@ export async function withModuleFederation( config.output.uniqueName = options.name; config.output.publicPath = 'auto'; + if (options.library?.type === 'var') { + config.output.scriptType = 'text/javascript'; + config.experiments.outputModule = false; + } + config.optimization = { runtimeChunk: false, }; @@ -36,6 +41,13 @@ export async function withModuleFederation( shared: { ...sharedDependencies, }, + /** + * remoteType: 'script' is required for the remote to be loaded as a script tag. + * remotes will need to be defined as: + * { appX: 'appX@http://localhost:3001/remoteEntry.js' } + * { appY: 'appY@http://localhost:3002/remoteEntry.js' } + */ + ...(options.library?.type === 'var' ? { remoteType: 'script' } : {}), }), sharedLibraries.getReplacementPlugin() ); diff --git a/packages/webpack/src/utils/module-federation/remotes.ts b/packages/webpack/src/utils/module-federation/remotes.ts index 0f6a714f3b8efd..6aeef8ea5177c0 100644 --- a/packages/webpack/src/utils/module-federation/remotes.ts +++ b/packages/webpack/src/utils/module-federation/remotes.ts @@ -12,29 +12,66 @@ import { extname } from 'path'; export function mapRemotes( remotes: Remotes, remoteEntryExt: 'js' | 'mjs', - determineRemoteUrl: (remote: string) => string + determineRemoteUrl: (remote: string) => string, + isRemoteGlobal = false ): Record { const mappedRemotes = {}; for (const remote of remotes) { if (Array.isArray(remote)) { - const [remoteName, remoteLocation] = remote; - const remoteLocationExt = extname(remoteLocation); - mappedRemotes[remoteName] = ['.js', '.mjs'].includes(remoteLocationExt) - ? remoteLocation - : `${ - remoteLocation.endsWith('/') - ? remoteLocation.slice(0, -1) - : remoteLocation - }/remoteEntry.${remoteEntryExt}`; + mappedRemotes[remote[0]] = handleArrayRemote( + remote, + remoteEntryExt, + isRemoteGlobal + ); } else if (typeof remote === 'string') { - mappedRemotes[remote] = determineRemoteUrl(remote); + mappedRemotes[remote] = handleStringRemote( + remote, + determineRemoteUrl, + isRemoteGlobal + ); } } return mappedRemotes; } +// Helper function to deal with remotes that are arrays +function handleArrayRemote( + remote: [string, string], + remoteEntryExt: 'js' | 'mjs', + isRemoteGlobal: boolean +): string { + const [remoteName, remoteLocation] = remote; + const remoteLocationExt = extname(remoteLocation); + + // If remote location already has .js or .mjs extension + if (['.js', '.mjs'].includes(remoteLocationExt)) { + return remoteLocation; + } + + const baseRemote = remoteLocation.endsWith('/') + ? remoteLocation.slice(0, -1) + : remoteLocation; + + const globalPrefix = isRemoteGlobal + ? `${remoteName.replace(/-/g, '_')}@` + : ''; + + return `${globalPrefix}${baseRemote}/remoteEntry.${remoteEntryExt}`; +} + +// Helper function to deal with remotes that are strings +function handleStringRemote( + remote: string, + determineRemoteUrl: (remote: string) => string, + isRemoteGlobal: boolean +): string { + const globalPrefix = isRemoteGlobal ? `${remote.replace(/-/g, '_')}@` : ''; + + return `${globalPrefix}${determineRemoteUrl(remote)}`; +} + /** * Map remote names to a format that can be understood and used by Module * Federation.