feat(angular): add withModuleFederationForSSR function (#13172)

This commit is contained in:
Colum Ferry 2022-11-15 16:35:09 +00:00 committed by GitHub
parent 54670c93e0
commit e70fd48880
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 228 additions and 85 deletions

View File

@ -162,6 +162,7 @@ It only uses language primitives and immutable objects
- [isStandaloneProject](../../devkit/index#isstandaloneproject) - [isStandaloneProject](../../devkit/index#isstandaloneproject)
- [joinPathFragments](../../devkit/index#joinpathfragments) - [joinPathFragments](../../devkit/index#joinpathfragments)
- [mapRemotes](../../devkit/index#mapremotes) - [mapRemotes](../../devkit/index#mapremotes)
- [mapRemotesForSSR](../../devkit/index#mapremotesforssr)
- [moveFilesToNewDirectory](../../devkit/index#movefilestonewdirectory) - [moveFilesToNewDirectory](../../devkit/index#movefilestonewdirectory)
- [names](../../devkit/index#names) - [names](../../devkit/index#names)
- [normalizePath](../../devkit/index#normalizepath) - [normalizePath](../../devkit/index#normalizepath)
@ -1495,6 +1496,27 @@ Federation.
--- ---
### mapRemotesForSSR
**mapRemotesForSSR**(`remotes`, `remoteEntryExt`, `determineRemoteUrl`): `Record`<`string`, `string`\>
Map remote names to a format that can be understood and used by Module
Federation.
#### Parameters
| Name | Type | Description |
| :------------------- | :-------------------------------------- | :------------------------------------------------------- |
| `remotes` | [`Remotes`](../../devkit/index#remotes) | The remotes to map |
| `remoteEntryExt` | `"js"` \| `"mjs"` | The file extension of the remoteEntry file |
| `determineRemoteUrl` | (`remote`: `string`) => `string` | The function used to lookup the URL of the served remote |
#### Returns
`Record`<`string`, `string`\>
---
### moveFilesToNewDirectory ### moveFilesToNewDirectory
**moveFilesToNewDirectory**(`tree`, `oldDir`, `newDir`): `void` **moveFilesToNewDirectory**(`tree`, `oldDir`, `newDir`): `void`

File diff suppressed because one or more lines are too long

View File

@ -1 +1,2 @@
export { withModuleFederation } from '../src/utils/mf/with-module-federation'; export { withModuleFederation } from '../src/utils/mf/with-module-federation';
export { withModuleFederationForSSR } from '../src/utils/mf/with-module-federation-ssr';

View File

@ -0,0 +1,94 @@
import {
applyAdditionalShared,
applySharedFunction,
createProjectGraphAsync,
getDependentPackagesForProject,
mapRemotes,
mapRemotesForSSR,
ModuleFederationConfig,
ProjectGraph,
readCachedProjectGraph,
SharedLibraryConfig,
sharePackages,
shareWorkspaceLibraries,
} from '@nrwl/devkit';
export function applyDefaultEagerPackages(
sharedConfig: Record<string, SharedLibraryConfig>
) {
const DEFAULT_PACKAGES_TO_LOAD_EAGERLY = [
'@angular/localize',
'@angular/localize/init',
];
for (const pkg of DEFAULT_PACKAGES_TO_LOAD_EAGERLY) {
if (!sharedConfig[pkg]) {
continue;
}
sharedConfig[pkg] = { ...sharedConfig[pkg], eager: true };
}
}
export const DEFAULT_NPM_PACKAGES_TO_AVOID = ['zone.js', '@nrwl/angular/mf'];
export const DEFAULT_ANGULAR_PACKAGES_TO_SHARE = [
'@angular/animations',
'@angular/common',
];
export async function getModuleFederationConfig(
mfConfig: ModuleFederationConfig,
determineRemoteUrl: (remote: string) => string,
options: { isServer: boolean } = { isServer: false }
) {
let projectGraph: ProjectGraph<any>;
try {
projectGraph = readCachedProjectGraph();
} catch (e) {
projectGraph = await createProjectGraphAsync();
}
const dependencies = getDependentPackagesForProject(
projectGraph,
mfConfig.name
);
const sharedLibraries = shareWorkspaceLibraries(
dependencies.workspaceLibraries
);
const npmPackages = sharePackages(
Array.from(
new Set([
...DEFAULT_ANGULAR_PACKAGES_TO_SHARE,
...dependencies.npmPackages.filter(
(pkg) => !DEFAULT_NPM_PACKAGES_TO_AVOID.includes(pkg)
),
])
)
);
DEFAULT_NPM_PACKAGES_TO_AVOID.forEach((pkgName) => {
if (pkgName in npmPackages) {
delete npmPackages[pkgName];
}
});
const sharedDependencies = {
...sharedLibraries.getLibraries(),
...npmPackages,
};
applyDefaultEagerPackages(sharedDependencies);
applySharedFunction(sharedDependencies, mfConfig.shared);
applyAdditionalShared(
sharedDependencies,
mfConfig.additionalShared,
projectGraph
);
const mapRemotesFunction = options.isServer ? mapRemotesForSSR : mapRemotes;
const mappedRemotes =
!mfConfig.remotes || mfConfig.remotes.length === 0
? {}
: mapRemotesFunction(mfConfig.remotes, 'mjs', determineRemoteUrl);
return { sharedLibraries, sharedDependencies, mappedRemotes };
}

View File

@ -0,0 +1,68 @@
import { readCachedProjectConfiguration } from 'nx/src/project-graph/project-graph';
import { ModuleFederationConfig } from '@nrwl/devkit';
import { getModuleFederationConfig } from './utils';
function determineRemoteUrl(remote: string) {
const remoteProjectConfiguration = readCachedProjectConfiguration(remote);
let publicHost = '';
try {
publicHost = remoteProjectConfiguration.targets.serve.options.publicHost;
} catch (error) {
throw new Error(
`Cannot automatically determine URL of remote (${remote}). Looked for property "publicHost" in the project's "serve" target.\n
You can also use the tuple syntax in your webpack config to configure your remotes. e.g. \`remotes: [['remote1', 'http://localhost:4201']]\``
);
}
return `${
publicHost.endsWith('/') ? publicHost.slice(0, -1) : publicHost
}/server/remoteEntry.js`;
}
export async function withModuleFederationForSSR(
options: ModuleFederationConfig
) {
const { sharedLibraries, sharedDependencies, mappedRemotes } =
await getModuleFederationConfig(options, determineRemoteUrl, {
isServer: true,
});
return (config) => ({
...(config ?? {}),
target: false,
output: {
...(config.output ?? {}),
uniqueName: options.name,
},
optimization: {
...(config.optimization ?? {}),
runtimeChunk: false,
},
resolve: {
...(config.resolve ?? {}),
alias: {
...(config.resolve?.alias ?? {}),
...sharedLibraries.getAliases(),
},
},
plugins: [
...(config.plugins ?? []),
new (require('@module-federation/node').UniversalFederationPlugin)(
{
name: options.name,
filename: 'remoteEntry.js',
exposes: options.exposes,
remotes: mappedRemotes,
shared: {
...sharedDependencies,
},
library: {
type: 'commonjs-module',
},
isServer: true,
},
{}
),
sharedLibraries.getReplacementPlugin(),
],
});
}

View File

@ -1,16 +1,5 @@
import { import { ModuleFederationConfig } from '@nrwl/devkit';
applyAdditionalShared, import { getModuleFederationConfig } from './utils';
applySharedFunction,
createProjectGraphAsync,
getDependentPackagesForProject,
mapRemotes,
ModuleFederationConfig,
ProjectGraph,
readCachedProjectGraph,
SharedLibraryConfig,
sharePackages,
shareWorkspaceLibraries,
} from '@nrwl/devkit';
import { readCachedProjectConfiguration } from 'nx/src/project-graph/project-graph'; import { readCachedProjectConfiguration } from 'nx/src/project-graph/project-graph';
import ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin'); import ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
@ -30,78 +19,9 @@ function determineRemoteUrl(remote: string) {
}/remoteEntry.mjs`; }/remoteEntry.mjs`;
} }
function applyDefaultEagerPackages(
sharedConfig: Record<string, SharedLibraryConfig>
) {
const DEFAULT_PACKAGES_TO_LOAD_EAGERLY = [
'@angular/localize',
'@angular/localize/init',
];
for (const pkg of DEFAULT_PACKAGES_TO_LOAD_EAGERLY) {
if (!sharedConfig[pkg]) {
continue;
}
sharedConfig[pkg] = { ...sharedConfig[pkg], eager: true };
}
}
export async function withModuleFederation(options: ModuleFederationConfig) { export async function withModuleFederation(options: ModuleFederationConfig) {
const DEFAULT_NPM_PACKAGES_TO_AVOID = ['zone.js', '@nrwl/angular/mf']; const { sharedLibraries, sharedDependencies, mappedRemotes } =
const DEFAULT_ANGULAR_PACKAGES_TO_SHARE = [ await getModuleFederationConfig(options, determineRemoteUrl);
'@angular/animations',
'@angular/common',
];
let projectGraph: ProjectGraph<any>;
try {
projectGraph = readCachedProjectGraph();
} catch (e) {
projectGraph = await createProjectGraphAsync();
}
const dependencies = getDependentPackagesForProject(
projectGraph,
options.name
);
const sharedLibraries = shareWorkspaceLibraries(
dependencies.workspaceLibraries
);
const npmPackages = sharePackages(
Array.from(
new Set([
...DEFAULT_ANGULAR_PACKAGES_TO_SHARE,
...dependencies.npmPackages.filter(
(pkg) => !DEFAULT_NPM_PACKAGES_TO_AVOID.includes(pkg)
),
])
)
);
DEFAULT_NPM_PACKAGES_TO_AVOID.forEach((pkgName) => {
if (pkgName in npmPackages) {
delete npmPackages[pkgName];
}
});
const sharedDependencies = {
...sharedLibraries.getLibraries(),
...npmPackages,
};
applyDefaultEagerPackages(sharedDependencies);
applySharedFunction(sharedDependencies, options.shared);
applyAdditionalShared(
sharedDependencies,
options.additionalShared,
projectGraph
);
const mappedRemotes =
!options.remotes || options.remotes.length === 0
? {}
: mapRemotes(options.remotes, 'mjs', determineRemoteUrl);
return (config) => ({ return (config) => ({
...(config ?? {}), ...(config ?? {}),

View File

@ -305,6 +305,7 @@ export {
applySharedFunction, applySharedFunction,
applyAdditionalShared, applyAdditionalShared,
mapRemotes, mapRemotes,
mapRemotesForSSR,
getNpmPackageSharedConfig, getNpmPackageSharedConfig,
shareWorkspaceLibraries, shareWorkspaceLibraries,
sharePackages, sharePackages,

View File

@ -34,3 +34,39 @@ export function mapRemotes(
return mappedRemotes; return mappedRemotes;
} }
/**
* Map remote names to a format that can be understood and used by Module
* Federation.
*
* @param remotes - The remotes to map
* @param remoteEntryExt - The file extension of the remoteEntry file
* @param determineRemoteUrl - The function used to lookup the URL of the served remote
*/
export function mapRemotesForSSR(
remotes: Remotes,
remoteEntryExt: 'js' | 'mjs',
determineRemoteUrl: (remote: string) => string
): Record<string, string> {
const mappedRemotes = {};
for (const remote of remotes) {
if (Array.isArray(remote)) {
const [remoteName, remoteLocation] = remote;
const remoteLocationExt = extname(remoteLocation);
mappedRemotes[remoteName] = `${remoteName}@${
['.js', '.mjs'].includes(remoteLocationExt)
? remoteLocation
: `${
remoteLocation.endsWith('/')
? remoteLocation.slice(0, -1)
: remoteLocation
}/remoteEntry.${remoteEntryExt}`
}`;
} else if (typeof remote === 'string') {
mappedRemotes[remote] = `${remote}@${determineRemoteUrl(remote)}`;
}
}
return mappedRemotes;
}

View File

@ -16,6 +16,7 @@ const IGNORE_MATCHES_IN_PACKAGE = {
'@ngrx/router-store', '@ngrx/router-store',
'@ngrx/store', '@ngrx/store',
'@storybook/angular', '@storybook/angular',
'@module-federation/node',
'rxjs', 'rxjs',
'semver', 'semver',
// installed dynamically by the library generator // installed dynamically by the library generator