258 lines
6.9 KiB
TypeScript
258 lines
6.9 KiB
TypeScript
import {
|
|
SharedLibraryConfig,
|
|
sharePackages,
|
|
shareWorkspaceLibraries,
|
|
} from './webpack-utils';
|
|
import {
|
|
createProjectGraphAsync,
|
|
ProjectGraph,
|
|
readCachedProjectGraph,
|
|
workspaceRoot,
|
|
Workspaces,
|
|
} from '@nrwl/devkit';
|
|
import {
|
|
getRootTsConfigPath,
|
|
readTsConfig,
|
|
} from '@nrwl/workspace/src/utilities/typescript';
|
|
import { ParsedCommandLine } from 'typescript';
|
|
import { readWorkspaceJson } from 'nx/src/project-graph/file-utils';
|
|
import ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
|
|
|
|
export type ModuleFederationLibrary = { type: string; name: string };
|
|
|
|
export type Remotes = string[] | [remoteName: string, remoteUrl: string][];
|
|
|
|
export interface ModuleFederationConfig {
|
|
name: string;
|
|
remotes?: string[];
|
|
library?: ModuleFederationLibrary;
|
|
exposes?: Record<string, string>;
|
|
shared?: (
|
|
libraryName: string,
|
|
library: SharedLibraryConfig
|
|
) => undefined | false | SharedLibraryConfig;
|
|
}
|
|
|
|
function recursivelyResolveWorkspaceDependents(
|
|
projectGraph: ProjectGraph<any>,
|
|
target: string,
|
|
seenTargets: Set<string> = new Set()
|
|
) {
|
|
if (seenTargets.has(target)) {
|
|
return [];
|
|
}
|
|
let dependencies = [target];
|
|
seenTargets.add(target);
|
|
|
|
const workspaceDependencies = (
|
|
projectGraph.dependencies[target] ?? []
|
|
).filter((dep) => !dep.target.startsWith('npm:'));
|
|
if (workspaceDependencies.length > 0) {
|
|
for (const dep of workspaceDependencies) {
|
|
dependencies = [
|
|
...dependencies,
|
|
...recursivelyResolveWorkspaceDependents(
|
|
projectGraph,
|
|
dep.target,
|
|
seenTargets
|
|
),
|
|
];
|
|
}
|
|
}
|
|
|
|
return dependencies;
|
|
}
|
|
|
|
function mapWorkspaceLibrariesToTsConfigImport(workspaceLibraries: string[]) {
|
|
const { projects } = new Workspaces(
|
|
workspaceRoot
|
|
).readWorkspaceConfiguration();
|
|
|
|
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 = projects[lib].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(name: string) {
|
|
let projectGraph: ProjectGraph;
|
|
|
|
try {
|
|
projectGraph = readCachedProjectGraph();
|
|
} catch (e) {
|
|
projectGraph = await createProjectGraphAsync();
|
|
}
|
|
|
|
const deps = projectGraph.dependencies[name].reduce(
|
|
(dependencies, dependency) => {
|
|
const workspaceLibraries = new Set(dependencies.workspaceLibraries);
|
|
const npmPackages = new Set(dependencies.npmPackages);
|
|
|
|
if (dependency.target.startsWith('npm:')) {
|
|
npmPackages.add(dependency.target.replace('npm:', ''));
|
|
} else {
|
|
workspaceLibraries.add(dependency.target);
|
|
}
|
|
|
|
return {
|
|
workspaceLibraries: [...workspaceLibraries],
|
|
npmPackages: [...npmPackages],
|
|
};
|
|
},
|
|
{ workspaceLibraries: [], npmPackages: [] }
|
|
);
|
|
const seenWorkspaceLibraries = new Set<string>();
|
|
deps.workspaceLibraries = deps.workspaceLibraries.reduce(
|
|
(workspaceLibraryDeps, workspaceLibrary) => [
|
|
...workspaceLibraryDeps,
|
|
...recursivelyResolveWorkspaceDependents(
|
|
projectGraph,
|
|
workspaceLibrary,
|
|
seenWorkspaceLibraries
|
|
),
|
|
],
|
|
[]
|
|
);
|
|
|
|
deps.workspaceLibraries = mapWorkspaceLibrariesToTsConfigImport(
|
|
deps.workspaceLibraries
|
|
);
|
|
return deps;
|
|
}
|
|
|
|
function determineRemoteUrl(remote: string) {
|
|
const workspace = readWorkspaceJson();
|
|
const serveTarget = workspace.projects[remote]?.targets?.serve;
|
|
|
|
if (!serveTarget) {
|
|
throw new Error(
|
|
`Cannot automatically determine URL of remote (${remote}). Looked for property "host" 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', '//localhost:4201']]\``
|
|
);
|
|
}
|
|
|
|
const host = serveTarget.options?.host ?? '//localhost';
|
|
const port = serveTarget.options?.port ?? 4201;
|
|
return `${remote}@${
|
|
host.endsWith('/') ? host.slice(0, -1) : host
|
|
}:${port}/remoteEntry.js`;
|
|
}
|
|
|
|
function mapRemotes(remotes: Remotes) {
|
|
const mappedRemotes = {};
|
|
|
|
for (const remote of remotes) {
|
|
if (Array.isArray(remote)) {
|
|
let [remoteName, remoteLocation] = remote;
|
|
if (!remoteLocation.includes('@')) {
|
|
remoteLocation = `${remoteName}@${remoteLocation}`;
|
|
}
|
|
if (!remoteLocation.match(/remoteEntry\.(js|mjs)$/)) {
|
|
remoteLocation = `${
|
|
remoteLocation.endsWith('/')
|
|
? remoteLocation.slice(0, -1)
|
|
: remoteLocation
|
|
}/remoteEntry.js`;
|
|
}
|
|
mappedRemotes[remoteName] = remoteLocation;
|
|
} else if (typeof remote === 'string') {
|
|
mappedRemotes[remote] = determineRemoteUrl(remote);
|
|
}
|
|
}
|
|
|
|
return mappedRemotes;
|
|
}
|
|
|
|
export async function withModuleFederation(options: ModuleFederationConfig) {
|
|
const reactWebpackConfig = require('../../plugins/webpack');
|
|
const ws = readWorkspaceJson();
|
|
const project = ws.projects[options.name];
|
|
|
|
if (!project) {
|
|
throw Error(
|
|
`Cannot find project "${options.name}". Check that the name is correct in module-federation.config.js`
|
|
);
|
|
}
|
|
|
|
const dependencies = await getDependentPackagesForProject(options.name);
|
|
const sharedLibraries = shareWorkspaceLibraries(
|
|
dependencies.workspaceLibraries
|
|
);
|
|
|
|
const npmPackages = sharePackages(dependencies.npmPackages);
|
|
|
|
const sharedDependencies = {
|
|
...sharedLibraries.getLibraries(),
|
|
...npmPackages,
|
|
};
|
|
|
|
if (options.shared) {
|
|
for (const [libraryName, library] of Object.entries(sharedDependencies)) {
|
|
const mappedDependency = options.shared(libraryName, library);
|
|
if (mappedDependency === false) {
|
|
delete sharedDependencies[libraryName];
|
|
continue;
|
|
} else if (!mappedDependency) {
|
|
continue;
|
|
}
|
|
|
|
sharedDependencies[libraryName] = mappedDependency;
|
|
}
|
|
}
|
|
|
|
return (config) => {
|
|
config = reactWebpackConfig(config);
|
|
config.output.uniqueName = options.name;
|
|
config.output.publicPath = 'auto';
|
|
|
|
config.optimization = {
|
|
runtimeChunk: false,
|
|
minimize: false,
|
|
};
|
|
|
|
const mappedRemotes =
|
|
!options.remotes || options.remotes.length === 0
|
|
? {}
|
|
: mapRemotes(options.remotes);
|
|
|
|
config.plugins.push(
|
|
new ModuleFederationPlugin({
|
|
name: options.name,
|
|
filename: 'remoteEntry.js',
|
|
exposes: options.exposes,
|
|
remotes: mappedRemotes,
|
|
shared: {
|
|
...sharedDependencies,
|
|
},
|
|
}),
|
|
sharedLibraries.getReplacementPlugin()
|
|
);
|
|
|
|
return config;
|
|
};
|
|
}
|