feat(module-federation): Add react support for dynamic federation (#20024)
This commit is contained in:
parent
c7c845dbc4
commit
6475c41ec8
@ -68,6 +68,11 @@
|
|||||||
"enum": ["eslint"],
|
"enum": ["eslint"],
|
||||||
"default": "eslint"
|
"default": "eslint"
|
||||||
},
|
},
|
||||||
|
"dynamic": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Should the host application use dynamic federation?",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
"skipFormat": {
|
"skipFormat": {
|
||||||
"description": "Skip formatting files.",
|
"description": "Skip formatting files.",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
|
|||||||
@ -28,6 +28,12 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["as-provided", "derived"]
|
"enum": ["as-provided", "derived"]
|
||||||
},
|
},
|
||||||
|
"dynamic": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Should the host application use dynamic federation?",
|
||||||
|
"default": false,
|
||||||
|
"x-priority": "internal"
|
||||||
|
},
|
||||||
"style": {
|
"style": {
|
||||||
"description": "The file extension to be used for style files.",
|
"description": "The file extension to be used for style files.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Tree, stripIndents } from '@nx/devkit';
|
|||||||
import {
|
import {
|
||||||
checkFilesExist,
|
checkFilesExist,
|
||||||
cleanupProject,
|
cleanupProject,
|
||||||
|
fileExists,
|
||||||
killPorts,
|
killPorts,
|
||||||
killProcessAndPorts,
|
killProcessAndPorts,
|
||||||
newProject,
|
newProject,
|
||||||
@ -10,6 +11,7 @@ import {
|
|||||||
runCLIAsync,
|
runCLIAsync,
|
||||||
runCommandUntil,
|
runCommandUntil,
|
||||||
runE2ETests,
|
runE2ETests,
|
||||||
|
tmpProjPath,
|
||||||
uniq,
|
uniq,
|
||||||
updateFile,
|
updateFile,
|
||||||
updateJson,
|
updateJson,
|
||||||
@ -165,9 +167,11 @@ describe('React Module Federation', () => {
|
|||||||
`apps/${app}/module-federation.server.config.ts`
|
`apps/${app}/module-federation.server.config.ts`
|
||||||
);
|
);
|
||||||
['build', 'server'].forEach((target) => {
|
['build', 'server'].forEach((target) => {
|
||||||
['development', 'production'].forEach((configuration) => {
|
['development', 'production'].forEach(async (configuration) => {
|
||||||
const cliOutput = runCLI(`run ${app}:${target}:${configuration}`);
|
const cliOutput = runCLI(`run ${app}:${target}:${configuration}`);
|
||||||
expect(cliOutput).toContain('Successfully ran target');
|
expect(cliOutput).toContain('Successfully ran target');
|
||||||
|
|
||||||
|
await killPorts(readPort(app));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -866,6 +870,91 @@ describe('React Module Federation', () => {
|
|||||||
}
|
}
|
||||||
}, 500_000);
|
}, 500_000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Dynamic Module Federation', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
tree = createTreeWithEmptyWorkspace();
|
||||||
|
proj = newProject();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => cleanupProject());
|
||||||
|
it('should load remote dynamic module', async () => {
|
||||||
|
const shell = uniq('shell');
|
||||||
|
const remote = uniq('remote');
|
||||||
|
|
||||||
|
runCLI(
|
||||||
|
`generate @nx/react:host ${shell} --remotes=${remote} --dynamic=true --project-name-and-root-format=as-provided --no-interactive`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Webpack prod config should not exists when loading dynamic modules
|
||||||
|
expect(
|
||||||
|
fileExists(`${tmpProjPath()}/${shell}/webpack.config.prod.ts`)
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
fileExists(
|
||||||
|
`${tmpProjPath()}/${shell}/src/assets/module-federation.manifest.json`
|
||||||
|
)
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
|
const manifest = readJson(
|
||||||
|
`${shell}/src/assets/module-federation.manifest.json`
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manifest[remote]).toBeDefined();
|
||||||
|
expect(manifest[remote]).toEqual('http://localhost:4201');
|
||||||
|
|
||||||
|
// update e2e
|
||||||
|
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 /${remote} 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');
|
||||||
|
|
||||||
|
const shellPort = readPort(shell);
|
||||||
|
const remotePort = readPort(remote);
|
||||||
|
|
||||||
|
if (runE2ETests()) {
|
||||||
|
// Serve Remote since it is dynamic and won't be started with the host
|
||||||
|
const remoteProcess = await runCommandUntil(
|
||||||
|
`serve-static ${remote} --no-watch --verbose`,
|
||||||
|
() => {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const hostE2eResultsSwc = await runCommandUntil(
|
||||||
|
`e2e ${shell}-e2e --no-watch --verbose`,
|
||||||
|
(output) => output.includes('All specs passed!')
|
||||||
|
);
|
||||||
|
|
||||||
|
await killProcessAndPorts(remoteProcess.pid, remotePort);
|
||||||
|
await killProcessAndPorts(hostE2eResultsSwc.pid, shellPort);
|
||||||
|
}
|
||||||
|
}, 500_000);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function readPort(appName: string): number {
|
function readPort(appName: string): number {
|
||||||
|
|||||||
105
packages/react/mf/dynamic-federation.ts
Normal file
105
packages/react/mf/dynamic-federation.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
export type ResolveRemoteUrlFunction = (
|
||||||
|
remoteName: string
|
||||||
|
) => string | Promise<string>;
|
||||||
|
|
||||||
|
declare const window: {
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare const document: {
|
||||||
|
head: {
|
||||||
|
appendChild: (script: HTMLScriptElement) => void;
|
||||||
|
};
|
||||||
|
createElement: (type: 'script') => any;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare const __webpack_init_sharing__: (scope: 'default') => Promise<void>;
|
||||||
|
declare const __webpack_share_scopes__: { default: unknown };
|
||||||
|
let remoteUrlDefinitions: Record<string, string>;
|
||||||
|
let resolveRemoteUrl: ResolveRemoteUrlFunction;
|
||||||
|
const remoteModuleMap = new Map<string, unknown>();
|
||||||
|
const remoteContainerMap = new Map<string, unknown>();
|
||||||
|
let initialSharingScopeCreated = false;
|
||||||
|
|
||||||
|
export function setRemoteUrlResolver(
|
||||||
|
_resolveRemoteUrl: ResolveRemoteUrlFunction
|
||||||
|
) {
|
||||||
|
resolveRemoteUrl = _resolveRemoteUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setRemoteDefinitions(definitions: Record<string, string>) {
|
||||||
|
remoteUrlDefinitions = definitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadRemoteModule(remoteName: string, moduleName: string) {
|
||||||
|
const remoteModuleKey = `${remoteName}:${moduleName}`;
|
||||||
|
if (remoteModuleMap.has(remoteModuleKey)) {
|
||||||
|
return remoteModuleMap.get(remoteModuleKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = remoteContainerMap.has(remoteName)
|
||||||
|
? remoteContainerMap.get(remoteName)
|
||||||
|
: await loadRemoteContainer(remoteName);
|
||||||
|
|
||||||
|
const factory = await container.get(moduleName);
|
||||||
|
const Module = factory();
|
||||||
|
|
||||||
|
remoteModuleMap.set(remoteModuleKey, Module);
|
||||||
|
|
||||||
|
return Module;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchRemoteModule = (url: string, remoteName: string): Promise<any> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = url;
|
||||||
|
script.type = 'text/javascript';
|
||||||
|
script.async = true;
|
||||||
|
script.onload = () => {
|
||||||
|
const proxy = {
|
||||||
|
get: (request) => window[remoteName].get(request),
|
||||||
|
init: (arg) => {
|
||||||
|
try {
|
||||||
|
window[remoteName].init(arg);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to initialize remote ${remoteName}`, e);
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
resolve(proxy);
|
||||||
|
};
|
||||||
|
script.onerror = () => reject(new Error(`Remote ${remoteName} not found`));
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
async function loadRemoteContainer(remoteName: string) {
|
||||||
|
if (!resolveRemoteUrl && !remoteUrlDefinitions) {
|
||||||
|
throw new Error(
|
||||||
|
'Call setRemoteDefinitions or setRemoteUrlResolver to allow Dynamic Federation to find the remote apps correctly.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!initialSharingScopeCreated) {
|
||||||
|
initialSharingScopeCreated = true;
|
||||||
|
await __webpack_init_sharing__('default');
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteUrl = remoteUrlDefinitions
|
||||||
|
? remoteUrlDefinitions[remoteName]
|
||||||
|
: await resolveRemoteUrl(remoteName);
|
||||||
|
|
||||||
|
let containerUrl = remoteUrl;
|
||||||
|
if (!remoteUrl.endsWith('.mjs') && !remoteUrl.endsWith('.js')) {
|
||||||
|
containerUrl = `${remoteUrl}${
|
||||||
|
remoteUrl.endsWith('/') ? '' : '/'
|
||||||
|
}remoteEntry.js`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = await fetchRemoteModule(containerUrl, remoteName);
|
||||||
|
await container.init(__webpack_share_scopes__.default);
|
||||||
|
|
||||||
|
remoteContainerMap.set(remoteName, container);
|
||||||
|
return container;
|
||||||
|
}
|
||||||
5
packages/react/mf/index.ts
Normal file
5
packages/react/mf/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export {
|
||||||
|
loadRemoteModule,
|
||||||
|
setRemoteDefinitions,
|
||||||
|
setRemoteUrlResolver,
|
||||||
|
} from './dynamic-federation';
|
||||||
@ -3,12 +3,20 @@ import * as React from 'react';
|
|||||||
import NxWelcome from "./nx-welcome";
|
import NxWelcome from "./nx-welcome";
|
||||||
<% } %>
|
<% } %>
|
||||||
import { Link, Route, Routes } from 'react-router-dom';
|
import { Link, Route, Routes } from 'react-router-dom';
|
||||||
|
<% if (dynamic) { %>
|
||||||
|
import { loadRemoteModule } from '@nx/react/mf';
|
||||||
|
<% } %>
|
||||||
|
|
||||||
<% if (remotes.length > 0) { %>
|
<% if (remotes.length > 0) { %>
|
||||||
<% remotes.forEach(function(r) { %>
|
<% remotes.forEach(function(r) { %>
|
||||||
|
<% if (dynamic) { %>
|
||||||
|
const <%= r.className %> = React.lazy(() => loadRemoteModule('<%= r.fileName %>', './Module'))
|
||||||
|
<% } else { %>
|
||||||
const <%= r.className %> = React.lazy(() => import('<%= r.fileName %>/Module'));
|
const <%= r.className %> = React.lazy(() => import('<%= r.fileName %>/Module'));
|
||||||
|
<% } %>
|
||||||
<% }); %>
|
<% }); %>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
<React.Suspense fallback={null}>
|
<React.Suspense fallback={null}>
|
||||||
|
|||||||
@ -3,7 +3,10 @@ import { ModuleFederationConfig } from '@nx/webpack';
|
|||||||
const config: ModuleFederationConfig = {
|
const config: ModuleFederationConfig = {
|
||||||
name: '<%= projectName %>',
|
name: '<%= projectName %>',
|
||||||
remotes: [
|
remotes: [
|
||||||
<% remotes.forEach(function(r) {%> "<%= r.fileName %>", <% }); %>
|
<% if (static) {
|
||||||
|
remotes.forEach(function(r) { %> "<%= r.fileName %>", <% });
|
||||||
|
}
|
||||||
|
%>
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,10 @@
|
|||||||
const moduleFederationConfig = {
|
const moduleFederationConfig = {
|
||||||
name: '<%= projectName %>',
|
name: '<%= projectName %>',
|
||||||
remotes: [
|
remotes: [
|
||||||
<% remotes.forEach(function(r) {%> "<%= r.fileName %>", <% }); %>
|
<% if (static) {
|
||||||
|
remotes.forEach(function(r) { %> "<%= r.fileName %>", <% });
|
||||||
|
}
|
||||||
|
%>
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,10 @@ const config: ModuleFederationConfig = {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
remotes: [
|
remotes: [
|
||||||
<% remotes.forEach(function(r) {%> "<%= r.fileName %>", <% }); %>
|
<% if (static) {
|
||||||
|
remotes.forEach(function(r) { %> "<%= r.fileName %>", <% });
|
||||||
|
}
|
||||||
|
%>
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1 +1,10 @@
|
|||||||
import('./bootstrap');
|
<% if (dynamic) { %>
|
||||||
|
import { setRemoteDefinitions } from '@nx/react/mf';
|
||||||
|
|
||||||
|
fetch('/assets/module-federation.manifest.json')
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(definitions => setRemoteDefinitions(definitions))
|
||||||
|
.then(() => import('./bootstrap').catch(err => console.error(err)));
|
||||||
|
<% } else { %>
|
||||||
|
import('./bootstrap').catch(err => console.error(err));
|
||||||
|
<% } %>
|
||||||
@ -13,6 +13,9 @@ module.exports = {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
remotes: [
|
remotes: [
|
||||||
<% remotes.forEach(function(r) {%> "<%= r.fileName %>", <% }); %>
|
<% if (static) {
|
||||||
|
remotes.forEach(function(r) { %> "<%= r.fileName %>", <% });
|
||||||
|
}
|
||||||
|
%>
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@ -1 +1,10 @@
|
|||||||
import('./bootstrap');
|
<% if (dynamic) { %>
|
||||||
|
import { setRemoteDefinitions } from '@nx/react/mf';
|
||||||
|
|
||||||
|
fetch('/assets/module-federation.manifest.json')
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(definitions => setRemoteDefinitions(definitions))
|
||||||
|
.then(() => import('./bootstrap').catch(err => console.error(err)));
|
||||||
|
<% } else { %>
|
||||||
|
import('./bootstrap').catch(err => console.error(err));
|
||||||
|
<% } %>
|
||||||
@ -39,6 +39,7 @@ export async function hostGeneratorInternal(
|
|||||||
const options: NormalizedSchema = {
|
const options: NormalizedSchema = {
|
||||||
...(await normalizeOptions<Schema>(host, schema, '@nx/react:host')),
|
...(await normalizeOptions<Schema>(host, schema, '@nx/react:host')),
|
||||||
typescriptConfiguration: schema.typescriptConfiguration ?? true,
|
typescriptConfiguration: schema.typescriptConfiguration ?? true,
|
||||||
|
dynamic: schema.dynamic ?? false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const initTask = await applicationGenerator(host, {
|
const initTask = await applicationGenerator(host, {
|
||||||
@ -71,6 +72,8 @@ export async function hostGeneratorInternal(
|
|||||||
skipFormat: true,
|
skipFormat: true,
|
||||||
projectNameAndRootFormat: options.projectNameAndRootFormat,
|
projectNameAndRootFormat: options.projectNameAndRootFormat,
|
||||||
typescriptConfiguration: options.typescriptConfiguration,
|
typescriptConfiguration: options.typescriptConfiguration,
|
||||||
|
dynamic: options.dynamic,
|
||||||
|
host: options.name,
|
||||||
});
|
});
|
||||||
tasks.push(remoteTask);
|
tasks.push(remoteTask);
|
||||||
remotePort++;
|
remotePort++;
|
||||||
|
|||||||
@ -1,15 +1,21 @@
|
|||||||
import { NormalizedSchema } from '../schema';
|
import { NormalizedSchema } from '../schema';
|
||||||
import { generateFiles, joinPathFragments, names } from '@nx/devkit';
|
import {
|
||||||
import { join } from 'path';
|
Tree,
|
||||||
|
generateFiles,
|
||||||
|
joinPathFragments,
|
||||||
|
names,
|
||||||
|
readProjectConfiguration,
|
||||||
|
} from '@nx/devkit';
|
||||||
|
|
||||||
export function addModuleFederationFiles(
|
export function addModuleFederationFiles(
|
||||||
host,
|
host: Tree,
|
||||||
options: NormalizedSchema,
|
options: NormalizedSchema,
|
||||||
defaultRemoteManifest: { name: string; port: number }[]
|
defaultRemoteManifest: { name: string; port: number }[]
|
||||||
) {
|
) {
|
||||||
const templateVariables = {
|
const templateVariables = {
|
||||||
...names(options.name),
|
...names(options.name),
|
||||||
...options,
|
...options,
|
||||||
|
static: !options?.dynamic,
|
||||||
tmpl: '',
|
tmpl: '',
|
||||||
remotes: defaultRemoteManifest.map(({ name, port }) => {
|
remotes: defaultRemoteManifest.map(({ name, port }) => {
|
||||||
return {
|
return {
|
||||||
@ -19,17 +25,23 @@ export function addModuleFederationFiles(
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const projectConfig = readProjectConfiguration(host, options.name);
|
||||||
|
const pathToMFManifest = joinPathFragments(
|
||||||
|
projectConfig.sourceRoot,
|
||||||
|
'assets/module-federation.manifest.json'
|
||||||
|
);
|
||||||
|
|
||||||
// Module federation requires bootstrap code to be dynamically imported.
|
// Module federation requires bootstrap code to be dynamically imported.
|
||||||
// Renaming original entry file so we can use `import(./bootstrap)` in
|
// Renaming original entry file so we can use `import(./bootstrap)` in
|
||||||
// new entry file.
|
// new entry file.
|
||||||
host.rename(
|
host.rename(
|
||||||
join(options.appProjectRoot, 'src/main.tsx'),
|
joinPathFragments(options.appProjectRoot, 'src/main.tsx'),
|
||||||
join(options.appProjectRoot, 'src/bootstrap.tsx')
|
joinPathFragments(options.appProjectRoot, 'src/bootstrap.tsx')
|
||||||
);
|
);
|
||||||
|
|
||||||
generateFiles(
|
generateFiles(
|
||||||
host,
|
host,
|
||||||
join(__dirname, `../files/common`),
|
joinPathFragments(__dirname, `../files/common`),
|
||||||
options.appProjectRoot,
|
options.appProjectRoot,
|
||||||
templateVariables
|
templateVariables
|
||||||
);
|
);
|
||||||
@ -40,25 +52,35 @@ export function addModuleFederationFiles(
|
|||||||
// New entry file is created here.
|
// New entry file is created here.
|
||||||
generateFiles(
|
generateFiles(
|
||||||
host,
|
host,
|
||||||
join(__dirname, `../files/${pathToModuleFederationFiles}`),
|
joinPathFragments(__dirname, `../files/${pathToModuleFederationFiles}`),
|
||||||
options.appProjectRoot,
|
options.appProjectRoot,
|
||||||
templateVariables
|
templateVariables
|
||||||
);
|
);
|
||||||
|
|
||||||
if (options.typescriptConfiguration) {
|
function deleteFileIfExists(host, filePath) {
|
||||||
|
if (host.exists(filePath)) {
|
||||||
|
host.delete(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processWebpackConfig(options, host, fileName) {
|
||||||
const pathToWebpackConfig = joinPathFragments(
|
const pathToWebpackConfig = joinPathFragments(
|
||||||
options.appProjectRoot,
|
options.appProjectRoot,
|
||||||
'webpack.config.js'
|
fileName
|
||||||
);
|
);
|
||||||
const pathToWebpackProdConfig = joinPathFragments(
|
deleteFileIfExists(host, pathToWebpackConfig);
|
||||||
options.appProjectRoot,
|
|
||||||
'webpack.config.prod.js'
|
|
||||||
);
|
|
||||||
if (host.exists(pathToWebpackConfig)) {
|
|
||||||
host.delete(pathToWebpackConfig);
|
|
||||||
}
|
}
|
||||||
if (host.exists(pathToWebpackProdConfig)) {
|
|
||||||
host.delete(pathToWebpackProdConfig);
|
if (options.typescriptConfiguration) {
|
||||||
|
processWebpackConfig(options, host, 'webpack.config.js');
|
||||||
|
processWebpackConfig(options, host, 'webpack.config.prod.js');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.dynamic) {
|
||||||
|
processWebpackConfig(options, host, 'webpack.config.prod.js');
|
||||||
|
processWebpackConfig(options, host, 'webpack.config.prod.ts');
|
||||||
|
if (!host.exists(pathToMFManifest)) {
|
||||||
|
host.write(pathToMFManifest, '{}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,6 +33,7 @@ export async function setupSsrForHost(
|
|||||||
project.root,
|
project.root,
|
||||||
{
|
{
|
||||||
...options,
|
...options,
|
||||||
|
static: !options?.dynamic,
|
||||||
remotes: defaultRemoteManifest.map(({ name, port }) => {
|
remotes: defaultRemoteManifest.map(({ name, port }) => {
|
||||||
return {
|
return {
|
||||||
...names(name),
|
...names(name),
|
||||||
|
|||||||
@ -25,6 +25,7 @@ export interface Schema {
|
|||||||
unitTestRunner: 'jest' | 'vitest' | 'none';
|
unitTestRunner: 'jest' | 'vitest' | 'none';
|
||||||
minimal?: boolean;
|
minimal?: boolean;
|
||||||
typescriptConfiguration?: boolean;
|
typescriptConfiguration?: boolean;
|
||||||
|
dynamic?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NormalizedSchema extends Schema {
|
export interface NormalizedSchema extends Schema {
|
||||||
|
|||||||
@ -74,6 +74,11 @@
|
|||||||
"enum": ["eslint"],
|
"enum": ["eslint"],
|
||||||
"default": "eslint"
|
"default": "eslint"
|
||||||
},
|
},
|
||||||
|
"dynamic": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Should the host application use dynamic federation?",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
"skipFormat": {
|
"skipFormat": {
|
||||||
"description": "Skip formatting files.",
|
"description": "Skip formatting files.",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
|
|||||||
@ -28,6 +28,7 @@ exports[`remote generator should create the remote with the correct config files
|
|||||||
exports[`remote generator should create the remote with the correct config files 3`] = `
|
exports[`remote generator should create the remote with the correct config files 3`] = `
|
||||||
"module.exports = {
|
"module.exports = {
|
||||||
name: 'test',
|
name: 'test',
|
||||||
|
|
||||||
exposes: {
|
exposes: {
|
||||||
'./Module': './src/remote-entry.ts',
|
'./Module': './src/remote-entry.ts',
|
||||||
},
|
},
|
||||||
@ -65,6 +66,7 @@ exports[`remote generator should create the remote with the correct config files
|
|||||||
|
|
||||||
const config: ModuleFederationConfig = {
|
const config: ModuleFederationConfig = {
|
||||||
name: 'test',
|
name: 'test',
|
||||||
|
|
||||||
exposes: {
|
exposes: {
|
||||||
'./Module': './src/remote-entry.ts',
|
'./Module': './src/remote-entry.ts',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2,6 +2,9 @@ import {ModuleFederationConfig} from '@nx/webpack';
|
|||||||
|
|
||||||
const config: ModuleFederationConfig = {
|
const config: ModuleFederationConfig = {
|
||||||
name: '<%= projectName %>',
|
name: '<%= projectName %>',
|
||||||
|
<% if (dynamic) { %>
|
||||||
|
library: { type: 'var', name: '<%= projectName %>'},
|
||||||
|
<% } %>
|
||||||
exposes: {
|
exposes: {
|
||||||
'./Module': './src/remote-entry.ts',
|
'./Module': './src/remote-entry.ts',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
name: '<%= projectName %>',
|
name: '<%= projectName %>',
|
||||||
|
<% if (dynamic) { %>
|
||||||
|
library: { type: 'var', name: '<%= projectName %>'},
|
||||||
|
<% } %>
|
||||||
exposes: {
|
exposes: {
|
||||||
'./Module': './src/remote-entry.ts',
|
'./Module': './src/remote-entry.ts',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { Tree } from '@nx/devkit';
|
||||||
|
|
||||||
|
export function addRemoteToDynamicHost(
|
||||||
|
tree: Tree,
|
||||||
|
remoteName: string,
|
||||||
|
remotePort: number,
|
||||||
|
pathToMfManifest: string
|
||||||
|
) {
|
||||||
|
const current = tree.read(pathToMfManifest, 'utf8');
|
||||||
|
tree.write(
|
||||||
|
pathToMfManifest,
|
||||||
|
JSON.stringify({
|
||||||
|
...JSON.parse(current),
|
||||||
|
[remoteName]: `http://localhost:${remotePort}`,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -20,6 +20,7 @@ import { Schema } from './schema';
|
|||||||
import setupSsrGenerator from '../setup-ssr/setup-ssr';
|
import setupSsrGenerator from '../setup-ssr/setup-ssr';
|
||||||
import { setupSsrForRemote } from './lib/setup-ssr-for-remote';
|
import { setupSsrForRemote } from './lib/setup-ssr-for-remote';
|
||||||
import { setupTspathForRemote } from './lib/setup-tspath-for-remote';
|
import { setupTspathForRemote } from './lib/setup-tspath-for-remote';
|
||||||
|
import { addRemoteToDynamicHost } from './lib/add-remote-to-dynamic-host';
|
||||||
|
|
||||||
export function addModuleFederationFiles(
|
export function addModuleFederationFiles(
|
||||||
host: Tree,
|
host: Tree,
|
||||||
@ -72,6 +73,7 @@ export async function remoteGeneratorInternal(host: Tree, schema: Schema) {
|
|||||||
const options: NormalizedSchema<Schema> = {
|
const options: NormalizedSchema<Schema> = {
|
||||||
...(await normalizeOptions<Schema>(host, schema, '@nx/react:remote')),
|
...(await normalizeOptions<Schema>(host, schema, '@nx/react:remote')),
|
||||||
typescriptConfiguration: schema.typescriptConfiguration ?? false,
|
typescriptConfiguration: schema.typescriptConfiguration ?? false,
|
||||||
|
dynamic: schema.dynamic ?? false,
|
||||||
};
|
};
|
||||||
const initAppTask = await applicationGenerator(host, {
|
const initAppTask = await applicationGenerator(host, {
|
||||||
...options,
|
...options,
|
||||||
@ -125,6 +127,20 @@ export async function remoteGeneratorInternal(host: Tree, schema: Schema) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.host && options.dynamic) {
|
||||||
|
const hostConfig = readProjectConfiguration(host, schema.host);
|
||||||
|
const pathToMFManifest = joinPathFragments(
|
||||||
|
hostConfig.sourceRoot,
|
||||||
|
'assets/module-federation.manifest.json'
|
||||||
|
);
|
||||||
|
addRemoteToDynamicHost(
|
||||||
|
host,
|
||||||
|
options.name,
|
||||||
|
options.devServerPort,
|
||||||
|
pathToMFManifest
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!options.skipFormat) {
|
if (!options.skipFormat) {
|
||||||
await formatFiles(host);
|
await formatFiles(host);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,7 @@ export interface Schema {
|
|||||||
tags?: string;
|
tags?: string;
|
||||||
unitTestRunner: 'jest' | 'vitest' | 'none';
|
unitTestRunner: 'jest' | 'vitest' | 'none';
|
||||||
typescriptConfiguration?: boolean;
|
typescriptConfiguration?: boolean;
|
||||||
|
dynamic?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NormalizedSchema extends ApplicationNormalizedSchema {
|
export interface NormalizedSchema extends ApplicationNormalizedSchema {
|
||||||
|
|||||||
@ -28,6 +28,12 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["as-provided", "derived"]
|
"enum": ["as-provided", "derived"]
|
||||||
},
|
},
|
||||||
|
"dynamic": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Should the host application use dynamic federation?",
|
||||||
|
"default": false,
|
||||||
|
"x-priority": "internal"
|
||||||
|
},
|
||||||
"style": {
|
"style": {
|
||||||
"description": "The file extension to be used for style files.",
|
"description": "The file extension to be used for style files.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
addDependenciesToPackageJson,
|
addDependenciesToPackageJson,
|
||||||
GeneratorCallback,
|
GeneratorCallback,
|
||||||
|
joinPathFragments,
|
||||||
readProjectConfiguration,
|
readProjectConfiguration,
|
||||||
Tree,
|
Tree,
|
||||||
updateProjectConfiguration,
|
updateProjectConfiguration,
|
||||||
@ -14,6 +15,7 @@ export function updateModuleFederationProject(
|
|||||||
appProjectRoot: string;
|
appProjectRoot: string;
|
||||||
devServerPort?: number;
|
devServerPort?: number;
|
||||||
typescriptConfiguration?: boolean;
|
typescriptConfiguration?: boolean;
|
||||||
|
dynamic?: boolean;
|
||||||
}
|
}
|
||||||
): GeneratorCallback {
|
): GeneratorCallback {
|
||||||
const projectConfig = readProjectConfiguration(host, options.projectName);
|
const projectConfig = readProjectConfiguration(host, options.projectName);
|
||||||
@ -33,6 +35,19 @@ export function updateModuleFederationProject(
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If host should be configured to use dynamic federation
|
||||||
|
if (options.dynamic) {
|
||||||
|
const pathToProdWebpackConfig = joinPathFragments(
|
||||||
|
projectConfig.root,
|
||||||
|
`webpack.prod.config.${options.typescriptConfiguration ? 'ts' : 'js'}`
|
||||||
|
);
|
||||||
|
if (host.exists(pathToProdWebpackConfig)) {
|
||||||
|
host.delete(pathToProdWebpackConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete projectConfig.targets.build.configurations.production?.webpackConfig;
|
||||||
|
}
|
||||||
|
|
||||||
projectConfig.targets.serve.executor =
|
projectConfig.targets.serve.executor =
|
||||||
'@nx/react:module-federation-dev-server';
|
'@nx/react:module-federation-dev-server';
|
||||||
projectConfig.targets.serve.options.port = options.devServerPort;
|
projectConfig.targets.serve.options.port = options.devServerPort;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user