Skip to content

Commit

Permalink
fix(angular): fix collecting secondary entry points for module federa…
Browse files Browse the repository at this point in the history
…tion builds (#10129)
  • Loading branch information
leosvelperez committed May 5, 2022
1 parent 8b81087 commit e212cb9
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 106 deletions.
Expand Up @@ -24,7 +24,7 @@ Array [
"requiredVersion": false,
},
"lodash": Object {
"requiredVersion": undefined,
"requiredVersion": "~4.17.20",
"singleton": true,
"strictVersion": true,
},
Expand Down
94 changes: 71 additions & 23 deletions packages/angular/src/utils/mfe/mfe-webpack.spec.ts
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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',
},
});
});
});
});

Expand All @@ -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 = {
Expand Down
134 changes: 87 additions & 47 deletions 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;
Expand Down Expand Up @@ -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<string, SharedLibraryConfig> {
Expand All @@ -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,
},
};
}, {});
Expand Down
52 changes: 17 additions & 35 deletions packages/angular/src/utils/mfe/with-module-federation.spec.ts
Expand Up @@ -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({
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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: {
Expand Down

0 comments on commit e212cb9

Please sign in to comment.