feat(module-federation): add remote configuration override (#19694)

## Current Behavior

The configuration of the served MFE always passed to the remotes. If a
new configuration is needed to skip one remote (e.g.
`serve:skip-remote1`) but `remote2` then a configuration called
`skip-remote1` is needed in the `remote2`.

## Expected Behavior

Add an ability to override the configuration and the empty
configurations in the remotes can be deleted.

## Related Issue(s)

Fixes #19693
This commit is contained in:
Robin Csutorás 2024-05-08 19:52:37 +02:00 committed by GitHub
parent 0066543096
commit a08133f440
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 206 additions and 17 deletions

View File

@ -105,7 +105,20 @@
}, },
"devRemotes": { "devRemotes": {
"type": "array", "type": "array",
"items": { "type": "string" }, "items": {
"oneOf": [
{ "type": "string" },
{
"type": "object",
"properties": {
"remoteName": { "type": "string" },
"configuration": { "type": "string" }
},
"required": ["remoteName"],
"additionalProperties": false
}
]
},
"description": "List of remote applications to run in development mode (i.e. using serve target).", "description": "List of remote applications to run in development mode (i.e. using serve target).",
"x-priority": "important" "x-priority": "important"
}, },
@ -147,7 +160,7 @@
{ "required": ["buildTarget"] }, { "required": ["buildTarget"] },
{ "required": ["browserTarget"] } { "required": ["browserTarget"] }
], ],
"examplesFile": "## Examples\n\n{% tabs %}\n\n{% tab label=\"Basic Usage\" %}\nThe Module Federation Dev Server will serve a host application and find the remote applications associated with the host and serve them statically also. \nSee an example set up of it below:\n\n```json\n{\n \"serve\": {\n \"executor\": \"@nx/angular:module-federation-dev-server\",\n \"configurations\": {\n \"production\": {\n \"buildTarget\": \"host:build:production\"\n },\n \"development\": {\n \"buildTarget\": \"host:build:development\"\n }\n },\n \"defaultConfiguration\": \"development\",\n \"options\": {\n \"port\": 4200,\n \"publicHost\": \"http://localhost:4200\"\n }\n }\n}\n```\n\n{% /tab %}\n\n{% tab label=\"Serve host with remotes that can be live reloaded\" %}\nThe Module Federation Dev Server will serve a host application and find the remote applications associated with the host and serve a set selection with live reloading enabled also. \nSee an example set up of it below:\n\n```json\n{\n \"serve-with-hmr-remotes\": {\n \"executor\": \"@nx/angular:module-federation-dev-server\",\n \"configurations\": {\n \"production\": {\n \"buildTarget\": \"host:build:production\"\n },\n \"development\": {\n \"buildTarget\": \"host:build:development\"\n }\n },\n \"defaultConfiguration\": \"development\",\n \"options\": {\n \"port\": 4200,\n \"publicHost\": \"http://localhost:4200\",\n \"devRemotes\": [\"remote1\", \"remote2\"]\n }\n }\n}\n```\n\n{% /tab %}\n\n{% /tabs %}\n" "examplesFile": "## Examples\n\n{% tabs %}\n\n{% tab label=\"Basic Usage\" %}\nThe Module Federation Dev Server will serve a host application and find the remote applications associated with the host and serve them statically also. \nSee an example set up of it below:\n\n```json\n{\n \"serve\": {\n \"executor\": \"@nx/angular:module-federation-dev-server\",\n \"configurations\": {\n \"production\": {\n \"buildTarget\": \"host:build:production\"\n },\n \"development\": {\n \"buildTarget\": \"host:build:development\"\n }\n },\n \"defaultConfiguration\": \"development\",\n \"options\": {\n \"port\": 4200,\n \"publicHost\": \"http://localhost:4200\"\n }\n }\n}\n```\n\n{% /tab %}\n\n{% tab label=\"Serve host with remotes that can be live reloaded\" %}\nThe Module Federation Dev Server will serve a host application and find the remote applications associated with the host and serve a set selection with live reloading enabled also. \nSee an example set up of it below:\n\n```json\n{\n \"serve-with-hmr-remotes\": {\n \"executor\": \"@nx/angular:module-federation-dev-server\",\n \"configurations\": {\n \"production\": {\n \"buildTarget\": \"host:build:production\"\n },\n \"development\": {\n \"buildTarget\": \"host:build:development\"\n }\n },\n \"defaultConfiguration\": \"development\",\n \"options\": {\n \"port\": 4200,\n \"publicHost\": \"http://localhost:4200\",\n \"devRemotes\": [\n \"remote1\",\n {\n \"remoteName\": \"remote2\",\n \"configuration\": \"development\"\n }\n ]\n }\n }\n}\n```\n\n{% /tab %}\n\n{% /tabs %}\n"
}, },
"description": "Serves host [Module Federation](https://module-federation.io/) applications ([webpack](https://webpack.js.org/)-based) allowing to specify which remote applications should be served with the host.", "description": "Serves host [Module Federation](https://module-federation.io/) applications ([webpack](https://webpack.js.org/)-based) allowing to specify which remote applications should be served with the host.",
"aliases": [], "aliases": [],

View File

@ -11,7 +11,20 @@
"properties": { "properties": {
"devRemotes": { "devRemotes": {
"type": "array", "type": "array",
"items": { "type": "string" }, "items": {
"oneOf": [
{ "type": "string" },
{
"type": "object",
"properties": {
"remoteName": { "type": "string" },
"configuration": { "type": "string" }
},
"required": ["remoteName"],
"additionalProperties": false
}
]
},
"description": "List of remote applications to run in development mode (i.e. using serve target).", "description": "List of remote applications to run in development mode (i.e. using serve target).",
"x-priority": "important" "x-priority": "important"
}, },
@ -114,6 +127,7 @@
"description": "Path to a Module Federation manifest file (e.g. `my/path/to/module-federation.manifest.json`) containing the dynamic remote applications relative to the workspace root." "description": "Path to a Module Federation manifest file (e.g. `my/path/to/module-federation.manifest.json`) containing the dynamic remote applications relative to the workspace root."
} }
}, },
"examplesFile": "## Examples\n\n{% tabs %}\n\n{% tab label=\"Basic Usage\" %}\nThe Module Federation Dev Server will serve a host application and find the remote applications associated with the host and serve them statically also. \nSee an example set up of it below:\n\n```json\n{\n \"serve\": {\n \"executor\": \"@nx/react:module-federation-dev-server\",\n \"configurations\": {\n \"production\": {\n \"buildTarget\": \"host:build:production\"\n },\n \"development\": {\n \"buildTarget\": \"host:build:development\"\n }\n },\n \"defaultConfiguration\": \"development\",\n \"options\": {\n \"port\": 4200,\n \"publicHost\": \"http://localhost:4200\"\n }\n }\n}\n```\n\n{% /tab %}\n\n{% tab label=\"Serve host with remotes that can be live reloaded\" %}\nThe Module Federation Dev Server will serve a host application and find the remote applications associated with the host and serve a set selection with live reloading enabled also. \nSee an example set up of it below:\n\n```json\n{\n \"serve-with-hmr-remotes\": {\n \"executor\": \"@nx/react:module-federation-dev-server\",\n \"configurations\": {\n \"production\": {\n \"buildTarget\": \"host:build:production\"\n },\n \"development\": {\n \"buildTarget\": \"host:build:development\"\n }\n },\n \"defaultConfiguration\": \"development\",\n \"options\": {\n \"port\": 4200,\n \"publicHost\": \"http://localhost:4200\",\n \"devRemotes\": [\n \"remote1\",\n {\n \"remoteName\": \"remote2\",\n \"configuration\": \"development\"\n }\n ]\n }\n }\n}\n```\n\n{% /tab %}\n\n{% /tabs %}\n",
"presets": [] "presets": []
}, },
"description": "Serve a host or remote application.", "description": "Serve a host or remote application.",

View File

@ -49,7 +49,13 @@ See an example set up of it below:
"options": { "options": {
"port": 4200, "port": 4200,
"publicHost": "http://localhost:4200", "publicHost": "http://localhost:4200",
"devRemotes": ["remote1", "remote2"] "devRemotes": [
"remote1",
{
"remoteName": "remote2",
"configuration": "development"
}
]
} }
} }
} }

View File

@ -155,11 +155,24 @@ export function getStaticRemotes(
} }
export function validateDevRemotes( export function validateDevRemotes(
options: { devRemotes?: string[] }, options: {
devRemotes?: (
| string
| {
remoteName: string;
configuration: string;
}
)[];
},
workspaceProjects: Record<string, ProjectConfiguration> workspaceProjects: Record<string, ProjectConfiguration>
): void { ): void {
const invalidDevRemotes = const invalidDevRemotes =
options.devRemotes?.filter((remote) => !workspaceProjects[remote]) ?? []; options.devRemotes?.filter(
(remote) =>
!(typeof remote === 'string'
? workspaceProjects[remote]
: workspaceProjects[remote.remoteName])
) ?? [];
if (invalidDevRemotes.length) { if (invalidDevRemotes.length) {
throw new Error( throw new Error(

View File

@ -28,12 +28,21 @@ export async function startRemotes(
'module-federation-dev-server' 'module-federation-dev-server'
); );
const configurationOverride = options.devRemotes.find(
(
r
): r is {
remoteName: string;
configuration: string;
} => typeof r !== 'string' && r.remoteName === app
)?.configuration;
remoteIters.push( remoteIters.push(
await runExecutor( await runExecutor(
{ {
project: app, project: app,
target, target,
configuration: context.configurationName, configuration: configurationOverride ?? context.configurationName,
}, },
{ {
...(target === 'serve' ? { verbose: options.verbose ?? false } : {}), ...(target === 'serve' ? { verbose: options.verbose ?? false } : {}),

View File

@ -108,8 +108,12 @@ export async function* moduleFederationDevServerExecutor(
'angular' 'angular'
); );
const remoteNames = options.devRemotes?.map((r) =>
typeof r === 'string' ? r : r.remoteName
);
const remotes = getRemotes( const remotes = getRemotes(
options.devRemotes, remoteNames,
options.skipRemotes, options.skipRemotes,
moduleFederationConfig, moduleFederationConfig,
{ {
@ -122,8 +126,10 @@ export async function* moduleFederationDevServerExecutor(
if (remotes.devRemotes.length > 0 && !schema.staticRemotesPort) { if (remotes.devRemotes.length > 0 && !schema.staticRemotesPort) {
options.staticRemotesPort = options.devRemotes.reduce((portToUse, r) => { options.staticRemotesPort = options.devRemotes.reduce((portToUse, r) => {
const remoteName = typeof r === 'string' ? r : r.remoteName;
const remotePort = const remotePort =
context.projectGraph.nodes[r].data.targets['serve'].options.port; context.projectGraph.nodes[remoteName].data.targets['serve'].options
.port;
if (remotePort >= portToUse) { if (remotePort >= portToUse) {
return remotePort + 1; return remotePort + 1;
} else { } else {

View File

@ -16,7 +16,13 @@ interface BaseSchema {
hmr?: boolean; hmr?: boolean;
watch?: boolean; watch?: boolean;
poll?: number; poll?: number;
devRemotes?: string[]; devRemotes?: (
| string
| {
remoteName: string;
configuration: string;
}
)[];
skipRemotes?: string[]; skipRemotes?: string[];
pathToManifestFile?: string; pathToManifestFile?: string;
static?: boolean; static?: boolean;

View File

@ -112,7 +112,24 @@
"devRemotes": { "devRemotes": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "oneOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"remoteName": {
"type": "string"
},
"configuration": {
"type": "string"
}
},
"required": ["remoteName"],
"additionalProperties": false
}
]
}, },
"description": "List of remote applications to run in development mode (i.e. using serve target).", "description": "List of remote applications to run in development mode (i.e. using serve target).",
"x-priority": "important" "x-priority": "important"

View File

@ -0,0 +1,66 @@
## Examples
{% tabs %}
{% tab label="Basic Usage" %}
The Module Federation Dev Server will serve a host application and find the remote applications associated with the host and serve them statically also.
See an example set up of it below:
```json
{
"serve": {
"executor": "@nx/react:module-federation-dev-server",
"configurations": {
"production": {
"buildTarget": "host:build:production"
},
"development": {
"buildTarget": "host:build:development"
}
},
"defaultConfiguration": "development",
"options": {
"port": 4200,
"publicHost": "http://localhost:4200"
}
}
}
```
{% /tab %}
{% tab label="Serve host with remotes that can be live reloaded" %}
The Module Federation Dev Server will serve a host application and find the remote applications associated with the host and serve a set selection with live reloading enabled also.
See an example set up of it below:
```json
{
"serve-with-hmr-remotes": {
"executor": "@nx/react:module-federation-dev-server",
"configurations": {
"production": {
"buildTarget": "host:build:production"
},
"development": {
"buildTarget": "host:build:development"
}
},
"defaultConfiguration": "development",
"options": {
"port": 4200,
"publicHost": "http://localhost:4200",
"devRemotes": [
"remote1",
{
"remoteName": "remote2",
"configuration": "development"
}
]
}
}
}
```
{% /tab %}
{% /tabs %}

View File

@ -26,7 +26,13 @@ import { existsSync } from 'fs';
import { extname } from 'path'; import { extname } from 'path';
type ModuleFederationDevServerOptions = WebDevServerOptions & { type ModuleFederationDevServerOptions = WebDevServerOptions & {
devRemotes?: string[]; devRemotes?: (
| string
| {
remoteName: string;
configuration: string;
}
)[];
skipRemotes?: string[]; skipRemotes?: string[];
static?: boolean; static?: boolean;
isInitialHost?: boolean; isInitialHost?: boolean;
@ -112,6 +118,15 @@ async function startRemotes(
'module-federation-dev-server' 'module-federation-dev-server'
); );
const configurationOverride = options.devRemotes?.find(
(
r
): r is {
remoteName: string;
configuration: string;
} => typeof r !== 'string' && r.remoteName === app
)?.configuration;
const overrides = const overrides =
target === 'serve' target === 'serve'
? { ? {
@ -130,7 +145,7 @@ async function startRemotes(
{ {
project: app, project: app,
target, target,
configuration: context.configurationName, configuration: configurationOverride ?? context.configurationName,
}, },
overrides, overrides,
context context
@ -307,8 +322,12 @@ export default async function* moduleFederationDevServer(
'react' 'react'
); );
const remoteNames = options.devRemotes?.map((r) =>
typeof r === 'string' ? r : r.remoteName
);
const remotes = getRemotes( const remotes = getRemotes(
options.devRemotes, remoteNames,
options.skipRemotes, options.skipRemotes,
moduleFederationConfig, moduleFederationConfig,
{ {
@ -321,8 +340,10 @@ export default async function* moduleFederationDevServer(
if (remotes.devRemotes.length > 0 && !initialStaticRemotesPorts) { if (remotes.devRemotes.length > 0 && !initialStaticRemotesPorts) {
options.staticRemotesPort = options.devRemotes.reduce((portToUse, r) => { options.staticRemotesPort = options.devRemotes.reduce((portToUse, r) => {
const remoteName = typeof r === 'string' ? r : r.remoteName;
const remotePort = const remotePort =
context.projectGraph.nodes[r].data.targets['serve'].options.port; context.projectGraph.nodes[remoteName].data.targets['serve'].options
.port;
if (remotePort >= portToUse) { if (remotePort >= portToUse) {
return remotePort + 1; return remotePort + 1;
} else { } else {

View File

@ -9,7 +9,24 @@
"devRemotes": { "devRemotes": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "oneOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"remoteName": {
"type": "string"
},
"configuration": {
"type": "string"
}
},
"required": ["remoteName"],
"additionalProperties": false
}
]
}, },
"description": "List of remote applications to run in development mode (i.e. using serve target).", "description": "List of remote applications to run in development mode (i.e. using serve target).",
"x-priority": "important" "x-priority": "important"
@ -114,5 +131,6 @@
"type": "string", "type": "string",
"description": "Path to a Module Federation manifest file (e.g. `my/path/to/module-federation.manifest.json`) containing the dynamic remote applications relative to the workspace root." "description": "Path to a Module Federation manifest file (e.g. `my/path/to/module-federation.manifest.json`) containing the dynamic remote applications relative to the workspace root."
} }
} },
"examplesFile": "../../../docs/module-federation-dev-server-examples.md"
} }