327 lines
8.7 KiB
TypeScript
327 lines
8.7 KiB
TypeScript
import {
|
|
getNpmPackageSharedConfig,
|
|
SharedLibraryConfig,
|
|
sharePackages,
|
|
shareWorkspaceLibraries,
|
|
} from './mf-webpack';
|
|
import {
|
|
createProjectGraphAsync,
|
|
ProjectGraph,
|
|
readCachedProjectGraph,
|
|
} from '@nrwl/devkit';
|
|
import {
|
|
getRootTsConfigPath,
|
|
readTsConfig,
|
|
} from '@nrwl/workspace/src/utilities/typescript';
|
|
import { ParsedCommandLine } from 'typescript';
|
|
import { readRootPackageJson } from './utils';
|
|
import { extname, join } from 'path';
|
|
import { readCachedProjectConfiguration } from 'nx/src/project-graph/project-graph';
|
|
import ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
|
|
|
|
export type MFRemotes = string[] | [remoteName: string, remoteUrl: string][];
|
|
|
|
type SharedFunction = (
|
|
libraryName: string,
|
|
sharedConfig: SharedLibraryConfig
|
|
) => SharedLibraryConfig | false;
|
|
type AdditionalSharedConfig = Array<
|
|
| string
|
|
| [libraryName: string, sharedConfig: SharedLibraryConfig]
|
|
| { libraryName: string; sharedConfig: SharedLibraryConfig }
|
|
>;
|
|
|
|
export interface MFConfig {
|
|
name: string;
|
|
remotes?: MFRemotes;
|
|
exposes?: Record<string, string>;
|
|
shared?: SharedFunction;
|
|
additionalShared?: AdditionalSharedConfig;
|
|
}
|
|
|
|
function collectDependencies(
|
|
projectGraph: ProjectGraph,
|
|
name: string,
|
|
dependencies = {
|
|
workspaceLibraries: new Set<string>(),
|
|
npmPackages: new Set<string>(),
|
|
},
|
|
seen: Set<string> = new Set()
|
|
): {
|
|
workspaceLibraries: Set<string>;
|
|
npmPackages: Set<string>;
|
|
} {
|
|
if (seen.has(name)) {
|
|
return dependencies;
|
|
}
|
|
seen.add(name);
|
|
|
|
(projectGraph.dependencies[name] ?? []).forEach((dependency) => {
|
|
if (dependency.target.startsWith('npm:')) {
|
|
dependencies.npmPackages.add(dependency.target.replace('npm:', ''));
|
|
} else {
|
|
dependencies.workspaceLibraries.add(dependency.target);
|
|
collectDependencies(projectGraph, dependency.target, dependencies, seen);
|
|
}
|
|
});
|
|
|
|
return dependencies;
|
|
}
|
|
|
|
function mapWorkspaceLibrariesToTsConfigImport(workspaceLibraries: string[]) {
|
|
const { nodes: projectGraphNodes } = readCachedProjectGraph();
|
|
|
|
const tsConfigPath = process.env.NX_TSCONFIG_PATH ?? getRootTsConfigPath();
|
|
const tsConfig: ParsedCommandLine = readTsConfig(tsConfigPath);
|
|
|
|
const tsconfigPathAliases: Record<string, string[]> = tsConfig.options?.paths;
|
|
|
|
if (!tsconfigPathAliases) {
|
|
return workspaceLibraries;
|
|
}
|
|
|
|
const mappedLibraries = [];
|
|
for (const lib of workspaceLibraries) {
|
|
const sourceRoot = projectGraphNodes[lib].data.sourceRoot;
|
|
let found = false;
|
|
|
|
for (const [key, value] of Object.entries(tsconfigPathAliases)) {
|
|
if (value.find((p) => p.startsWith(sourceRoot))) {
|
|
mappedLibraries.push(key);
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
mappedLibraries.push(lib);
|
|
}
|
|
}
|
|
|
|
return mappedLibraries;
|
|
}
|
|
|
|
async function getDependentPackagesForProject(
|
|
projectGraph: ProjectGraph,
|
|
name: string
|
|
) {
|
|
const { npmPackages, workspaceLibraries } = collectDependencies(
|
|
projectGraph,
|
|
name
|
|
);
|
|
|
|
return {
|
|
workspaceLibraries: mapWorkspaceLibrariesToTsConfigImport([
|
|
...workspaceLibraries,
|
|
]),
|
|
npmPackages: [...npmPackages],
|
|
};
|
|
}
|
|
|
|
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
|
|
}/remoteEntry.mjs`;
|
|
}
|
|
|
|
function mapRemotes(remotes: MFRemotes) {
|
|
const mappedRemotes = {};
|
|
|
|
for (const remote of remotes) {
|
|
if (Array.isArray(remote)) {
|
|
const [remoteName, remoteLocation] = remote;
|
|
const remoteLocationExt = extname(remoteLocation);
|
|
mappedRemotes[remoteName] = ['.js', '.mjs'].includes(remoteLocationExt)
|
|
? remoteLocation
|
|
: join(remoteLocation, 'remoteEntry.mjs');
|
|
} else if (typeof remote === 'string') {
|
|
mappedRemotes[remote] = determineRemoteUrl(remote);
|
|
}
|
|
}
|
|
|
|
return mappedRemotes;
|
|
}
|
|
|
|
function applySharedFunction(
|
|
sharedConfig: Record<string, SharedLibraryConfig>,
|
|
sharedFn: SharedFunction | undefined
|
|
): void {
|
|
if (!sharedFn) {
|
|
return;
|
|
}
|
|
|
|
for (const [libraryName, library] of Object.entries(sharedConfig)) {
|
|
const mappedDependency = sharedFn(libraryName, library);
|
|
if (mappedDependency === false) {
|
|
delete sharedConfig[libraryName];
|
|
continue;
|
|
} else if (!mappedDependency) {
|
|
continue;
|
|
}
|
|
|
|
sharedConfig[libraryName] = mappedDependency;
|
|
}
|
|
}
|
|
|
|
function addStringDependencyToSharedConfig(
|
|
sharedConfig: Record<string, SharedLibraryConfig>,
|
|
dependency: string,
|
|
projectGraph: ProjectGraph
|
|
): void {
|
|
if (projectGraph.nodes[dependency]) {
|
|
sharedConfig[dependency] = { requiredVersion: false };
|
|
} else if (projectGraph.externalNodes?.[`npm:${dependency}`]) {
|
|
const pkgJson = readRootPackageJson();
|
|
const config = getNpmPackageSharedConfig(
|
|
dependency,
|
|
pkgJson.dependencies?.[dependency] ??
|
|
pkgJson.devDependencies?.[dependency]
|
|
);
|
|
|
|
if (!config) {
|
|
return;
|
|
}
|
|
|
|
sharedConfig[dependency] = config;
|
|
} else {
|
|
throw new Error(
|
|
`The specified dependency "${dependency}" in the additionalShared configuration does not exist in the project graph. ` +
|
|
`Please check your additionalShared configuration and make sure you are including valid workspace projects or npm packages.`
|
|
);
|
|
}
|
|
}
|
|
|
|
function applyAdditionalShared(
|
|
sharedConfig: Record<string, SharedLibraryConfig>,
|
|
additionalShared: AdditionalSharedConfig | undefined,
|
|
projectGraph: ProjectGraph
|
|
): void {
|
|
if (!additionalShared) {
|
|
return;
|
|
}
|
|
|
|
for (const shared of additionalShared) {
|
|
if (typeof shared === 'string') {
|
|
addStringDependencyToSharedConfig(sharedConfig, shared, projectGraph);
|
|
} else if (Array.isArray(shared)) {
|
|
sharedConfig[shared[0]] = shared[1];
|
|
} else if (typeof shared === 'object') {
|
|
sharedConfig[shared.libraryName] = shared.sharedConfig;
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
sharedConfig[pkg] = {
|
|
...(sharedConfig[pkg] ?? {}),
|
|
eager: true,
|
|
};
|
|
}
|
|
}
|
|
|
|
export async function withModuleFederation(options: MFConfig) {
|
|
const DEFAULT_NPM_PACKAGES_TO_AVOID = ['zone.js', '@nrwl/angular/mf'];
|
|
|
|
let projectGraph: ProjectGraph<any>;
|
|
try {
|
|
projectGraph = readCachedProjectGraph();
|
|
} catch (e) {
|
|
projectGraph = await createProjectGraphAsync();
|
|
}
|
|
|
|
const dependencies = await getDependentPackagesForProject(
|
|
projectGraph,
|
|
options.name
|
|
);
|
|
const sharedLibraries = shareWorkspaceLibraries(
|
|
dependencies.workspaceLibraries
|
|
);
|
|
|
|
const npmPackages = sharePackages(
|
|
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);
|
|
|
|
return (config) => ({
|
|
...(config ?? {}),
|
|
output: {
|
|
...(config.output ?? {}),
|
|
uniqueName: options.name,
|
|
publicPath: 'auto',
|
|
},
|
|
optimization: {
|
|
...(config.optimization ?? {}),
|
|
runtimeChunk: false,
|
|
},
|
|
resolve: {
|
|
...(config.resolve ?? {}),
|
|
alias: {
|
|
...(config.resolve?.alias ?? {}),
|
|
...sharedLibraries.getAliases(),
|
|
},
|
|
},
|
|
experiments: {
|
|
...(config.experiments ?? {}),
|
|
outputModule: true,
|
|
},
|
|
plugins: [
|
|
...(config.plugins ?? []),
|
|
new ModuleFederationPlugin({
|
|
name: options.name,
|
|
filename: 'remoteEntry.mjs',
|
|
exposes: options.exposes,
|
|
remotes: mappedRemotes,
|
|
shared: {
|
|
...sharedDependencies,
|
|
},
|
|
library: {
|
|
type: 'module',
|
|
},
|
|
}),
|
|
sharedLibraries.getReplacementPlugin(),
|
|
],
|
|
});
|
|
}
|