UNPKG

@nx/module-federation

Version:

The Nx Plugin for Module Federation contains executors and utilities that support building applications using Module Federation.

247 lines (246 loc) • 11 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.shareWorkspaceLibraries = shareWorkspaceLibraries; exports.getNpmPackageSharedConfig = getNpmPackageSharedConfig; exports.sharePackages = sharePackages; exports.applySharedFunction = applySharedFunction; exports.applyAdditionalShared = applyAdditionalShared; const path_1 = require("path"); const package_json_1 = require("./package-json"); const typescript_1 = require("./typescript"); const secondary_entry_points_1 = require("./secondary-entry-points"); const devkit_1 = require("@nx/devkit"); const fs_1 = require("fs"); const core_1 = require("@rspack/core"); /** * 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 * @param bundler - The bundler to use for the replacement plugin */ function shareWorkspaceLibraries(workspaceLibs, tsConfigPath = process.env.NX_TSCONFIG_PATH ?? (0, typescript_1.getRootTsConfigPath)(), bundler = 'rspack') { if (!workspaceLibs) { return getEmptySharedLibrariesConfig(); } const tsconfigPathAliases = (0, typescript_1.readTsPathMappings)(tsConfigPath); if (!Object.keys(tsconfigPathAliases).length) { return getEmptySharedLibrariesConfig(); } // Nested projects must come first, sort them as such const sortedTsConfigPathAliases = {}; Object.keys(tsconfigPathAliases) .sort((a, b) => { return b.split('/').length - a.split('/').length; }) .forEach((key) => (sortedTsConfigPathAliases[key] = tsconfigPathAliases[key])); const pathMappings = []; for (const [key, paths] of Object.entries(sortedTsConfigPathAliases)) { 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 (0, secondary_entry_points_1.collectWorkspaceLibrarySecondaryEntryPoints)(library, sortedTsConfigPathAliases).forEach(({ name, path }) => pathMappings.push({ name, path, })); pathMappings.push({ name: key, path: (0, path_1.normalize)((0, path_1.join)(devkit_1.workspaceRoot, paths[0])), }); } const normalModuleReplacementPluginImpl = bundler === 'rspack' ? core_1.NormalModuleReplacementPlugin : require('webpack').NormalModuleReplacementPlugin; return { getAliases: () => pathMappings.reduce((aliases, library) => ({ ...aliases, // If the library path ends in a wildcard, remove it as webpack/rspack can't handle this in resolve.alias // e.g. path/to/my/lib/* -> path/to/my/lib [library.name]: library.path.replace(/\/\*$/, ''), }), {}), getLibraries: (projectRoot, eager) => { let pkgJson = null; if (projectRoot && (0, fs_1.existsSync)((0, devkit_1.joinPathFragments)(devkit_1.workspaceRoot, projectRoot, 'package.json'))) { pkgJson = (0, devkit_1.readJsonFile)((0, devkit_1.joinPathFragments)(devkit_1.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 ? (0, path_1.join)(workspaceLib.root, 'package.json') : null; if (libPackageJsonPath && (0, fs_1.existsSync)(libPackageJsonPath)) { pkgJson = (0, devkit_1.readJsonFile)(libPackageJsonPath); if (pkgJson) { version = pkgJson.version; } } } return { ...libraries, [library.name]: { ...(version ? { requiredVersion: version, singleton: true, } : { requiredVersion: false }), eager, }, }; }, {}); }, getReplacementPlugin: () => new normalModuleReplacementPluginImpl(/./, (req) => { if (!req.request.startsWith('.')) { return; } const from = req.context; const to = (0, path_1.normalize)((0, path_1.join)(req.context, req.request)); for (const library of pathMappings) { const libFolder = (0, path_1.normalize)((0, path_1.dirname)(library.path)); if (!from.startsWith(libFolder) && to.startsWith(libFolder)) { const newReq = library.name.endsWith('/*') ? /** * req usually is in the form of "../../../path/to/file" * library.path is usually in the form of "/Users/username/path/to/Workspace/path/to/library" * * When a wildcard is used in the TS path mappings, we want to get everything after the import to * re-route the request correctly inline with the webpack/rspack resolve.alias */ (0, path_1.join)(library.name, req.request.split(library.path.replace(devkit_1.workspaceRoot, '').replace('/*', ''))[1]) : library.name; req.request = newReq; } } }), }; } /** * 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 */ function getNpmPackageSharedConfig(pkgName, version) { if (!version) { devkit_1.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 */ function sharePackages(packages) { const pkgJson = (0, package_json_1.readRootPackageJson)(); const allPackages = []; packages.forEach((pkg) => { const pkgVersion = pkgJson.dependencies?.[pkg] ?? pkgJson.devDependencies?.[pkg]; allPackages.push({ name: pkg, version: pkgVersion }); (0, secondary_entry_points_1.collectPackageSecondaryEntryPoints)(pkg, pkgVersion, allPackages); }); return allPackages.reduce((shared, pkg) => { const config = getNpmPackageSharedConfig(pkg.name, pkg.version); if (config) { shared[pkg.name] = config; } return shared; }, {}); } /** * 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 */ function applySharedFunction(sharedConfig, sharedFn) { 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 */ function applyAdditionalShared(sharedConfig, additionalShared, projectGraph) { 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, dependency, projectGraph) { if (projectGraph.nodes[dependency]) { sharedConfig[dependency] = { requiredVersion: false }; } else if (projectGraph.externalNodes?.[`npm:${dependency}`]) { const pkgJson = (0, package_json_1.readRootPackageJson)(); const config = getNpmPackageSharedConfig(dependency, pkgJson.dependencies?.[dependency] ?? pkgJson.devDependencies?.[dependency]); if (!config) { return; } sharedConfig[dependency] = config; } else { const pkgJsonPath = require.resolve(`${dependency}/package.json`); if (!pkgJsonPath) { throw new Error(`Could not find package ${dependency} when applying it as a shared package. Are you sure it has been installed?`); } const pkgJson = (0, devkit_1.readJsonFile)(pkgJsonPath); const config = getNpmPackageSharedConfig(dependency, pkgJson.version); } } function getEmptySharedLibrariesConfig(bundler = 'rspack') { const normalModuleReplacementPluginImpl = bundler === 'rspack' ? core_1.NormalModuleReplacementPlugin : require('webpack').NormalModuleReplacementPlugin; return { getAliases: () => ({}), getLibraries: () => ({}), getReplacementPlugin: () => new normalModuleReplacementPluginImpl(/./, () => { }), }; }