Skip to content

Commit

Permalink
feat(react): add support for passing additional shared dependencies i…
Browse files Browse the repository at this point in the history
…n the module federation config (#10169)
  • Loading branch information
leosvelperez committed May 9, 2022
1 parent eb4243b commit 425adf1
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 65 deletions.
29 changes: 19 additions & 10 deletions packages/react/src/module-federation/models.ts
@@ -1,21 +1,30 @@
export type ModuleFederationLibrary = { type: string; name: string };

export type Remotes = string[] | [remoteName: string, remoteUrl: string][];

export interface SharedLibraryConfig {
singleton: boolean;
strictVersion: boolean;
requiredVersion: string;
eager: boolean;
singleton?: boolean;
strictVersion?: boolean;
requiredVersion?: false | string;
eager?: boolean;
}

export type ModuleFederationLibrary = { type: string; name: string };
export type SharedFunction = (
libraryName: string,
sharedConfig: SharedLibraryConfig
) => undefined | false | SharedLibraryConfig;

export type Remotes = string[] | [remoteName: string, remoteUrl: string][];
export type AdditionalSharedConfig = Array<
| string
| [libraryName: string, sharedConfig: SharedLibraryConfig]
| { libraryName: string; sharedConfig: SharedLibraryConfig }
>;

export interface ModuleFederationConfig {
name: string;
remotes?: string[];
library?: ModuleFederationLibrary;
exposes?: Record<string, string>;
shared?: (
libraryName: string,
library: SharedLibraryConfig
) => undefined | false | SharedLibraryConfig;
shared?: SharedFunction;
additionalShared?: AdditionalSharedConfig;
}
16 changes: 16 additions & 0 deletions packages/react/src/module-federation/package-json.ts
@@ -0,0 +1,16 @@
import { joinPathFragments, readJsonFile, workspaceRoot } from '@nrwl/devkit';
import { existsSync } from 'fs';

export function readRootPackageJson(): {
dependencies?: { [key: string]: string };
devDependencies?: { [key: string]: string };
} {
const pkgJsonPath = joinPathFragments(workspaceRoot, 'package.json');
if (!existsSync(pkgJsonPath)) {
throw new Error(
'NX MFE: Could not find root package.json to determine dependency versions.'
);
}

return readJsonFile(pkgJsonPath);
}
62 changes: 31 additions & 31 deletions packages/react/src/module-federation/webpack-utils.ts
@@ -1,13 +1,14 @@
import { existsSync, readFileSync } from 'fs';
import { existsSync } from 'fs';
import { NormalModuleReplacementPlugin } from 'webpack';
import { joinPathFragments, logger, workspaceRoot } from '@nrwl/devkit';
import { logger, workspaceRoot } from '@nrwl/devkit';
import { dirname, join, normalize } from 'path';
import { ParsedCommandLine } from 'typescript';
import {
getRootTsConfigPath,
readTsConfig,
} from '@nrwl/workspace/src/utilities/typescript';
import { SharedLibraryConfig } from './models';
import { readRootPackageJson } from './package-json';

export function shareWorkspaceLibraries(
libraries: string[],
Expand All @@ -25,7 +26,7 @@ export function shareWorkspaceLibraries(
if (!tsconfigPathAliases) {
return {
getAliases: () => [],
getLibraries: () => {},
getLibraries: () => ({}),
getReplacementPlugin: () =>
new NormalModuleReplacementPlugin(/./, () => {}),
};
Expand Down Expand Up @@ -54,7 +55,7 @@ export function shareWorkspaceLibraries(
...libraries,
[library.name]: { requiredVersion: false, eager },
}),
{}
{} as Record<string, SharedLibraryConfig>
),
getReplacementPlugin: () =>
new NormalModuleReplacementPlugin(/./, (req) => {
Expand All @@ -75,38 +76,37 @@ export function shareWorkspaceLibraries(
};
}

export function sharePackages(
packages: string[]
): Record<string, SharedLibraryConfig> {
const pkgJsonPath = joinPathFragments(workspaceRoot, 'package.json');
if (!existsSync(pkgJsonPath)) {
throw new Error(
'NX: Could not find root package.json to determine dependency versions.'
export function getNpmPackageSharedConfig(
pkgName: string,
version: string
): SharedLibraryConfig | undefined {
if (!version) {
logger.warn(
`Could not find a version for "${pkgName}" in the root "package.json" ` +
'when collecting shared packages for the Module Federation setup. ' +
'The package will not be shared.'
);

return undefined;
}

const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
return { singleton: true, strictVersion: true, requiredVersion: version };
}

return packages.reduce((shared, pkgName) => {
const version =
pkgJson.dependencies?.[pkgName] ?? pkgJson.devDependencies?.[pkgName];
if (!version) {
logger.warn(
`Could not find a version for "${pkgName}" in the root "package.json" ` +
'when collecting shared packages for the Module Federation setup. ' +
'The package will not be shared.'
);
export function sharePackages(
packages: string[]
): Record<string, SharedLibraryConfig> {
const pkgJson = readRootPackageJson();

return shared;
return packages.reduce((shared, pkg) => {
const config = getNpmPackageSharedConfig(
pkg,
pkgJson.dependencies?.[pkg] ?? pkgJson.devDependencies?.[pkg]
);
if (config) {
shared[pkg] = config;
}

return {
...shared,
[pkgName]: {
singleton: true,
strictVersion: true,
requiredVersion: version,
},
};
}, {});
return shared;
}, {} as Record<string, SharedLibraryConfig>);
}
127 changes: 103 additions & 24 deletions packages/react/src/module-federation/with-module-federation.ts
@@ -1,4 +1,8 @@
import { sharePackages, shareWorkspaceLibraries } from './webpack-utils';
import {
getNpmPackageSharedConfig,
sharePackages,
shareWorkspaceLibraries,
} from './webpack-utils';
import {
createProjectGraphAsync,
ProjectGraph,
Expand All @@ -12,8 +16,15 @@ import {
} from '@nrwl/workspace/src/utilities/typescript';
import { ParsedCommandLine } from 'typescript';
import { readWorkspaceJson } from 'nx/src/project-graph/file-utils';
import { ModuleFederationConfig, Remotes } from './models';
import {
AdditionalSharedConfig,
ModuleFederationConfig,
Remotes,
SharedFunction,
SharedLibraryConfig,
} from './models';
import ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
import { readRootPackageJson } from './package-json';

function collectDependencies(
projectGraph: ProjectGraph,
Expand Down Expand Up @@ -79,14 +90,10 @@ function mapWorkspaceLibrariesToTsConfigImport(workspaceLibraries: string[]) {
return mappedLibraries;
}

async function getDependentPackagesForProject(name: string) {
let projectGraph: ProjectGraph<any>;
try {
projectGraph = readCachedProjectGraph();
} catch (e) {
projectGraph = await createProjectGraphAsync();
}

async function getDependentPackagesForProject(
projectGraph: ProjectGraph,
name: string
) {
const { npmPackages, workspaceLibraries } = collectDependencies(
projectGraph,
name
Expand Down Expand Up @@ -140,6 +147,75 @@ function mapRemotes(remotes: Remotes) {
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?.[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;
}
}
}

export async function withModuleFederation(options: ModuleFederationConfig) {
const reactWebpackConfig = require('../../plugins/webpack');
const ws = readWorkspaceJson();
Expand All @@ -151,7 +227,17 @@ export async function withModuleFederation(options: ModuleFederationConfig) {
);
}

const dependencies = await getDependentPackagesForProject(options.name);
let projectGraph: ProjectGraph<any>;
try {
projectGraph = readCachedProjectGraph();
} catch (e) {
projectGraph = await createProjectGraphAsync();
}

const dependencies = await getDependentPackagesForProject(
projectGraph,
options.name
);
const sharedLibraries = shareWorkspaceLibraries(
dependencies.workspaceLibraries
);
Expand All @@ -163,19 +249,12 @@ export async function withModuleFederation(options: ModuleFederationConfig) {
...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;
}
}
applySharedFunction(sharedDependencies, options.shared);
applyAdditionalShared(
sharedDependencies,
options.additionalShared,
projectGraph
);

return (config) => {
config = reactWebpackConfig(config);
Expand Down

0 comments on commit 425adf1

Please sign in to comment.