feat(react): Support loading remotes via library: var (#19192)
This commit is contained in:
parent
b5ac50a773
commit
0369224b1c
@ -282,6 +282,98 @@ 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} --remotes=${remote} --project-name-and-root-format=as-provided --no-interactive`
|
||||
);
|
||||
|
||||
// update host and remote to use library type var
|
||||
updateFile(
|
||||
`${shell}/module-federation.config.ts`,
|
||||
stripIndents`
|
||||
import { ModuleFederationConfig } from '@nx/webpack';
|
||||
|
||||
const config: ModuleFederationConfig = {
|
||||
name: '${shell}',
|
||||
library: { type: 'var', name: '${shell}' },
|
||||
remotes: ['${remote}'],
|
||||
};
|
||||
|
||||
export default config;
|
||||
`
|
||||
);
|
||||
|
||||
updateFile(
|
||||
`${shell}/webpack.config.prod.ts`,
|
||||
`export { default } from './webpack.config';`
|
||||
);
|
||||
|
||||
updateFile(
|
||||
`${remote}/module-federation.config.ts`,
|
||||
stripIndents`
|
||||
import { ModuleFederationConfig } from '@nx/webpack';
|
||||
|
||||
const config: ModuleFederationConfig = {
|
||||
name: '${remote}',
|
||||
library: { type: 'var', name: '${remote}' },
|
||||
exposes: {
|
||||
'./Module': './src/remote-entry.ts',
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
`
|
||||
);
|
||||
|
||||
updateFile(
|
||||
`${remote}/webpack.config.prod.ts`,
|
||||
`export { default } from './webpack.config';`
|
||||
);
|
||||
|
||||
// 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[0].toUpperCase()}${remote.slice(
|
||||
1
|
||||
)}').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;
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
@ -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()
|
||||
);
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user