import type { SharedLibraryConfig, SharedWorkspaceLibraryConfig, WorkspaceLibrary, } from './models'; import { AdditionalSharedConfig, SharedFunction } from './models'; import { dirname, join, normalize } from 'path'; import { readRootPackageJson } from './package-json'; import { readTsPathMappings, getRootTsConfigPath } from './typescript'; import { collectPackageSecondaryEntryPoints, collectWorkspaceLibrarySecondaryEntryPoints, } from './secondary-entry-points'; import { type ProjectGraph, workspaceRoot, logger, readJsonFile, joinPathFragments, } from '@nx/devkit'; import { existsSync } from 'fs'; import type { PackageJson } from 'nx/src/utils/package-json'; /** * Build an object of functions to be used with the ModuleFederationPlugin to * share Nx Workspace Libraries between Hosts and Remotes. * * @param workspaceLibs - The Nx Workspace Libraries to share * @param tsConfigPath - The path to TS Config File that contains the Path Mappings for the Libraries */ export function shareWorkspaceLibraries( workspaceLibs: WorkspaceLibrary[], tsConfigPath = process.env.NX_TSCONFIG_PATH ?? getRootTsConfigPath() ): SharedWorkspaceLibraryConfig { if (!workspaceLibs) { return getEmptySharedLibrariesConfig(); } const tsconfigPathAliases = readTsPathMappings(tsConfigPath); if (!Object.keys(tsconfigPathAliases).length) { return getEmptySharedLibrariesConfig(); } const pathMappings: { name: string; path: string }[] = []; for (const [key, paths] of Object.entries(tsconfigPathAliases)) { const library = workspaceLibs.find((lib) => lib.importKey === key); if (!library) { continue; } // This is for Angular Projects that use ng-package.json // It will do nothing for React Projects collectWorkspaceLibrarySecondaryEntryPoints( library, tsconfigPathAliases ).forEach(({ name, path }) => pathMappings.push({ name, path, }) ); pathMappings.push({ name: key, path: normalize(join(workspaceRoot, paths[0])), }); } const webpack = require('webpack'); return { getAliases: () => pathMappings.reduce( (aliases, library) => ({ ...aliases, [library.name]: library.path }), {} ), getLibraries: ( projectRoot: string, eager?: boolean ): Record => { let pkgJson: PackageJson = null; if ( projectRoot && existsSync( joinPathFragments(workspaceRoot, projectRoot, 'package.json') ) ) { pkgJson = readJsonFile( joinPathFragments(workspaceRoot, projectRoot, 'package.json') ); } return pathMappings.reduce((libraries, library) => { // Check to see if the library version is declared in the app's package.json let version = pkgJson?.dependencies?.[library.name]; if (!version && workspaceLibs.length > 0) { const workspaceLib = workspaceLibs.find( (lib) => lib.importKey === library.name ); const libPackageJsonPath = workspaceLib ? join(workspaceLib.root, 'package.json') : null; if (libPackageJsonPath && existsSync(libPackageJsonPath)) { pkgJson = readJsonFile(libPackageJsonPath); if (pkgJson) { version = pkgJson.version; } } } return { ...libraries, [library.name]: { ...(version ? { requiredVersion: version, singleton: true, } : { requiredVersion: false }), eager, }, }; }, {} as Record); }, getReplacementPlugin: () => new webpack.NormalModuleReplacementPlugin(/./, (req) => { if (!req.request.startsWith('.')) { return; } const from = req.context; const to = normalize(join(req.context, req.request)); for (const library of pathMappings) { const libFolder = normalize(dirname(library.path)); if (!from.startsWith(libFolder) && to.startsWith(libFolder)) { req.request = library.name; } } }), }; } /** * Build the Module Federation Share Config for a specific package and the * specified version of that package. * @param pkgName - Name of the package to share * @param version - Version of the package to require by other apps in the Module Federation setup */ 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; } return { singleton: true, strictVersion: true, requiredVersion: version }; } /** * Create a dictionary of packages and their Module Federation Shared Config * from an array of package names. * * Lookup the versions of the packages from the root package.json file in the * workspace. * @param packages - Array of package names as strings */ export function sharePackages( packages: string[] ): Record { const pkgJson = readRootPackageJson(); const allPackages: { name: string; version: string }[] = []; packages.forEach((pkg) => { const pkgVersion = pkgJson.dependencies?.[pkg] ?? pkgJson.devDependencies?.[pkg]; allPackages.push({ name: pkg, version: pkgVersion }); collectPackageSecondaryEntryPoints(pkg, pkgVersion, allPackages); }); return allPackages.reduce((shared, pkg) => { const config = getNpmPackageSharedConfig(pkg.name, pkg.version); if (config) { shared[pkg.name] = config; } return shared; }, {} as Record); } /** * Apply a custom function provided by the user that will modify the Shared Config * of the dependencies for the Module Federation build. * * @param sharedConfig - The original Shared Config to be modified * @param sharedFn - The custom function to run */ export 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; } } /** * Add additional dependencies to the shared package that may not have been * discovered by the project graph. * * This can be useful for applications that use a Dependency Injection system * that expects certain Singleton values to be present in the shared injection * hierarchy. * * @param sharedConfig - The original Shared Config * @param additionalShared - The additional dependencies to add * @param projectGraph - The Nx project graph */ export 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; } } } function addStringDependencyToSharedConfig( sharedConfig: Record, 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 getEmptySharedLibrariesConfig() { const webpack = require('webpack'); return { getAliases: () => ({}), getLibraries: () => ({}), getReplacementPlugin: () => new webpack.NormalModuleReplacementPlugin(/./, () => {}), }; }