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"],
|
||||
"default": "eslint"
|
||||
},
|
||||
"dynamic": {
|
||||
"type": "boolean",
|
||||
"description": "Should the host application use dynamic federation?",
|
||||
"default": false
|
||||
},
|
||||
"skipFormat": {
|
||||
"description": "Skip formatting files.",
|
||||
"type": "boolean",
|
||||
|
||||
@ -28,6 +28,12 @@
|
||||
"type": "string",
|
||||
"enum": ["as-provided", "derived"]
|
||||
},
|
||||
"dynamic": {
|
||||
"type": "boolean",
|
||||
"description": "Should the host application use dynamic federation?",
|
||||
"default": false,
|
||||
"x-priority": "internal"
|
||||
},
|
||||
"style": {
|
||||
"description": "The file extension to be used for style files.",
|
||||
"type": "string",
|
||||
|
||||
@ -2,6 +2,7 @@ import { Tree, stripIndents } from '@nx/devkit';
|
||||
import {
|
||||
checkFilesExist,
|
||||
cleanupProject,
|
||||
fileExists,
|
||||
killPorts,
|
||||
killProcessAndPorts,
|
||||
newProject,
|
||||
@ -10,6 +11,7 @@ import {
|
||||
runCLIAsync,
|
||||
runCommandUntil,
|
||||
runE2ETests,
|
||||
tmpProjPath,
|
||||
uniq,
|
||||
updateFile,
|
||||
updateJson,
|
||||
@ -165,9 +167,11 @@ describe('React Module Federation', () => {
|
||||
`apps/${app}/module-federation.server.config.ts`
|
||||
);
|
||||
['build', 'server'].forEach((target) => {
|
||||
['development', 'production'].forEach((configuration) => {
|
||||
['development', 'production'].forEach(async (configuration) => {
|
||||
const cliOutput = runCLI(`run ${app}:${target}:${configuration}`);
|
||||
expect(cliOutput).toContain('Successfully ran target');
|
||||
|
||||
await killPorts(readPort(app));
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -866,6 +870,91 @@ describe('React Module Federation', () => {
|
||||
}
|
||||
}, 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 {
|
||||
|
||||
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 { Link, Route, Routes } from 'react-router-dom';
|
||||
<% if (dynamic) { %>
|
||||
import { loadRemoteModule } from '@nx/react/mf';
|
||||
<% } %>
|
||||
|
||||
<% if (remotes.length > 0) { %>
|
||||
<% 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'));
|
||||
<% } %>
|
||||
<% }); %>
|
||||
<% } %>
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<React.Suspense fallback={null}>
|
||||
|
||||
@ -3,8 +3,11 @@ import { ModuleFederationConfig } from '@nx/webpack';
|
||||
const config: ModuleFederationConfig = {
|
||||
name: '<%= projectName %>',
|
||||
remotes: [
|
||||
<% remotes.forEach(function(r) {%> "<%= r.fileName %>", <% }); %>
|
||||
],
|
||||
<% if (static) {
|
||||
remotes.forEach(function(r) { %> "<%= r.fileName %>", <% });
|
||||
}
|
||||
%>
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@ -6,8 +6,11 @@
|
||||
const moduleFederationConfig = {
|
||||
name: '<%= projectName %>',
|
||||
remotes: [
|
||||
<% remotes.forEach(function(r) {%> "<%= r.fileName %>", <% }); %>
|
||||
],
|
||||
<% if (static) {
|
||||
remotes.forEach(function(r) { %> "<%= r.fileName %>", <% });
|
||||
}
|
||||
%>
|
||||
],
|
||||
};
|
||||
|
||||
module.exports = moduleFederationConfig;
|
||||
|
||||
@ -15,8 +15,11 @@ const config: ModuleFederationConfig = {
|
||||
*
|
||||
*/
|
||||
remotes: [
|
||||
<% remotes.forEach(function(r) {%> "<%= r.fileName %>", <% }); %>
|
||||
],
|
||||
<% if (static) {
|
||||
remotes.forEach(function(r) { %> "<%= r.fileName %>", <% });
|
||||
}
|
||||
%>
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@ -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.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 = {
|
||||
...(await normalizeOptions<Schema>(host, schema, '@nx/react:host')),
|
||||
typescriptConfiguration: schema.typescriptConfiguration ?? true,
|
||||
dynamic: schema.dynamic ?? false,
|
||||
};
|
||||
|
||||
const initTask = await applicationGenerator(host, {
|
||||
@ -71,6 +72,8 @@ export async function hostGeneratorInternal(
|
||||
skipFormat: true,
|
||||
projectNameAndRootFormat: options.projectNameAndRootFormat,
|
||||
typescriptConfiguration: options.typescriptConfiguration,
|
||||
dynamic: options.dynamic,
|
||||
host: options.name,
|
||||
});
|
||||
tasks.push(remoteTask);
|
||||
remotePort++;
|
||||
|
||||
@ -1,15 +1,21 @@
|
||||
import { NormalizedSchema } from '../schema';
|
||||
import { generateFiles, joinPathFragments, names } from '@nx/devkit';
|
||||
import { join } from 'path';
|
||||
import {
|
||||
Tree,
|
||||
generateFiles,
|
||||
joinPathFragments,
|
||||
names,
|
||||
readProjectConfiguration,
|
||||
} from '@nx/devkit';
|
||||
|
||||
export function addModuleFederationFiles(
|
||||
host,
|
||||
host: Tree,
|
||||
options: NormalizedSchema,
|
||||
defaultRemoteManifest: { name: string; port: number }[]
|
||||
) {
|
||||
const templateVariables = {
|
||||
...names(options.name),
|
||||
...options,
|
||||
static: !options?.dynamic,
|
||||
tmpl: '',
|
||||
remotes: defaultRemoteManifest.map(({ name, port }) => {
|
||||
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.
|
||||
// Renaming original entry file so we can use `import(./bootstrap)` in
|
||||
// new entry file.
|
||||
host.rename(
|
||||
join(options.appProjectRoot, 'src/main.tsx'),
|
||||
join(options.appProjectRoot, 'src/bootstrap.tsx')
|
||||
joinPathFragments(options.appProjectRoot, 'src/main.tsx'),
|
||||
joinPathFragments(options.appProjectRoot, 'src/bootstrap.tsx')
|
||||
);
|
||||
|
||||
generateFiles(
|
||||
host,
|
||||
join(__dirname, `../files/common`),
|
||||
joinPathFragments(__dirname, `../files/common`),
|
||||
options.appProjectRoot,
|
||||
templateVariables
|
||||
);
|
||||
@ -40,25 +52,35 @@ export function addModuleFederationFiles(
|
||||
// New entry file is created here.
|
||||
generateFiles(
|
||||
host,
|
||||
join(__dirname, `../files/${pathToModuleFederationFiles}`),
|
||||
joinPathFragments(__dirname, `../files/${pathToModuleFederationFiles}`),
|
||||
options.appProjectRoot,
|
||||
templateVariables
|
||||
);
|
||||
|
||||
if (options.typescriptConfiguration) {
|
||||
function deleteFileIfExists(host, filePath) {
|
||||
if (host.exists(filePath)) {
|
||||
host.delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
function processWebpackConfig(options, host, fileName) {
|
||||
const pathToWebpackConfig = joinPathFragments(
|
||||
options.appProjectRoot,
|
||||
'webpack.config.js'
|
||||
fileName
|
||||
);
|
||||
const pathToWebpackProdConfig = joinPathFragments(
|
||||
options.appProjectRoot,
|
||||
'webpack.config.prod.js'
|
||||
);
|
||||
if (host.exists(pathToWebpackConfig)) {
|
||||
host.delete(pathToWebpackConfig);
|
||||
}
|
||||
if (host.exists(pathToWebpackProdConfig)) {
|
||||
host.delete(pathToWebpackProdConfig);
|
||||
deleteFileIfExists(host, pathToWebpackConfig);
|
||||
}
|
||||
|
||||
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,
|
||||
{
|
||||
...options,
|
||||
static: !options?.dynamic,
|
||||
remotes: defaultRemoteManifest.map(({ name, port }) => {
|
||||
return {
|
||||
...names(name),
|
||||
|
||||
@ -25,6 +25,7 @@ export interface Schema {
|
||||
unitTestRunner: 'jest' | 'vitest' | 'none';
|
||||
minimal?: boolean;
|
||||
typescriptConfiguration?: boolean;
|
||||
dynamic?: boolean;
|
||||
}
|
||||
|
||||
export interface NormalizedSchema extends Schema {
|
||||
|
||||
@ -74,6 +74,11 @@
|
||||
"enum": ["eslint"],
|
||||
"default": "eslint"
|
||||
},
|
||||
"dynamic": {
|
||||
"type": "boolean",
|
||||
"description": "Should the host application use dynamic federation?",
|
||||
"default": false
|
||||
},
|
||||
"skipFormat": {
|
||||
"description": "Skip formatting files.",
|
||||
"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`] = `
|
||||
"module.exports = {
|
||||
name: 'test',
|
||||
|
||||
exposes: {
|
||||
'./Module': './src/remote-entry.ts',
|
||||
},
|
||||
@ -65,6 +66,7 @@ exports[`remote generator should create the remote with the correct config files
|
||||
|
||||
const config: ModuleFederationConfig = {
|
||||
name: 'test',
|
||||
|
||||
exposes: {
|
||||
'./Module': './src/remote-entry.ts',
|
||||
},
|
||||
|
||||
@ -2,6 +2,9 @@ import {ModuleFederationConfig} from '@nx/webpack';
|
||||
|
||||
const config: ModuleFederationConfig = {
|
||||
name: '<%= projectName %>',
|
||||
<% if (dynamic) { %>
|
||||
library: { type: 'var', name: '<%= projectName %>'},
|
||||
<% } %>
|
||||
exposes: {
|
||||
'./Module': './src/remote-entry.ts',
|
||||
},
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
module.exports = {
|
||||
name: '<%= projectName %>',
|
||||
<% if (dynamic) { %>
|
||||
library: { type: 'var', name: '<%= projectName %>'},
|
||||
<% } %>
|
||||
exposes: {
|
||||
'./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 { setupSsrForRemote } from './lib/setup-ssr-for-remote';
|
||||
import { setupTspathForRemote } from './lib/setup-tspath-for-remote';
|
||||
import { addRemoteToDynamicHost } from './lib/add-remote-to-dynamic-host';
|
||||
|
||||
export function addModuleFederationFiles(
|
||||
host: Tree,
|
||||
@ -72,6 +73,7 @@ export async function remoteGeneratorInternal(host: Tree, schema: Schema) {
|
||||
const options: NormalizedSchema<Schema> = {
|
||||
...(await normalizeOptions<Schema>(host, schema, '@nx/react:remote')),
|
||||
typescriptConfiguration: schema.typescriptConfiguration ?? false,
|
||||
dynamic: schema.dynamic ?? false,
|
||||
};
|
||||
const initAppTask = await applicationGenerator(host, {
|
||||
...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) {
|
||||
await formatFiles(host);
|
||||
}
|
||||
|
||||
@ -26,6 +26,7 @@ export interface Schema {
|
||||
tags?: string;
|
||||
unitTestRunner: 'jest' | 'vitest' | 'none';
|
||||
typescriptConfiguration?: boolean;
|
||||
dynamic?: boolean;
|
||||
}
|
||||
|
||||
export interface NormalizedSchema extends ApplicationNormalizedSchema {
|
||||
|
||||
@ -28,6 +28,12 @@
|
||||
"type": "string",
|
||||
"enum": ["as-provided", "derived"]
|
||||
},
|
||||
"dynamic": {
|
||||
"type": "boolean",
|
||||
"description": "Should the host application use dynamic federation?",
|
||||
"default": false,
|
||||
"x-priority": "internal"
|
||||
},
|
||||
"style": {
|
||||
"description": "The file extension to be used for style files.",
|
||||
"type": "string",
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import {
|
||||
addDependenciesToPackageJson,
|
||||
GeneratorCallback,
|
||||
joinPathFragments,
|
||||
readProjectConfiguration,
|
||||
Tree,
|
||||
updateProjectConfiguration,
|
||||
@ -14,6 +15,7 @@ export function updateModuleFederationProject(
|
||||
appProjectRoot: string;
|
||||
devServerPort?: number;
|
||||
typescriptConfiguration?: boolean;
|
||||
dynamic?: boolean;
|
||||
}
|
||||
): GeneratorCallback {
|
||||
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 =
|
||||
'@nx/react:module-federation-dev-server';
|
||||
projectConfig.targets.serve.options.port = options.devServerPort;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user