Leosvel Pérez Espinosa 752d418f78
feat(angular): support angular cli v20.0.0-rc.3 (#30715)
Add support for the Angular CLI **20.0.0-rc.3** version.
2025-05-26 10:00:47 -04:00

756 lines
22 KiB
TypeScript

import * as fs from 'fs';
import * as tsUtils from './typescript';
jest.mock('nx/src/devkit-exports', () => {
return {
...jest.requireActual('nx/src/devkit-exports'),
readJsonFile: jest.fn(),
};
});
import * as nxFileutils from 'nx/src/devkit-exports';
import { sharePackages, shareWorkspaceLibraries } from './share';
describe('MF Share Utils', () => {
afterEach(() => jest.clearAllMocks());
describe('ShareWorkspaceLibraries', () => {
it('should error when the tsconfig file does not exist', () => {
// ARRANGE
jest
.spyOn(fs, 'existsSync')
.mockImplementation((p: string) => p?.endsWith('.node'));
// ACT
try {
shareWorkspaceLibraries([
{ name: 'shared', root: 'libs/shared', importKey: '@myorg/shared' },
]);
} catch (error) {
// ASSERT
expect(error.message).toContain(
'NX MF: TsConfig Path for workspace libraries does not exist!'
);
}
});
it('should create an object with correct setup', () => {
// ARRANGE
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
jest.spyOn(tsUtils, 'readTsPathMappings').mockReturnValue({
'@myorg/shared': ['/libs/shared/src/index.ts'],
});
// ACT
const sharedLibraries = shareWorkspaceLibraries([
{ name: 'shared', root: 'libs/shared', importKey: '@myorg/shared' },
]);
// ASSERT
expect(sharedLibraries.getAliases()).toHaveProperty('@myorg/shared');
expect(sharedLibraries.getAliases()['@myorg/shared']).toContain(
'libs/shared/src/index.ts'
);
expect(sharedLibraries.getLibraries('libs/shared')).toEqual({
'@myorg/shared': {
eager: undefined,
requiredVersion: false,
},
});
});
it('should order nested projects first', () => {
// ARRANGE
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
jest.spyOn(tsUtils, 'readTsPathMappings').mockReturnValue({
'@myorg/shared': ['/libs/shared/src/index.ts'],
'@myorg/shared/components': ['/libs/shared/components/src/index.ts'],
});
// ACT
const sharedLibraries = shareWorkspaceLibraries([
{ name: 'shared', root: 'libs/shared', importKey: '@myorg/shared' },
{
name: 'shared-components',
root: 'libs/shared/components',
importKey: '@myorg/shared/components',
},
]);
// ASSERT
expect(Object.keys(sharedLibraries.getAliases())[0]).toEqual(
'@myorg/shared/components'
);
});
it('should handle path mappings with wildcards correctly in non-buildable libraries', () => {
// ARRANGE
jest.spyOn(fs, 'existsSync').mockImplementation((file: string) => true);
jest.spyOn(tsUtils, 'readTsPathMappings').mockReturnValue({
'@myorg/shared': ['/libs/shared/src/index.ts'],
'@myorg/shared/*': ['/libs/shared/src/lib/*'],
});
// ACT
const sharedLibraries = shareWorkspaceLibraries([
{ name: 'shared', root: 'libs/shared', importKey: '@myorg/shared' },
]);
// ASSERT
expect(sharedLibraries.getAliases()).toHaveProperty('@myorg/shared');
expect(sharedLibraries.getAliases()['@myorg/shared']).toContain(
'libs/shared/src/index.ts'
);
expect(sharedLibraries.getLibraries('libs/shared')).toEqual({
'@myorg/shared': {
eager: undefined,
requiredVersion: false,
},
});
});
it('should create an object with empty setup when tsconfig does not contain the shared lib', () => {
// ARRANGE
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
jest.spyOn(tsUtils, 'readTsPathMappings').mockReturnValue({});
// ACT
const sharedLibraries = shareWorkspaceLibraries([
{ name: 'shared', root: 'libs/shared', importKey: '@myorg/shared' },
]);
// ASSERT
expect(sharedLibraries.getAliases()).toEqual({});
expect(sharedLibraries.getLibraries('libs/shared')).toEqual({});
});
});
describe('SharePackages', () => {
it('should throw when it cannot find root package.json', () => {
// ARRANGE
jest
.spyOn(fs, 'existsSync')
.mockImplementation((p: string) => p.endsWith('.node'));
// ACT
try {
sharePackages(['@angular/core']);
} catch (error) {
// ASSERT
expect(error.message).toEqual(
'NX MF: Could not find root package.json to determine dependency versions.'
);
}
});
it('should correctly map the shared packages to objects', () => {
// ARRANGE
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
jest.spyOn(nxFileutils, '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',
},
}));
jest.spyOn(fs, 'readdirSync').mockReturnValue([]);
// ACT
const packages = sharePackages([
'@angular/core',
'@angular/common',
'rxjs',
]);
// ASSERT
expect(packages).toEqual({
'@angular/common': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/common/http': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/common/http/testing': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/common/locales/*': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/common/locales/global/*': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/common/testing': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/common/upgrade': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/core': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/core/event-dispatch-contract.min.js': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/core/primitives/di': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/core/primitives/event-dispatch': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/core/primitives/signals': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/core/rxjs-interop': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/core/schematics/*': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/core/testing': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
rxjs: {
requiredVersion: '~7.4.0',
singleton: true,
strictVersion: true,
},
'rxjs/ajax': {
requiredVersion: '~7.4.0',
singleton: true,
strictVersion: true,
},
'rxjs/fetch': {
requiredVersion: '~7.4.0',
singleton: true,
strictVersion: true,
},
'rxjs/internal/*': {
requiredVersion: '~7.4.0',
singleton: true,
strictVersion: true,
},
'rxjs/operators': {
requiredVersion: '~7.4.0',
singleton: true,
strictVersion: true,
},
'rxjs/testing': {
requiredVersion: '~7.4.0',
singleton: true,
strictVersion: true,
},
'rxjs/webSocket': {
requiredVersion: '~7.4.0',
singleton: true,
strictVersion: true,
},
});
});
// TODO: Get with colum and figure out why this stopped working
xit('should correctly map the shared packages to objects even with nested entry points', () => {
// ARRANGE
/**
* This creates a bunch of mocks that aims to test that
* the sharePackages function can handle nested
* entrypoints in the package that is being shared.
*
* This will set up a directory structure that matches
* the following:
*
* - @angular/core/
* - package.json
* - @angular/common/
* - http/
* - testing/
* - package.json
* - package.json
* - rxjs
* - package.json
*
* The result is that there would be 4 packages that
* need to be shared, as determined by the folders
* containing the package.json files
*/
createMockedFSForNestedEntryPoints();
// ACT
const packages = sharePackages([
'@angular/core',
'@angular/common',
'rxjs',
]);
// ASSERT
expect(packages).toEqual({
'@angular/core': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/core/event-dispatch-contract.min.js': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/core/primitives/di': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/core/primitives/event-dispatch': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/core/primitives/signals': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/core/rxjs-interop': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/core/schematics/*': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/core/testing': {
singleton: true,
strictVersion: true,
requiredVersion: '~13.2.0',
},
'@angular/common': {
singleton: true,
strictVersion: true,
requiredVersion: '~13.2.0',
},
'@angular/common/http': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/common/http/testing': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/common/locales/*': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/common/locales/global/*': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/common/testing': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/common/upgrade': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
rxjs: {
requiredVersion: '~7.4.0',
singleton: true,
strictVersion: true,
},
'rxjs/ajax': {
requiredVersion: '~7.4.0',
singleton: true,
strictVersion: true,
},
'rxjs/fetch': {
requiredVersion: '~7.4.0',
singleton: true,
strictVersion: true,
},
'rxjs/internal/*': {
requiredVersion: '~7.4.0',
singleton: true,
strictVersion: true,
},
'rxjs/operators': {
requiredVersion: '~7.4.0',
singleton: true,
strictVersion: true,
},
'rxjs/testing': {
requiredVersion: '~7.4.0',
singleton: true,
strictVersion: true,
},
'rxjs/webSocket': {
singleton: true,
strictVersion: true,
requiredVersion: '~7.4.0',
},
});
});
it('should not collect a folder with a package.json when cannot be required', () => {
// ARRANGE
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
jest.spyOn(nxFileutils, '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' },
};
});
jest
.spyOn(fs, 'readdirSync')
.mockImplementation((directoryPath: string) => {
const packages = {
'@angular/core': ['testing', 'schematics'],
};
for (const key of Object.keys(packages)) {
if (directoryPath.endsWith(key)) {
return packages[key];
}
}
return [];
});
jest
.spyOn(fs, 'lstatSync')
.mockReturnValue({ isDirectory: () => true } as any);
// ACT
const packages = sharePackages(['@angular/core']);
// ASSERT
expect(packages).toStrictEqual({
'@angular/core': {
singleton: true,
strictVersion: true,
requiredVersion: '~13.2.0',
},
'@angular/core/event-dispatch-contract.min.js': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/core/primitives/di': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/core/primitives/event-dispatch': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/core/primitives/signals': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/core/rxjs-interop': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/core/schematics/*': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/core/testing': {
singleton: true,
strictVersion: true,
requiredVersion: '~13.2.0',
},
});
});
it('should collect secondary entry points from exports and fall back to lookinp up for package.json', () => {
// ARRANGE
jest
.spyOn(fs, 'existsSync')
.mockImplementation(
(path: string) => !path.endsWith('/secondary/package.json')
);
jest.spyOn(nxFileutils, 'readJsonFile').mockImplementation((file) => {
if (file.endsWith('pkg1/package.json')) {
return {
name: 'pkg1',
version: '1.0.0',
exports: {
'.': './index.js',
'./package.json': './package.json',
'./secondary': './secondary/index.js',
},
};
}
// @angular/core/package.json won't have exports, so it looks up for package.json
return {
name: file
.replace(/\\/g, '/')
.replace(/^.*node_modules[/]/, '')
.replace('/package.json', ''),
dependencies: { pkg1: '1.0.0', '@angular/core': '~13.2.0' },
};
});
jest
.spyOn(fs, 'readdirSync')
.mockImplementation((directoryPath: string) => {
const packages = {
pkg1: ['secondary'],
'@angular/core': ['testing'],
};
for (const key of Object.keys(packages)) {
if (directoryPath.endsWith(key)) {
return packages[key];
}
}
return [];
});
jest
.spyOn(fs, 'lstatSync')
.mockReturnValue({ isDirectory: () => true } as any);
// ACT
const packages = sharePackages(['pkg1', '@angular/core']);
// ASSERT
expect(packages).toStrictEqual({
pkg1: {
singleton: true,
strictVersion: true,
requiredVersion: '1.0.0',
},
'pkg1/secondary': {
singleton: true,
strictVersion: true,
requiredVersion: '1.0.0',
},
'@angular/core': {
singleton: true,
strictVersion: true,
requiredVersion: '~13.2.0',
},
'@angular/core/event-dispatch-contract.min.js': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/core/primitives/di': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/core/primitives/event-dispatch': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/core/primitives/signals': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/core/rxjs-interop': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/core/schematics/*': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/core/testing': {
singleton: true,
strictVersion: true,
requiredVersion: '~13.2.0',
},
});
});
it('should not throw when the main entry point package.json cannot be required', () => {
// ARRANGE
jest
.spyOn(fs, 'existsSync')
.mockImplementation(
(file: string) =>
!file.endsWith('non-existent-top-level-package/package.json')
);
jest.spyOn(nxFileutils, 'readJsonFile').mockImplementation((file) => {
return {
name: file
.replace(/\\/g, '/')
.replace(/^.*node_modules[/]/, '')
.replace('/package.json', ''),
dependencies: { '@angular/core': '~13.2.0' },
};
});
// ACT & ASSERT
expect(() =>
sharePackages(['non-existent-top-level-package'])
).not.toThrow();
});
});
it('should using shared library version from root package.json if available', () => {
// ARRANGE
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
jest
.spyOn(nxFileutils, 'readJsonFile')
.mockImplementation((file: string) => {
if (file.endsWith('package.json')) {
return {
dependencies: {
'@myorg/shared': '1.0.0',
},
};
}
});
jest.spyOn(tsUtils, 'readTsPathMappings').mockReturnValue({
'@myorg/shared': ['/libs/shared/src/index.ts'],
'@myorg/shared/*': ['/libs/shared/src/lib/*'],
});
// ACT
const sharedLibraries = shareWorkspaceLibraries(
[{ name: 'shared', root: 'libs/shared', importKey: '@myorg/shared' }],
'/'
);
// ASSERT
expect(sharedLibraries.getLibraries('libs/shared')).toEqual({
'@myorg/shared': {
eager: undefined,
requiredVersion: '1.0.0',
singleton: true,
},
});
});
it('should use shared library version from library package.json if project package.json does not have it', () => {
// ARRANGE
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
jest
.spyOn(nxFileutils, 'readJsonFile')
.mockImplementation((file: string) => {
if (file.endsWith('libs/shared/package.json')) {
return {
version: '1.0.0',
};
} else {
return {};
}
});
jest.spyOn(tsUtils, 'readTsPathMappings').mockReturnValue({
'@myorg/shared': ['/libs/shared/src/index.ts'],
'@myorg/shared/*': ['/libs/shared/src/lib/*'],
});
// ACT
const sharedLibraries = shareWorkspaceLibraries(
[{ name: 'shared', root: 'libs/shared', importKey: '@myorg/shared' }],
null
);
// ASSERT
expect(sharedLibraries.getLibraries('libs/shared')).toEqual({
'@myorg/shared': {
eager: undefined,
requiredVersion: '1.0.0',
singleton: true,
},
});
});
});
function createMockedFSForNestedEntryPoints() {
jest.spyOn(fs, 'existsSync').mockImplementation((file: string) => {
if (file.endsWith('http/package.json')) {
return false;
} else {
return true;
}
});
jest.spyOn(nxFileutils, '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',
},
}));
jest.spyOn(fs, 'readdirSync').mockImplementation((directoryPath: string) => {
const PACKAGE_SETUP = {
'@angular/core': [],
'@angular/common': ['http'],
http: ['testing'],
testing: [],
};
for (const key of Object.keys(PACKAGE_SETUP)) {
if (directoryPath.endsWith(key)) {
return PACKAGE_SETUP[key];
}
}
return [];
});
jest
.spyOn(fs, 'lstatSync')
.mockReturnValue({ isDirectory: () => true } as any);
}