diff --git a/packages/react/src/module-federation/models.ts b/packages/react/src/module-federation/models.ts index 4f84cef436962..ca83aa6b8a67f 100644 --- a/packages/react/src/module-federation/models.ts +++ b/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; - shared?: ( - libraryName: string, - library: SharedLibraryConfig - ) => undefined | false | SharedLibraryConfig; + shared?: SharedFunction; + additionalShared?: AdditionalSharedConfig; } diff --git a/packages/react/src/module-federation/package-json.ts b/packages/react/src/module-federation/package-json.ts new file mode 100644 index 0000000000000..b0fbff11dea14 --- /dev/null +++ b/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); +} diff --git a/packages/react/src/module-federation/webpack-utils.ts b/packages/react/src/module-federation/webpack-utils.ts index 28519174aac9c..84e887c7113fb 100644 --- a/packages/react/src/module-federation/webpack-utils.ts +++ b/packages/react/src/module-federation/webpack-utils.ts @@ -1,6 +1,6 @@ -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 { @@ -8,6 +8,7 @@ import { readTsConfig, } from '@nrwl/workspace/src/utilities/typescript'; import { SharedLibraryConfig } from './models'; +import { readRootPackageJson } from './package-json'; export function shareWorkspaceLibraries( libraries: string[], @@ -25,7 +26,7 @@ export function shareWorkspaceLibraries( if (!tsconfigPathAliases) { return { getAliases: () => [], - getLibraries: () => {}, + getLibraries: () => ({}), getReplacementPlugin: () => new NormalModuleReplacementPlugin(/./, () => {}), }; @@ -54,7 +55,7 @@ export function shareWorkspaceLibraries( ...libraries, [library.name]: { requiredVersion: false, eager }, }), - {} + {} as Record ), getReplacementPlugin: () => new NormalModuleReplacementPlugin(/./, (req) => { @@ -75,38 +76,37 @@ export function shareWorkspaceLibraries( }; } -export function sharePackages( - packages: string[] -): Record { - 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 { + 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); } diff --git a/packages/react/src/module-federation/with-module-federation.ts b/packages/react/src/module-federation/with-module-federation.ts index ea5ae1e2533da..376ab51e0ba80 100644 --- a/packages/react/src/module-federation/with-module-federation.ts +++ b/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, @@ -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, @@ -79,14 +90,10 @@ function mapWorkspaceLibrariesToTsConfigImport(workspaceLibraries: string[]) { return mappedLibraries; } -async function getDependentPackagesForProject(name: string) { - let projectGraph: ProjectGraph; - try { - projectGraph = readCachedProjectGraph(); - } catch (e) { - projectGraph = await createProjectGraphAsync(); - } - +async function getDependentPackagesForProject( + projectGraph: ProjectGraph, + name: string +) { const { npmPackages, workspaceLibraries } = collectDependencies( projectGraph, name @@ -140,6 +147,75 @@ function mapRemotes(remotes: Remotes) { return mappedRemotes; } +function applySharedFunction( + sharedConfig: Record, + 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, + 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, + 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(); @@ -151,7 +227,17 @@ export async function withModuleFederation(options: ModuleFederationConfig) { ); } - const dependencies = await getDependentPackagesForProject(options.name); + let projectGraph: ProjectGraph; + try { + projectGraph = readCachedProjectGraph(); + } catch (e) { + projectGraph = await createProjectGraphAsync(); + } + + const dependencies = await getDependentPackagesForProject( + projectGraph, + options.name + ); const sharedLibraries = shareWorkspaceLibraries( dependencies.workspaceLibraries ); @@ -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);