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);
|
}, 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 {
|
function readPort(appName: string): number {
|
||||||
const config = readJson(join('apps', appName, 'project.json'));
|
const config = readJson(join('apps', appName, 'project.json'));
|
||||||
return config.targets.serve.options.port;
|
return config.targets.serve.options.port;
|
||||||
|
|||||||
@ -95,14 +95,26 @@ export async function getModuleFederationConfig(
|
|||||||
projectGraph
|
projectGraph
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Choose the correct mapRemotes function based on the server state.
|
||||||
const mapRemotesFunction = options.isServer ? mapRemotesForSSR : mapRemotes;
|
const mapRemotesFunction = options.isServer ? mapRemotesForSSR : mapRemotes;
|
||||||
const determineRemoteUrlFn =
|
|
||||||
options.determineRemoteUrl ||
|
// Determine the URL function, either from provided options or by using a default.
|
||||||
getFunctionDeterminateRemoteUrl(options.isServer);
|
const determineRemoteUrlFunction = options.determineRemoteUrl
|
||||||
const mappedRemotes =
|
? options.determineRemoteUrl
|
||||||
!mfConfig.remotes || mfConfig.remotes.length === 0
|
: getFunctionDeterminateRemoteUrl(options.isServer);
|
||||||
? {}
|
|
||||||
: mapRemotesFunction(mfConfig.remotes, 'js', determineRemoteUrlFn);
|
// 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 };
|
return { sharedLibraries, sharedDependencies, mappedRemotes };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,11 @@ export async function withModuleFederation(
|
|||||||
config.output.uniqueName = options.name;
|
config.output.uniqueName = options.name;
|
||||||
config.output.publicPath = 'auto';
|
config.output.publicPath = 'auto';
|
||||||
|
|
||||||
|
if (options.library?.type === 'var') {
|
||||||
|
config.output.scriptType = 'text/javascript';
|
||||||
|
config.experiments.outputModule = false;
|
||||||
|
}
|
||||||
|
|
||||||
config.optimization = {
|
config.optimization = {
|
||||||
runtimeChunk: false,
|
runtimeChunk: false,
|
||||||
};
|
};
|
||||||
@ -36,6 +41,13 @@ export async function withModuleFederation(
|
|||||||
shared: {
|
shared: {
|
||||||
...sharedDependencies,
|
...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()
|
sharedLibraries.getReplacementPlugin()
|
||||||
);
|
);
|
||||||
|
|||||||
@ -12,29 +12,66 @@ import { extname } from 'path';
|
|||||||
export function mapRemotes(
|
export function mapRemotes(
|
||||||
remotes: Remotes,
|
remotes: Remotes,
|
||||||
remoteEntryExt: 'js' | 'mjs',
|
remoteEntryExt: 'js' | 'mjs',
|
||||||
determineRemoteUrl: (remote: string) => string
|
determineRemoteUrl: (remote: string) => string,
|
||||||
|
isRemoteGlobal = false
|
||||||
): Record<string, string> {
|
): Record<string, string> {
|
||||||
const mappedRemotes = {};
|
const mappedRemotes = {};
|
||||||
|
|
||||||
for (const remote of remotes) {
|
for (const remote of remotes) {
|
||||||
if (Array.isArray(remote)) {
|
if (Array.isArray(remote)) {
|
||||||
const [remoteName, remoteLocation] = remote;
|
mappedRemotes[remote[0]] = handleArrayRemote(
|
||||||
const remoteLocationExt = extname(remoteLocation);
|
remote,
|
||||||
mappedRemotes[remoteName] = ['.js', '.mjs'].includes(remoteLocationExt)
|
remoteEntryExt,
|
||||||
? remoteLocation
|
isRemoteGlobal
|
||||||
: `${
|
);
|
||||||
remoteLocation.endsWith('/')
|
|
||||||
? remoteLocation.slice(0, -1)
|
|
||||||
: remoteLocation
|
|
||||||
}/remoteEntry.${remoteEntryExt}`;
|
|
||||||
} else if (typeof remote === 'string') {
|
} else if (typeof remote === 'string') {
|
||||||
mappedRemotes[remote] = determineRemoteUrl(remote);
|
mappedRemotes[remote] = handleStringRemote(
|
||||||
|
remote,
|
||||||
|
determineRemoteUrl,
|
||||||
|
isRemoteGlobal
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return mappedRemotes;
|
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
|
* Map remote names to a format that can be understood and used by Module
|
||||||
* Federation.
|
* Federation.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user