Skip to content

Commit

Permalink
feat(react): Support loading remotes via library: var
Browse files Browse the repository at this point in the history
  • Loading branch information
ndcunningham committed Sep 18, 2023
1 parent 11fcb8f commit 794e193
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 18 deletions.
80 changes: 80 additions & 0 deletions e2e/react-core/src/react-module-federation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,86 @@ 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`,
`
import { getGreeting } from '../support/app.po';
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;
Expand Down
26 changes: 19 additions & 7 deletions packages/react/src/module-federation/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
12 changes: 12 additions & 0 deletions packages/react/src/module-federation/with-module-federation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand All @@ -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()
);
Expand Down
59 changes: 48 additions & 11 deletions packages/webpack/src/utils/module-federation/remotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> {
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.
Expand Down

0 comments on commit 794e193

Please sign in to comment.