diff --git a/packages/angular/ng-package.json b/packages/angular/ng-package.json index 811a8eb6dcf9b..f0f89458bb2a8 100644 --- a/packages/angular/ng-package.json +++ b/packages/angular/ng-package.json @@ -15,7 +15,8 @@ "webpack-merge", "ts-node", "tsconfig-paths", - "semver" + "semver", + "webpack" ], "keepLifecycleScripts": true } diff --git a/packages/angular/package.json b/packages/angular/package.json index 23e39f38f03c7..e7bd942fbc4e0 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -50,6 +50,7 @@ "webpack-merge": "5.7.3", "ts-node": "~9.1.1", "tsconfig-paths": "^3.9.0", - "semver": "7.3.4" + "semver": "7.3.4", + "webpack": "^5.58.1" } } diff --git a/packages/angular/src/builders/webpack-server/webpack-server.impl.ts b/packages/angular/src/builders/webpack-server/webpack-server.impl.ts index 1d6a663806ea9..3f3f7adeb90b6 100644 --- a/packages/angular/src/builders/webpack-server/webpack-server.impl.ts +++ b/packages/angular/src/builders/webpack-server/webpack-server.impl.ts @@ -13,6 +13,11 @@ import { normalizeOptions } from './lib'; import type { Schema } from './schema'; export function webpackServer(schema: Schema, context: BuilderContext) { + process.env.NX_TSCONFIG_PATH = joinPathFragments( + context.workspaceRoot, + 'tsconfig.base.json' + ); + const options = normalizeOptions(schema); const workspaceConfig = new Workspaces( context.workspaceRoot diff --git a/packages/angular/src/utils/mfe-webpack.spec.ts b/packages/angular/src/utils/mfe-webpack.spec.ts new file mode 100644 index 0000000000000..1469d5a876af3 --- /dev/null +++ b/packages/angular/src/utils/mfe-webpack.spec.ts @@ -0,0 +1,125 @@ +jest.mock('fs'); +jest.mock('@nrwl/workspace'); +import * as fs from 'fs'; +import * as workspace from '@nrwl/workspace'; + +import { sharePackages, shareWorkspaceLibraries } from './mfe-webpack'; + +describe('MFE Webpack Utils', () => { + afterEach(() => jest.clearAllMocks()); + + describe('ShareWorkspaceLibraries', () => { + it('should error when the tsconfig file does not exist', () => { + // ARRANGE + (fs.existsSync as jest.Mock).mockReturnValue(false); + + // ACT + try { + shareWorkspaceLibraries(['@myorg/shared']); + } catch (error) { + // ASSERT + expect(error.message).toEqual( + 'NX MFE: TsConfig Path for workspace libraries does not exist! (undefined)' + ); + } + }); + + it('should create an object with correct setup', () => { + // ARRANGE + (fs.existsSync as jest.Mock).mockReturnValue(true); + (workspace.readTsConfig as jest.Mock).mockReturnValue({ + options: { + paths: { + '@myorg/shared': ['/libs/shared/src/index.ts'], + }, + }, + }); + + // ACT + const sharedLibraries = shareWorkspaceLibraries(['@myorg/shared']); + // ASSERT + expect(sharedLibraries.getAliases()).toHaveProperty('@myorg/shared'); + expect(sharedLibraries.getAliases()['@myorg/shared']).toContain( + 'libs/shared/src/index.ts' + ); + expect(sharedLibraries.getLibraries()).toEqual({ + '@myorg/shared': { + eager: undefined, + requiredVersion: false, + }, + }); + }); + + it('should create an object with empty setup when tsconfig does not contain the shared lib', () => { + // ARRANGE + (fs.existsSync as jest.Mock).mockReturnValue(true); + (workspace.readTsConfig as jest.Mock).mockReturnValue({ + options: { + paths: {}, + }, + }); + + // ACT + const sharedLibraries = shareWorkspaceLibraries(['@myorg/shared']); + // ASSERT + expect(sharedLibraries.getAliases()).toEqual({}); + expect(sharedLibraries.getLibraries()).toEqual({}); + }); + }); + + describe('SharePackages', () => { + it('should throw when it cannot find root package.json', () => { + // ARRANGE + (fs.existsSync as jest.Mock).mockReturnValue(false); + + // ACT + try { + sharePackages(['@angular/core']); + } catch (error) { + // ASSERT + expect(error.message).toEqual( + 'NX MFE: Could not find root package.json to determine dependency versions.' + ); + } + }); + + it('should correctly map the shared packages to objects', () => { + // ARRANGE + (fs.existsSync as jest.Mock).mockReturnValue(true); + (fs.readFileSync as jest.Mock).mockReturnValue( + JSON.stringify({ + dependencies: { + '@angular/core': '~13.2.0', + '@angular/common': '~13.2.0', + rxjs: '~7.4.0', + }, + }) + ); + + // ACT + const packages = sharePackages([ + '@angular/core', + '@angular/common', + 'rxjs', + ]); + // ASSERT + expect(packages).toEqual({ + '@angular/core': { + singleton: true, + strictVersion: true, + requiredVersion: '~13.2.0', + }, + '@angular/common': { + singleton: true, + strictVersion: true, + requiredVersion: '~13.2.0', + }, + rxjs: { + singleton: true, + strictVersion: true, + requiredVersion: '~7.4.0', + }, + }); + }); + }); +}); diff --git a/packages/angular/src/utils/mfe-webpack.ts b/packages/angular/src/utils/mfe-webpack.ts new file mode 100644 index 0000000000000..57a6ecdd1dfa2 --- /dev/null +++ b/packages/angular/src/utils/mfe-webpack.ts @@ -0,0 +1,96 @@ +import { readTsConfig } from '@nrwl/workspace'; +import { existsSync, readFileSync } from 'fs'; +import { NormalModuleReplacementPlugin } from 'webpack'; +import { appRootPath as rootPath } from '@nrwl/tao/src/utils/app-root'; +import { normalizePath, joinPathFragments } from '@nrwl/devkit'; +import { dirname } from 'path'; +import { ParsedCommandLine } from 'typescript'; + +export function shareWorkspaceLibraries( + libraries: string[], + tsConfigPath = process.env.NX_TSCONFIG_PATH +) { + if (!existsSync(tsConfigPath)) { + throw new Error( + `NX MFE: TsConfig Path for workspace libraries does not exist! (${tsConfigPath})` + ); + } + + const tsConfig: ParsedCommandLine = readTsConfig(tsConfigPath); + const tsconfigPathAliases = tsConfig.options?.paths; + + if (!tsconfigPathAliases) { + return { + getAliases: () => [], + getLibraries: () => [], + getReplacementPlugin: () => + new NormalModuleReplacementPlugin(/./, () => {}), + }; + } + + const pathMappings: { name: string; path: string }[] = []; + for (const [key, paths] of Object.entries(tsconfigPathAliases)) { + if (libraries && libraries.includes(key)) { + const pathToLib = normalizePath(joinPathFragments(rootPath, paths[0])); + pathMappings.push({ + name: key, + path: pathToLib, + }); + } + } + + return { + getAliases: () => + pathMappings.reduce( + (aliases, library) => ({ ...aliases, [library.name]: library.path }), + {} + ), + getLibraries: (eager?: boolean) => + pathMappings.reduce( + (libraries, library) => ({ + ...libraries, + [library.name]: { requiredVersion: false, eager }, + }), + {} + ), + getReplacementPlugin: () => + new NormalModuleReplacementPlugin(/./, (req) => { + if (!req.request.startsWith('.')) { + return; + } + + const from = req.context; + const to = normalizePath(joinPathFragments(req.context, req.request)); + + for (const library of pathMappings) { + const libFolder = normalizePath(dirname(library.path)); + if (!from.startsWith(libFolder) && to.startsWith(libFolder)) { + req.request = library.name; + } + } + }), + }; +} + +export function sharePackages(packages: string[]) { + const pkgJsonPath = joinPathFragments(rootPath, 'package.json'); + if (!existsSync(pkgJsonPath)) { + throw new Error( + 'NX MFE: Could not find root package.json to determine dependency versions.' + ); + } + + const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')); + + return packages.reduce( + (shared, pkgName) => ({ + ...shared, + [pkgName]: { + singleton: true, + strictVersion: true, + requiredVersion: pkgJson.dependencies[pkgName], + }, + }), + {} + ); +}