From e212cb9fe143759b6e9afd3a87ba8312251bc3ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leosvel=20P=C3=A9rez=20Espinosa?= Date: Thu, 5 May 2022 09:14:56 +0100 Subject: [PATCH] fix(angular): fix collecting secondary entry points for module federation builds (#10129) --- .../with-module-federation.spec.ts.snap | 2 +- .../angular/src/utils/mfe/mfe-webpack.spec.ts | 94 +++++++++--- packages/angular/src/utils/mfe/mfe-webpack.ts | 134 ++++++++++++------ .../utils/mfe/with-module-federation.spec.ts | 52 +++---- 4 files changed, 176 insertions(+), 106 deletions(-) diff --git a/packages/angular/src/utils/mfe/__snapshots__/with-module-federation.spec.ts.snap b/packages/angular/src/utils/mfe/__snapshots__/with-module-federation.spec.ts.snap index 6362138afff68..0b00a21561e76 100644 --- a/packages/angular/src/utils/mfe/__snapshots__/with-module-federation.spec.ts.snap +++ b/packages/angular/src/utils/mfe/__snapshots__/with-module-federation.spec.ts.snap @@ -24,7 +24,7 @@ Array [ "requiredVersion": false, }, "lodash": Object { - "requiredVersion": undefined, + "requiredVersion": "~4.17.20", "singleton": true, "strictVersion": true, }, diff --git a/packages/angular/src/utils/mfe/mfe-webpack.spec.ts b/packages/angular/src/utils/mfe/mfe-webpack.spec.ts index 6c28a24619ca4..4ac20e9b686c1 100644 --- a/packages/angular/src/utils/mfe/mfe-webpack.spec.ts +++ b/packages/angular/src/utils/mfe/mfe-webpack.spec.ts @@ -3,6 +3,7 @@ jest.mock('@nrwl/workspace/src/utilities/typescript'); import * as fs from 'fs'; import * as tsUtils from '@nrwl/workspace/src/utilities/typescript'; +import * as devkit from '@nrwl/devkit'; import { sharePackages, shareWorkspaceLibraries } from './mfe-webpack'; describe('MFE Webpack Utils', () => { @@ -86,16 +87,14 @@ describe('MFE Webpack Utils', () => { it('should correctly map the shared packages to objects', () => { // ARRANGE (fs.existsSync as jest.Mock).mockReturnValue(true); - (fs.readFileSync as jest.Mock).mockImplementation((file) => - JSON.stringify({ - name: file.replace(/\\/g, '/').replace(/^.*node_modules[/]/, ''), - dependencies: { - '@angular/core': '~13.2.0', - '@angular/common': '~13.2.0', - rxjs: '~7.4.0', - }, - }) - ); + jest.spyOn(devkit, 'readJsonFile').mockImplementation((file) => ({ + name: file.replace(/\\/g, '/').replace(/^.*node_modules[/]/, ''), + dependencies: { + '@angular/core': '~13.2.0', + '@angular/common': '~13.2.0', + rxjs: '~7.4.0', + }, + })); (fs.readdirSync as jest.Mock).mockReturnValue([]); // ACT @@ -181,6 +180,57 @@ describe('MFE Webpack Utils', () => { }, }); }); + + it('should not collect a folder with a package.json when cannot be required', () => { + // ARRANGE + (fs.existsSync as jest.Mock).mockReturnValue(true); + jest.spyOn(devkit, 'readJsonFile').mockImplementation((file) => { + // the "schematics" folder is not an entry point + if (file.endsWith('@angular/core/schematics/package.json')) { + return {}; + } + + return { + name: file + .replace(/\\/g, '/') + .replace(/^.*node_modules[/]/, '') + .replace('/package.json', ''), + dependencies: { '@angular/core': '~13.2.0' }, + }; + }); + (fs.readdirSync as jest.Mock).mockImplementation( + (directoryPath: string) => { + const packages = { + '@angular/core': ['testing', 'schematics'], + }; + + for (const key of Object.keys(packages)) { + if (directoryPath.endsWith(key)) { + return packages[key]; + } + } + return []; + } + ); + (fs.lstatSync as jest.Mock).mockReturnValue({ isDirectory: () => true }); + + // ACT + const packages = sharePackages(['@angular/core']); + + // ASSERT + expect(packages).toStrictEqual({ + '@angular/core': { + singleton: true, + strictVersion: true, + requiredVersion: '~13.2.0', + }, + '@angular/core/testing': { + singleton: true, + strictVersion: true, + requiredVersion: '~13.2.0', + }, + }); + }); }); }); @@ -193,19 +243,17 @@ function createMockedFSForNestedEntryPoints() { } }); - (fs.readFileSync as jest.Mock).mockImplementation((file) => - JSON.stringify({ - name: file - .replace(/\\/g, '/') - .replace(/^.*node_modules[/]/, '') - .replace('/package.json', ''), - dependencies: { - '@angular/core': '~13.2.0', - '@angular/common': '~13.2.0', - rxjs: '~7.4.0', - }, - }) - ); + jest.spyOn(devkit, 'readJsonFile').mockImplementation((file) => ({ + name: file + .replace(/\\/g, '/') + .replace(/^.*node_modules[/]/, '') + .replace('/package.json', ''), + dependencies: { + '@angular/core': '~13.2.0', + '@angular/common': '~13.2.0', + rxjs: '~7.4.0', + }, + })); (fs.readdirSync as jest.Mock).mockImplementation((directoryPath: string) => { const PACKAGE_SETUP = { diff --git a/packages/angular/src/utils/mfe/mfe-webpack.ts b/packages/angular/src/utils/mfe/mfe-webpack.ts index eda3528a23314..71ad35223bd1c 100644 --- a/packages/angular/src/utils/mfe/mfe-webpack.ts +++ b/packages/angular/src/utils/mfe/mfe-webpack.ts @@ -1,12 +1,17 @@ -import { existsSync, lstatSync, readdirSync, readFileSync } from 'fs'; -import { NormalModuleReplacementPlugin } from 'webpack'; -import { joinPathFragments, workspaceRoot } from '@nrwl/devkit'; -import { dirname, join, normalize } from 'path'; -import { ParsedCommandLine } from 'typescript'; +import { + joinPathFragments, + logger, + readJsonFile, + workspaceRoot, +} from '@nrwl/devkit'; import { getRootTsConfigPath, readTsConfig, } from '@nrwl/workspace/src/utilities/typescript'; +import { existsSync, lstatSync, readdirSync } from 'fs'; +import { dirname, join, normalize, relative } from 'path'; +import { ParsedCommandLine } from 'typescript'; +import { NormalModuleReplacementPlugin } from 'webpack'; export interface SharedLibraryConfig { singleton: boolean; @@ -81,44 +86,70 @@ export function shareWorkspaceLibraries( }; } -function collectPackageSecondaries(pkgName: string, packages: string[]) { - const pathToPackage = join(workspaceRoot, 'node_modules', pkgName); - const directories = readdirSync(pathToPackage) +function getNonNodeModulesSubDirs(directory: string): string[] { + return readdirSync(directory) .filter((file) => file !== 'node_modules') - .map((file) => join(pathToPackage, file)) + .map((file) => join(directory, file)) .filter((file) => lstatSync(file).isDirectory()); +} - const recursivelyCheckSubDirectories = ( - directories: string[], - secondaries: string[] - ) => { - for (const directory of directories) { - if (existsSync(join(directory, 'package.json'))) { - secondaries.push(directory); - } - - const subDirs = readdirSync(directory) - .filter((file) => file !== 'node_modules') - .map((file) => join(directory, file)) - .filter((file) => lstatSync(file).isDirectory()); - recursivelyCheckSubDirectories(subDirs, secondaries); +function recursivelyCollectSecondaryEntryPointsFromDirectory( + pkgName: string, + pkgVersion: string, + pkgRoot: string, + directories: string[], + collectedPackages: { name: string; version: string }[] +): void { + for (const directory of directories) { + const packageJsonPath = join(directory, 'package.json'); + if (existsSync(packageJsonPath)) { + const importName = joinPathFragments( + pkgName, + relative(pkgRoot, directory) + ); + + try { + // require the secondary entry point to try to rule out sample code + require.resolve(importName, { paths: [workspaceRoot] }); + const { name } = readJsonFile(packageJsonPath); + // further check to make sure what we were able to require is the + // same as the package name + if (name === importName) { + collectedPackages.push({ name, version: pkgVersion }); + } + } catch {} } - }; - const secondaries = []; - recursivelyCheckSubDirectories(directories, secondaries); - - for (const secondary of secondaries) { - const pathToPkg = join(secondary, 'package.json'); - const libName = JSON.parse(readFileSync(pathToPkg, 'utf-8')).name; - if (!libName) { - continue; - } - packages.push(libName); - collectPackageSecondaries(libName, packages); + const subDirs = getNonNodeModulesSubDirs(directory); + recursivelyCollectSecondaryEntryPointsFromDirectory( + pkgName, + pkgVersion, + pkgRoot, + subDirs, + collectedPackages + ); } } +function collectPackageSecondaryEntryPoints( + pkgName: string, + pkgVersion: string, + collectedPackages: { name: string; version: string }[] +): void { + const packageJsonPath = require.resolve(`${pkgName}/package.json`, { + paths: [workspaceRoot], + }); + const pathToPackage = dirname(packageJsonPath); + const subDirs = getNonNodeModulesSubDirs(pathToPackage); + recursivelyCollectSecondaryEntryPointsFromDirectory( + pkgName, + pkgVersion, + pathToPackage, + subDirs, + collectedPackages + ); +} + export function sharePackages( packages: string[] ): Record { @@ -129,23 +160,32 @@ export function sharePackages( ); } - const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')); - - const allPackages = [...packages]; - packages.forEach((pkg) => collectPackageSecondaries(pkg, allPackages)); - - return allPackages.reduce((shared, pkgName) => { - const nameToUseForVersionLookup = - pkgName.split('/').length > 2 - ? pkgName.split('/').slice(0, 2).join('/') - : pkgName; + const pkgJson = readJsonFile(pkgJsonPath); + 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) => { + if (!pkg.version) { + logger.warn( + `Could not find a version for "${pkg.name}" in the root "package.json" ` + + 'when collecting shared packages for the Module Federation setup. ' + + 'The package will not be shared.' + ); + + return shared; + } return { ...shared, - [pkgName]: { + [pkg.name]: { singleton: true, strictVersion: true, - requiredVersion: pkgJson.dependencies[nameToUseForVersionLookup], + requiredVersion: pkg.version, }, }; }, {}); diff --git a/packages/angular/src/utils/mfe/with-module-federation.spec.ts b/packages/angular/src/utils/mfe/with-module-federation.spec.ts index 31dcb8db20a25..18057a5d6b7e9 100644 --- a/packages/angular/src/utils/mfe/with-module-federation.spec.ts +++ b/packages/angular/src/utils/mfe/with-module-federation.spec.ts @@ -6,11 +6,13 @@ import * as graph from '@nrwl/devkit'; import * as typescriptUtils from '@nrwl/workspace/src/utilities/typescript'; import * as workspace from 'nx/src/project-graph/file-utils'; import * as fs from 'fs'; +import * as devkit from '@nrwl/devkit'; import { withModuleFederation } from './with-module-federation'; describe('withModuleFederation', () => { afterEach(() => jest.clearAllMocks()); + it('should create a host config correctly', async () => { // ARRANGE (graph.readCachedProjectGraph as jest.Mock).mockReturnValue({ @@ -38,13 +40,9 @@ describe('withModuleFederation', () => { }); (fs.existsSync as jest.Mock).mockReturnValue(true); - (fs.readFileSync as jest.Mock).mockReturnValue( - JSON.stringify({ - dependencies: { - '@angular/core': '~13.2.0', - }, - }) - ); + jest.spyOn(devkit, 'readJsonFile').mockImplementation(() => ({ + dependencies: { '@angular/core': '~13.2.0' }, + })); (typescriptUtils.readTsConfig as jest.Mock).mockReturnValue({ options: { @@ -91,13 +89,9 @@ describe('withModuleFederation', () => { }); (fs.existsSync as jest.Mock).mockReturnValue(true); - (fs.readFileSync as jest.Mock).mockReturnValue( - JSON.stringify({ - dependencies: { - '@angular/core': '~13.2.0', - }, - }) - ); + jest.spyOn(devkit, 'readJsonFile').mockImplementation(() => ({ + dependencies: { '@angular/core': '~13.2.0' }, + })); (typescriptUtils.readTsConfig as jest.Mock).mockReturnValue({ options: { @@ -145,13 +139,9 @@ describe('withModuleFederation', () => { }); (fs.existsSync as jest.Mock).mockReturnValue(true); - (fs.readFileSync as jest.Mock).mockReturnValue( - JSON.stringify({ - dependencies: { - '@angular/core': '~13.2.0', - }, - }) - ); + jest.spyOn(devkit, 'readJsonFile').mockImplementation(() => ({ + dependencies: { '@angular/core': '~13.2.0', lodash: '~4.17.20' }, + })); (typescriptUtils.readTsConfig as jest.Mock).mockReturnValue({ options: { @@ -203,13 +193,9 @@ describe('withModuleFederation', () => { }); (fs.existsSync as jest.Mock).mockReturnValue(true); - (fs.readFileSync as jest.Mock).mockReturnValue( - JSON.stringify({ - dependencies: { - '@angular/core': '~13.2.0', - }, - }) - ); + jest.spyOn(devkit, 'readJsonFile').mockImplementation(() => ({ + dependencies: { '@angular/core': '~13.2.0' }, + })); (typescriptUtils.readTsConfig as jest.Mock).mockReturnValue({ options: { @@ -261,13 +247,9 @@ describe('withModuleFederation', () => { }); (fs.existsSync as jest.Mock).mockReturnValue(true); - (fs.readFileSync as jest.Mock).mockReturnValue( - JSON.stringify({ - dependencies: { - '@angular/core': '~13.2.0', - }, - }) - ); + jest.spyOn(devkit, 'readJsonFile').mockImplementation(() => ({ + dependencies: { '@angular/core': '~13.2.0' }, + })); (typescriptUtils.readTsConfig as jest.Mock).mockImplementation(() => ({ options: {