diff --git a/libs/mf-runtime/package.json b/libs/mf-runtime/package.json index a60add2a..061c4281 100644 --- a/libs/mf-runtime/package.json +++ b/libs/mf-runtime/package.json @@ -1,10 +1,10 @@ { "name": "@angular-architects/module-federation-runtime", "license": "MIT", - "version": "14.3.0-beta.4", + "version": "14.3.0", "peerDependencies": { - "@angular/common": ">=14.0.0-next.15", - "@angular/core": ">=14.0.0-next.15" + "@angular/common": ">=14.0.0", + "@angular/core": ">=14.0.0" }, "dependencies": { "tslib": "^2.0.0" diff --git a/libs/mf-tools/package.json b/libs/mf-tools/package.json index b7b60913..f0dc6fdb 100644 --- a/libs/mf-tools/package.json +++ b/libs/mf-tools/package.json @@ -1,13 +1,13 @@ { "name": "@angular-architects/module-federation-tools", - "version": "14.3.0-beta.4", + "version": "14.3.0", "license": "MIT", "peerDependencies": { - "@angular/common": ">=14.0.0-next.15", - "@angular/core": ">=14.0.0-next.15", - "@angular/router": ">=14.0.0-next.15", - "@angular-architects/module-federation": "^14.3.0-beta.4", - "@angular/platform-browser": ">=14.0.0-next.15", + "@angular/common": ">=14.0.0", + "@angular/core": ">=14.0.0", + "@angular/router": ">=14.0.0", + "@angular-architects/module-federation": "^14.3.0", + "@angular/platform-browser": ">=14.0.0", "rxjs": ">= 6.0.0" }, "dependencies": { diff --git a/libs/mf/README.md b/libs/mf/README.md index 30bd89d9..f9beb148 100644 --- a/libs/mf/README.md +++ b/libs/mf/README.md @@ -8,10 +8,11 @@ Big thanks to the following people who helped to make this possible: - [Tobias Koppers](https://twitter.com/wSokra), Founder of Webpack - [Dmitriy Shekhovtsov](https://twitter.com/valorkin), Angular GDE +- [Michael Egger-Zikes](https://twitter.com/MikeZks), Angular Architects ## Prequisites -- Angular CLI 12 +- Angular CLI 12 or higher (13, 14) ## Motivation ๐Ÿ’ฅ @@ -36,10 +37,21 @@ Since Version 1.2, we also provide some advanced features like: ## Which Version to use? - Angular 12: @angular-architects/module-federation: ^12.0.0 -- Angular 13: @angular-architects/module-federation: ^14.0.0 +- Angular 13: @angular-architects/module-federation: ~14.2.0 +- Angular 14: @angular-architects/module-federation: ^14.3.0 Beginning with Angular 13, we had to add some changes to adjust to the Angular CLI. Please see the next section for this. +## Update + +This library supports ``ng update``: + +``` +ng update @angular-architects/module-federation +``` + +If you update by hand (e. g. via ``npm install``), make sure you also install a respective version of ngx-build-plus (version 14 for Angular 14, version 13 for Angular 13, etc.) + ## Upgrade from Angular 12 or lower Beginning with Angular 13, the CLI generates EcmaScript modules instead of script files. This affects how we work with Module Federation a bit. @@ -50,28 +62,6 @@ Please find information on migrating here: If you start from the scratch, ``ng add`` will take care of these settings. -## Upgrade from Version 1.x - -After updating the libs, you need to adjust the ``webpack.conf.js`` a bit: - - -```diff -module.exports = { - output: { - uniqueName: "delme3", -+ publicPath: "auto" - }, - optimization: { - runtimeChunk: false - }, -+ resolve: { -+ alias: { -+ ...sharedMappings.getAliases(), -+ } -+ }, - [...] -} -``` ## Usage ๐Ÿ› ๏ธ @@ -81,13 +71,23 @@ module.exports = { 2. Adjust the generated ``webpack.config.js`` file 3. Repeat this for further projects in your workspace (if needed) -## Nx +### Nx 1. ``npm install @angular-architects/module-federation`` 2. ``ng g @angular-architects/module-federation:init`` 3. Adjust the generated ``webpack.config.js`` file 4. Repeat this for further projects in your workspace (if needed) +### ๐Ÿ†•๐Ÿ”ฅ Version 14+: Use the --type switch to get the new streamlined configuration + +With version 14, we've introduced a --type switch for ``ng add`` and the ``init`` schematic. Set it to one of the following values to get a more streamlined configuration file: + +- ``host`` +- ``dynamic-host`` +- ``remote`` + +A dynamic host reads the micro frontend's URLs from a configuration file at runtime. + ## Getting Started ๐Ÿงช Please find here a [tutorial](https://github.com/angular-architects/module-federation-plugin/blob/12.0.0/libs/mf/tutorial/tutorial.md) that shows how to use this plugin. @@ -105,11 +105,8 @@ Please have a look at this [article series about Module Federation](https://www. This [example](https://github.com/manfredsteyer/module-federation-plugin-example) loads a microfrontend into a shell: -![Microfrontend Loaded into Shell](https://github.com/angular-architects/module-federation-plugin/raw/main/packages/mf/tutorial/result.png) - Please have a look into the example's **readme**. It points you to the important aspects of using Module Federation. - ## Advanced Features While the above-mentioned tutorial and blog articles guide you through using Module Federation, this section draws your attention to some advanced aspects of this plugin and Module Federation in general. @@ -190,7 +187,30 @@ Let's assume, you have an Angular CLI Monorepo or an Nx Monorepo using path mapp You can now share such a library across all your micro frontends (apps) in your mono repo. This means, this library will be only loaded once. -To accomplish this, just register this lib name with the ``SharedMappings`` instance in your webpack config: +#### New streamlined configuration in version 14+ + +Beginning with version 14, we use a more steamlined configuration, when using the above mentioned --type switch with one of the following options: ``remote``, ``host``, ``dynamic-host``. + +This new configuration automatically shares all local libararies. Hence, you don't need to do a thing. + +However, if you want to control, which local libraries to share, you can use the the ``sharedMappings`` array: + +```javascript +module.exports = withModuleFederationPlugin({ + + shared: { + ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }), + }, + + sharedMappings: ['shared-lib'], +}); +``` + +Please don't forget that sharing in Module Federation is always an opt-in: You need to add this setting to each micro frontend that should share it. + +#### Legacy-Syntax and version 12-13 + +In previous versions, you registered the lib name with the ``SharedMappings`` instance in your webpack config: ```javascript const mf = require("@angular-architects/module-federation/webpack"); @@ -207,7 +227,7 @@ sharedMappings.register( Beginning with version 1.2, the boilerplate for using ``SharedMappings`` is generated for you. You only need to add your lib's name here. -This generated code includes providing the metadata for these libraries for the ``ModuleFederationPlugin`` and adding a plugin making sure that even source code generated by the Angular Compiler uses the shared version of the library. +This generated code includes providing metadata for these libraries for the ``ModuleFederationPlugin`` and adding a plugin making sure that even source code generated by the Angular Compiler uses the shared version of the library. ```javascript plugins: [ @@ -297,6 +317,28 @@ The options passed to shareAll are applied to all dependencies found in your ``p This might come in handy in an mono repo scenario and when doing some experiments/ trouble shooting. +#### Eager and Pinned + +> Big thanks to [Michael Egger-Zickes](https://twitter.com/MikeZks), who came up with these solutions. + +Module Federation allows to directly bundle shared dependencies into your app's bundles. Hence, you don't need to load an additional bundle per shared dependency. This can be interesting to improve an application's startup performance, when there are lots of shared dependencies. + +One possible usage for improving the startup times is to set ``eager`` to ``true`` **just** for the host. The remotes loaded later can reuse these eager dependencies alothough they've been shipped via the host's bundle (e. g. its ``main.js``). This works best, if the host always has the highest compatible versions of the shared dependencies. Also, in this case, you don't need to load the remote entry points upfront. + +While the ``eager`` flag is an out of the box feature provided by module federation since its very first days, we need to adjust the webpack configuration used by the Angular CLI a bit to avoid code duplication in the generated bundles. The new ``withModuleFederationPlugin`` helper that has been introduces with this plugin's version 14 and is the basis for the new streamlined configuration, does this by default. The config just needs to set eager to ``true``. + +```javascript +module.exports = withModuleFederationPlugin({ + + shared: { + ...shareAll({ singleton: true, eager: true, pinned: true, strictVersion: true, requiredVersion: 'auto' }), + }, + +}); +``` + +As shown in the last example, we also added another property: pinned. This makes sure, the shared dependency in put into the application's (e. g. the host's) bundle, even though it's not used there. This allows to preload dependencies that are needed later but subsequently loaded micro frontends via one bundle. + ### Nx Integration If the plugin detects that you are using Nx (it basically looks for a ``nx.json``), it uses the builders provided by Nx. @@ -425,8 +467,6 @@ shared: share({ }) ``` - - #### Not exported Components If you use a shared component without exporting it via your library's barrel (``index.ts`` or ``public-api.ts``), you get the following error at runtime: diff --git a/libs/mf/collection.json b/libs/mf/collection.json index 26fa0f98..0263bc4f 100644 --- a/libs/mf/collection.json +++ b/libs/mf/collection.json @@ -7,7 +7,11 @@ "factory": "./src/schematics/mf/schematic#add", "schema": "./src/schematics/mf/schema.json", "description": "Initialize an angular project for webpack module federation", - "aliases": ["init"] + }, + "init": { + "factory": "./src/schematics/mf/schematic#add", + "schema": "./src/schematics/mf/schema.json", + "description": "Initialize an angular project for webpack module federation", }, "config": { "factory": "./src/schematics/mf/schematic", diff --git a/libs/mf/package.json b/libs/mf/package.json index a8fbdde9..93b3f0f7 100644 --- a/libs/mf/package.json +++ b/libs/mf/package.json @@ -1,6 +1,6 @@ { "name": "@angular-architects/module-federation", - "version": "14.3.0-beta.4", + "version": "14.3.0", "license": "MIT", "repository": { "type": "GitHub", @@ -17,7 +17,7 @@ "schematics": "./collection.json", "builders": "./builders.json", "dependencies": { - "@angular-architects/module-federation-runtime": "14.3.0-beta.4", + "@angular-architects/module-federation-runtime": "14.3.0", "word-wrap": "^1.2.3", "callsite": "^1.0.0", "node-fetch": "^2.6.7", diff --git a/libs/mf/src/schematics/mf/files/webpack.config.js b/libs/mf/src/schematics/mf/files/webpack.config.js index 4651413d..784cadad 100644 --- a/libs/mf/src/schematics/mf/files/webpack.config.js +++ b/libs/mf/src/schematics/mf/files/webpack.config.js @@ -16,5 +16,4 @@ module.exports = withModuleFederationPlugin({ ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }), }, - sharedMappings: [], }); diff --git a/libs/mf/src/schematics/mf/schematic.ts b/libs/mf/src/schematics/mf/schematic.ts index 223f6869..0770917d 100644 --- a/libs/mf/src/schematics/mf/schematic.ts +++ b/libs/mf/src/schematics/mf/schematic.ts @@ -301,11 +301,11 @@ export default function config(options: MfSchematicSchema): Rule { const dep = getPackageJsonDependency(tree, "ngx-build-plus"); - if (!dep || !semver.satisfies(dep.version, '>=14.0.0-beta.0')) { + if (!dep || !semver.satisfies(dep.version, '>=14.0.0')) { addPackageJsonDependency(tree, { name: 'ngx-build-plus', type: NodeDependencyType.Dev, - version: '>=14.0.0-beta.0', + version: '^14.0.0', overwrite: true }); diff --git a/libs/mf/src/schematics/migrate-to-14-3/schematic.ts b/libs/mf/src/schematics/migrate-to-14-3/schematic.ts index 683b3502..7796a46f 100644 --- a/libs/mf/src/schematics/migrate-to-14-3/schematic.ts +++ b/libs/mf/src/schematics/migrate-to-14-3/schematic.ts @@ -9,7 +9,7 @@ export function index(): Rule { addPackageJsonDependency(tree, { name: 'ngx-build-plus', type: NodeDependencyType.Dev, - version: '>=14.0.0-beta.0', + version: '^14.0.0', overwrite: true }); diff --git a/libs/mf/tutorial/mfe1.png b/libs/mf/tutorial/mfe1.png index c6a1cac5..05e1a4cb 100644 Binary files a/libs/mf/tutorial/mfe1.png and b/libs/mf/tutorial/mfe1.png differ diff --git a/libs/mf/tutorial/result.png b/libs/mf/tutorial/result.png index fa66ed59..8e9562e2 100644 Binary files a/libs/mf/tutorial/result.png and b/libs/mf/tutorial/result.png differ diff --git a/libs/mf/tutorial/shell.png b/libs/mf/tutorial/shell.png index f0993eba..18fed558 100644 Binary files a/libs/mf/tutorial/shell.png and b/libs/mf/tutorial/shell.png differ diff --git a/libs/mf/tutorial/tutorial.md b/libs/mf/tutorial/tutorial.md index af07651d..70c65ad9 100644 --- a/libs/mf/tutorial/tutorial.md +++ b/libs/mf/tutorial/tutorial.md @@ -6,7 +6,7 @@ This tutorial shows how to use Webpack Module Federation together with the Angul ![Microfrontend Loaded into Shell](https://github.com/angular-architects/module-federation-plugin/raw/main/libs/mf/tutorial/result.png) -**Important**: This tutorial is written for Angular and **Angular CLI 13.1** and higher. To find out about the small differences for lower versions of Angular and for the migration from such a lower version, please have a look to our [migration guide](https://github.com/angular-architects/module-federation-plugin/blob/main/migration-guide.md). +**Important**: This tutorial is written for Angular and **Angular CLI 14** and higher. To find out about the small differences for lower versions of Angular and for the migration from such a lower version, please have a look to our [migration guide](https://github.com/angular-architects/module-federation-plugin/blob/main/migration-guide.md). ## Part 1: Clone and Inspect the Starterkit @@ -27,13 +27,13 @@ In this part you will clone the starterkit and inspect its projects. ``` 3. Start the shell (``ng serve shell -o``) and inspect it a bit: - 1. Click on the ``flights`` link. It leads to a dummy route. This route will later be used for loading the separately compiled microfrontend. + 1. Click on the ``flights`` link. It leads to a dummy route. This route will later be used for loading the separately compiled Micro Frontend. 2. Have a look to the shell's source code. 3. Stop the CLI (``CTRL+C``). -4. Do the same for the microfrontend. In this project, it's called ``mfe1`` (Microfrontend 1) You can start it with ``ng serve mfe1 -o``. +4. Do the same for the Micro Frontend. In this project, it's called ``mfe1`` (Micro Frontend 1) You can start it with ``ng serve mfe1 -o``. ## Part 2: Activate and Configure Module Federation @@ -42,85 +42,58 @@ Now, let's activate and configure module federation: 1. Install ``@angular-architects/module-federation`` into the shell and into the micro frontend: ``` - ng add @angular-architects/module-federation --project shell --port 5000 + ng add @angular-architects/module-federation --project mfe1 --type remote --port 4201 - ng add @angular-architects/module-federation --project mfe1 --port 3000 + ng add @angular-architects/module-federation --project shell --type host --port 4200 ``` This activates module federation, assigns a port for ng serve, and generates the skeleton of a module federation configuration. -2. Open the ``tsconfig.json`` in your workspace's root and ensure your self that the ``target`` property points to ``ES2020`` or higher. This is a prerequisite for using Module Federation with Angular 13.1 or higher. - 3. Switch into the project ``mfe1`` and open the generated configuration file ``projects\mfe1\webpack.config.js``. It contains the module federation configuration for ``mfe1``. Adjust it as follows: ```javascript - const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); + const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack'); - [...] + module.exports = withModuleFederationPlugin({ - module.exports = { - [...], - plugins: [ - new ModuleFederationPlugin({ - - library: { type: "module" }, - - // For remotes (please adjust) - name: "mfe1", - filename: "remoteEntry.js", - exposes: { - // Update this: - './Module': './projects/mfe1/src/app/flights/flights.module.ts', - }, - shared: share({ - "@angular/core": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, - "@angular/common": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, - "@angular/common/http": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, - "@angular/router": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, - [...] - }) - }), - [...] - ], - }; + name: 'mfe1', + + exposes: { + // Update this whole line (both, left and right part): + './Module': './projects/mfe1/src/app/flights/flights.module.ts' + }, + + shared: { + ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }), + }, + + }); ``` This exposes the ``FlightsModule`` under the Name ``./Module``. Hence, the shell can use this path to load it. -3. Switch into the ``shell`` project and open the file ``projects\shell\webpack.config.js``. Adjust it as follows: +3. Switch into the ``shell`` project and open the file ``projects\shell\webpack.config.js``. Make sure, the mapping in the remotes section uses port ``4201`` (and hence, points to the Micro Frontend): ```javascript - const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); + const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack'); - [...] + module.exports = withModuleFederationPlugin({ - module.exports = { - [...], - plugins: [ - new ModuleFederationPlugin({ - - library: { type: "module" }, - - // Make sure to use port 3000 - remotes: { - 'mfe1': "http://localhost:3000/remoteEntry.js" - }, - shared: share({ - "@angular/core": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, - "@angular/common": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, - "@angular/common/http": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, - "@angular/router": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, - [...] - }) - }), - [...] - ], - }; + remotes: { + // Check this line. Is port 4201 configured? + "mfe1": "http://localhost:4201/remoteEntry.js", + }, + + shared: { + ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }), + }, + + }); ``` - This references the separately compiled and deployed ``mfe1`` project. There are some alternatives to configure its URL (see links at the end). + This references the separately compiled and deployed ``mfe1`` project. -4. Open the ``shell``'s router config (``projects\shell\src\app\app.routes.ts``) and add a route loading the microfrontend: +4. Open the ``shell``'s router config (``projects\shell\src\app\app.routes.ts``) and add a route loading the Micro Frontend: ```javascript { @@ -150,27 +123,29 @@ Now, let's try it out! **Hint:** You might use two terminals for this. -2. After a browser window with the shell opened (``http://localhost:5000``), click on ``Flights``. This should load the microfrontend into the shell: +2. After a browser window with the shell opened (``http://localhost:4200``), click on ``Flights``. This should load the Micro Frontend into the shell: ![Shell](https://github.com/angular-architects/module-federation-plugin/raw/main/libs/mf/tutorial/shell.png) -3. Also, ensure yourself that the microfrontend also runs in standalone mode at http://localhost:3000: +3. Also, ensure yourself that the Micro Frontend also runs in standalone mode at http://localhost:4201: ![Microfrontend](https://github.com/angular-architects/module-federation-plugin/raw/main/libs/mf/tutorial/mfe1.png) +**Hint:** You can also call the following script to start all projects at once: ``npm run run:all``. This script is added by the Module Federation plugin. Congratulations! You've implemented your first Module Federation project with Angular! ## Part 4: Switch to Dynamic Federation -Now, let's remove the need for registering the micro frontends upfront with with shell. +Now, let's remove the need for registering the Micro Frontends upfront with with shell. + +### Part 4a: Basic Usage of Dynamic Federation -1. Switch to your ``shell`` application and open the file ``webpack.config.js``. Here, remove the registered remotes: +1. Switch to your ``shell`` application and open the file ``projects\shell\webpack.config.js``. Here, remove the registered remotes: ```javascript remotes: { - // Remove this line or comment it out: - // "mfe1": "http://localhost:3000/remoteEntry.js", + // "mfe1": "http://localhost:4201/remoteEntry.js", }, ``` @@ -187,7 +162,7 @@ Now, let's remove the need for registering the micro frontends upfront with with loadChildren: () => loadRemoteModule({ type: 'module', - remoteEntry: 'http://localhost:3000/remoteEntry.js', + remoteEntry: 'http://localhost:4201/remoteEntry.js', exposedModule: './Module' }) .then(m => m.FlightsModule) @@ -196,13 +171,15 @@ Now, let's remove the need for registering the micro frontends upfront with with ] ``` - *Remarks:* ``type: 'module'`` is needed for Angular 13.1 or higher as beginning with version 13 the CLI emits EcmaScript modules instead of "plain old" JavaScript files. + *Remarks:* ``type: 'module'`` is needed for Angular 13 or higher as beginning with version 13 the CLI emits EcmaScript modules instead of "plain old" JavaScript files. 3. Restart both, the ``shell`` and the micro frontend (``mfe1``). 4. The shell should still be able to load the micro frontend. However, now it's loaded dynamically. -This was quite easy, wasn't it? However, we can improve this solution a bit. Ideally, we load the remote entry upfront before Angular bootstraps. In this early phase, Module Federation tries to determine the highest compatible versions of all dependencies. Let's assume, the shell provides version 1.0.0 of a dependency (specifying ^1.0.0 in its ``package.json``) and the micro frontend uses version 1.1.0 (specifying ^1.1.0 in its ``package.json``). In this case, they would go with version 1.1.0. However, this is only possible if the remote's entry is loaded upfront. +### Part 4b: Loading Meta Data Upfront + +This was quite easy, wasn't it? However, we can improve this solution a bit. Ideally, we load the Micro Frontend's remoteEntry.js upfront.before Angular bootstraps. This file contains meta data about the Micro Frontend, esp. about its shared dependencies. Knowing about them upfront help Module Federation to avoid version conflicts. 1. Switch to the ``shell`` project and open the file ``main.ts``. Adjust it as follows: @@ -210,7 +187,10 @@ This was quite easy, wasn't it? However, we can improve this solution a bit. Ide import { loadRemoteEntry } from '@angular-architects/module-federation'; Promise.all([ - loadRemoteEntry({type: 'module', remoteEntry: 'http://localhost:3000/remoteEntry.js') + loadRemoteEntry({ + type: 'module', + remoteEntry: 'http://localhost:4201/remoteEntry.js' + }) ]) .catch(err => console.error('Error loading remote entries', err)) .then(() => import('./bootstrap')) @@ -221,7 +201,54 @@ This was quite easy, wasn't it? However, we can improve this solution a bit. Ide 3. The shell should still be able to load the micro frontend. -## Step 5: Share a Library of Your Monorepo + +### Part 4c: Use a Registry + +So far, we just hardcoded the urls pointing to our Micro Frontends. However, in a real world scenario, we would rather get this information at runtime from a config file or a registry service. This is what this exercise is about. + +1. Switch to the shell, and create a file ``mf.manifest.json`` in its ``assets`` folder (``projects\shell\src\assets\mf.manifest.json``): + + ```json + { + "mfe1": "http://localhost:4201/remoteEntry.js" + } + ``` + +2. Adjust the shell's ``main.ts`` (``projects/shell/src/main.ts``) as follows: + + ```typescript + import { loadManifest } from '@angular-architects/module-federation'; + + loadManifest('assets/mf.manifest.json') + .catch(err => console.error('Error loading remote entries', err)) + .then(() => import('./bootstrap')) + .catch(err => console.error(err)); + ``` + + The imported ``loadManifest`` function also loads the remote entry points. + +3. Adjust the shell's lazy route pointing to the Micro Frontend as follows (``projects/shell/src/app/app.routes.ts``): + + ```typescript + { + path: 'flights', + loadChildren: () => + loadRemoteModule({ + type: 'manifest', + remoteName: 'mfe1', + exposedModule: './Module' + }) + .then(m => m.FlightsModule) + }, + ``` + +4. Restart both, the ``shell`` and the micro frontend (``mfe1``). + +5. The shell should still be able to load the micro frontend. + +**Hint:** The ``ng add`` command used initially also provides an option ``--type dynamic-host``. This makes ng add to generate the ``mf.manifest.json`` and generates the call to ``loadManifest`` in the ``main.ts``. + +## Step 5: Communication Between Micro Frontends and Sharing Monorepo Libraries 1. Add a library to your monorepo: @@ -229,31 +256,21 @@ This was quite easy, wasn't it? However, we can improve this solution a bit. Ide ng g lib auth-lib ``` -2. In your ``tsconfig.json`` in the project's root, adjust the path mapping for ``auth-lib`` so that it points to the libs entry point: +2. In your ``tsconfig.json`` in the workspace's root, adjust the path mapping for ``auth-lib`` so that it points to the libs entry point: ```json "auth-lib": [ - "projects/auth-lib/src/public-api.ts" + "projects/auth-lib/src/public-api.ts" ] ``` 3. As most IDEs only read global configuration files like the ``tsconfig.json`` once, restart your IDE (Alternatively, your IDE might also provide an option for reloading these settings). -4. Open the ``shell``'s ``webpack.config.js`` and register the created ``auth-lib`` with the ``sharedMappings``: +4. Switch to your ``auth-lib`` project and open the file ``auth-lib.service.ts`` (``projects\auth-lib\src\lib\auth-lib.service.ts``). Adjust it as follows: ```typescript - const sharedMappings = new mf.SharedMappings(); - sharedMappings.register( - path.join(__dirname, '../../tsconfig.json'), - ['auth-lib'] // <-- Add this entry! - ); - ``` + import { Injectable } from '@angular/core'; -5. Also open the micro frontends (``mfe1``) ``webpack.config.js`` and do the same. - -6. Switch to your ``auth-lib`` project and open the file ``auth-lib.service.ts``. Adjust it as follows: - - ```typescript @Injectable({ providedIn: 'root' }) @@ -275,7 +292,7 @@ This was quite easy, wasn't it? However, we can improve this solution a bit. Ide } ``` -7. Switch to your ``shell`` project and open its ``app.component.ts``. Use the shared ``AuthLibService`` to login a user: +5. Switch to your ``shell`` project and open its ``app.component.ts`` (``projects\shell\src\app\app.component.ts``). Use the ``AuthLibService`` to login a user: ```typescript import { AuthLibService } from 'auth-lib'; @@ -299,11 +316,11 @@ This was quite easy, wasn't it? However, we can improve this solution a bit. Ide ```typescript export class FlightsSearchComponent { - [...] - + // Add this: user = this.service.user; - constructor(private service: AuthLibService, [...]) { } + // And add that: + constructor(private service: AuthLibService) { } [...] } @@ -313,15 +330,83 @@ This was quite easy, wasn't it? However, we can improve this solution a bit. Ide ```html
-
{{user}}
+
User: {{user}}
[...]
``` 10. Restart both, the ``shell`` and the micro frontend (``mfe1``). -11. In the shell, navigate to the micro frontend. If it shows the same user name, the library is shared. +11. In the shell, navigate to the micro frontend. If it shows the used user name ``Max``, the library is shared. + + +**Remarks:** All the libraries of your Monorepo are shared by default. The next section shows how to select libraries to share. + +## Bonus - Part 6: Explicitly Configure Shared Dependencies + +So far, all dependencies have been shared. The used ``shareAll`` function makes sure, all packages in your ``package.json``'s ``dependencies`` section are shared and by default, all monorepo-internal libraries like the ``auth-lib`` are shared too. + +While this makes getting started with Module Federation easy, we can get a more performant solution by directly defining what to share. This is because shared dependencies are not tree-shakable and they end up in a bundle of their on that needs to be loaded. + +For explicitly sharing our dependencies, you could switch to the following configurations: + +1. Shell's ``webpack.config.js`` (``projects\shell\webpack.config.js``): + + ```javascript + // Import share instead of shareAll: + const { share, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack'); + + module.exports = withModuleFederationPlugin({ + + remotes: { + // Check this line. Is port 4201 configured? + // "mfe1": "http://localhost:4201/remoteEntry.js", + }, + + // Explicitly share packages: + shared: share({ + "@angular/core": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, + "@angular/common": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, + "@angular/common/http": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, + "@angular/router": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, + }), + + // Explicitly share mono-repo libs: + sharedMappings: ['auth-lib'], + + }); + ``` + +1. Micro Frontend's ``webpack.config.js`` (``projects\mfe1\webpack.config.js``): + + ```javascript + // Import share instead of shareAll: + const { share, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack'); + + module.exports = withModuleFederationPlugin({ + + name: 'mfe1', + + exposes: { + // Update this whole line (both, left and right part): + './Module': './projects/mfe1/src/app/flights/flights.module.ts' + }, + + // Explicitly share packages: + shared: share({ + "@angular/core": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, + "@angular/common": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, + "@angular/common/http": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, + "@angular/router": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, + }), + + // Explicitly share mono-repo libs: + sharedMappings: ['auth-lib'], + + }); + ``` +After that, restart the ``shell`` and the Micro Frontend. ## More Details on Module Federation diff --git a/migration-guide-13.md b/migration-guide-13.md new file mode 100644 index 00000000..107d0fa1 --- /dev/null +++ b/migration-guide-13.md @@ -0,0 +1,196 @@ +# Migration Guide for Angular 13 + +Beginning with version 13, the Angular CLI compiles emits EcmaScript modules. This also effects how entry points for Module Federation are generated. This guide shows how you can adjust to this. + +## Big Thanks + +Big thanks to all the people, that helped with this migration: + +- [Tobias Koppers](https://twitter.com/wSokra), Founder of Webpack +- [Colum Ferry](https://twitter.com/ferrycolum), Senior Software Engineer +at NRWL +- [Thomas Sandeep](https://github.com/SandeepThomas) +- [Michael Zikes](https://twitter.com/MikeZks) + +## Upgrade to the Newest Version of @angular-architects/module-federation + +``` +yarn add @angular-architects/module-federation@14.0.0-rc.1 +``` + +## Upgrade to Angular and Angular CLI 13.1 (!) or higher + +As we need a newer webpack version, don't go with Angular 13.0 but with 13.1 or higher. + +## Update your Compilation Target + +In your ``tsconfig.json`` or ``tsconfig.base.json``, make sure, your compilation ``target`` is ``es2020`` or higher: + + +```json +{ + "compileOnSave": false, + "compilerOptions": { + [...] + "target": "es2020", + [...] + }, + [...] +} +``` + +## Adjust your webpack Configs + +Add the following setting to all your webpack configs: + +```diff +[...] +module.exports = { + [...] ++ experiments: { ++ outputModule: true ++ }, + plugins: [ + new ModuleFederationPlugin({ ++ library: { type: "module" }, + + [...] + }) + ] +}; +``` + +## Static Federation + +If you use static federation, you need to further adjust your shell's webpack config. As EcmaScript modules can be directly imported, there is no ``remoteName`` anymore. Before, this name was used as the name of a global variable that made the remote available. Hence, remove it from the values in your ``remotes`` section: + +```diff +[...] +module.exports = { + [...] + plugins: [ + new ModuleFederationPlugin({ + library: { type: "module" }, + + // For hosts (please adjust) + remotes: { + // Load as module +- "mfe1": "mfe1@http://localhost:3000/remoteEntry.js", ++ "mfe1": "http://localhost:3000/remoteEntry.js", + + }, + + [...] + }) + + ] +}; +``` + +## Dynamic Federation + +Adjust your usage of ``loadRemoteModule``, e. g. in your routing config: + +```diff +{ + path: 'flights', + loadChildren: () => + loadRemoteModule({ ++ type: 'module', + remoteEntry: 'http://localhost:3000/remoteEntry.js', +- remoteName: 'mfe1', + exposedModule: './Module' + }) + .then(m => m.FlightsModule) +}, +``` + +Also, adjust your usage of ``loadRemoteEntry``, e. g. in your ``main.ts``: + +```diff +- loadRemoteEntry('http://localhost:3000/remoteEntry.js', 'mfe1') ++ loadRemoteEntry({ type: 'module', remoteEntry: 'http://localhost:3000/remoteEntry.js'}) + .then(_ => import('./bootstrap').catch(err => console.error(err))) + +``` + +### Adjusting your angular.json + +To prevent issues with live reloads, you need to add a ``publicHost`` property to your remote's configuration in your ``angular.json``. Hence, adjust the section ``project/remote-project-name/architect/serve/options`` as follows: + +```diff +[...] +"options": { ++ "publicHost": "http://localhost:3000", + "port": 3000, + "extraWebpackConfig": "projects/mfe1/webpack.config.js" +} +[...] +``` + +### Deployment: Enable CORS + +As remotes are now loaded as EcmaScript modules, the same origin policy is in place. Hence, if your micro frontends and the shell are deployed to different origins, you need to enable CORS. The same holds true if you run your application after building it with a command line web server like ``serve`` (``serve``, e. g., has a ``--cors`` options). + +### Advanced: Dynamic Federation with Script-based Remotes + +If you also want to load (existing) script-based remotes into your shell, e. g. remotes built with Angular 12 used for a [Multi-Version/Multi-Framework setup](https://www.npmjs.com/package/@angular-architects/module-federation-tools), you can pass ``type: 'script'`` to both, ``loadRemoteModule`` and ``loadRemoteEntry``. In this case, you also need to pass a ``remoteName``. + +### Advanced: Static Federation with Script-based Remotes + +If you want to load (existing) script-based remote into your shell, e. g. such built with Angular 12, you can use the following syntax in the shell's ``webpack.config.js``. + +In the following example, ``mfe1`` is loaded as a module while ``mfe2`` is loaded as a script: + +```javascript +remotes: { + // Load as module: + mfe1": "http://localhost:3000/remoteEntry.js", + + // Load as script: + mfe2": "script mfe2@http://localhost:3000/remoteEntry.js", +} +``` + +### Advanced: Opting-out of Using EcmaScript Modules + +While moving forward with Modules and aligning with the CLI is a good idea, you might to temporarily opt-out of using them. This gives you some additional time for the migration as it brings back the behavior of Angular 12. For this, adjust your webpack configs as follows: + +```diff +module.exports = { + output: { + uniqueName: "dashboard", + publicPath: "auto", ++ scriptType: 'text/javascript' + }, + [...] +} +``` + +Also, don't use the settings introduced above for Angular 13.1+: + + +```diff +[...] +module.exports = { + [...] +- experiments: { +- outputModule: true +- }, + plugins: [ + new ModuleFederationPlugin({ +- library: { type: "module" }, + + [...] + }) + ] +}; +``` + +## SSR + +We have a sound solution including Schematics for SSR in Angular 12. However, because of a bug in Angular Universal 13, SSR is currently not supported for Angular 13. However, we are monitoring this situation and providing a solution as soon as these issues are fixed. + +## Example + +see https://github.com/manfredsteyer/mf-angular-13 diff --git a/migration-guide-14.md b/migration-guide-14.md new file mode 100644 index 00000000..da8c0c73 --- /dev/null +++ b/migration-guide-14.md @@ -0,0 +1,100 @@ +# Migration Guide for Angular 14 + +## Streamlined Configuration + +With version 14, we introduced a new and more streamlined way of configuring Module Federation. **The old way still works**, but you might want to move over to the new more concise way using the new ``withModuleFederationPlugin`` helper function. + +The schematics and ng add use this new way automatically if you set the ``--type`` switch to ``host``, ``dynamic-host``, or ``remote``. + +This is an example for configuring a remote with the new streamlined form: + +```javascript +const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack'); + +// Version 14 +module.exports = withModuleFederationPlugin({ + + name: 'mfe1', + + exposes: { + './Module': './projects/mfe1/src/app/flights/flights.module.ts', + }, + + shared: { + ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }), + }, + +}); +``` + +In version 13, the same looked like this: + +```javascript +// Version 13 +const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); +const mf = require("@angular-architects/module-federation/webpack"); +const path = require("path"); + +const share = mf.share; + +const sharedMappings = new mf.SharedMappings(); +sharedMappings.register( + path.join(__dirname, '../../tsconfig.json'), + ['auth-lib'] +); + +module.exports = { + output: { + uniqueName: "mfe1", + publicPath: "auto" + }, + optimization: { + runtimeChunk: false + }, + resolve: { + alias: { + ...sharedMappings.getAliases(), + } + }, + experiments: { + outputModule: true + }, + plugins: [ + new ModuleFederationPlugin({ + library: { type: "module" }, + + // For remotes (please adjust) + name: "mfe1", + filename: "remoteEntry.js", // 2-3K w/ Meta Data + exposes: { + './Module': './projects/mfe1/src/app/flights/flights.module.ts', + }, + shared: share({ + "@angular/core": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, + "@angular/common": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, + "@angular/router": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, + "@angular/common/http": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, + + // Uncomment for sharing lib of an Angular CLI or Nx workspace + ...sharedMappings.getDescriptors() + }) + + }), + // Uncomment for sharing lib of an Angular CLI or Nx workspace + sharedMappings.getPlugin(), + ], +}; +``` + +While the new version is quite shorter, it still contains the settings, one usually adjusts -- the settings passed to the ModuleFederationPlugin. The new helper ``withModuleFederationPlugin`` supports a **super set** of these settings and uses **smart defaults**. + +These defaults are: + +- ``library: { type: "module" }``: This is what you need for Angular >= 13 as CLI 13 switched over to emitting "real" EcmaScript modules instead of just ordinary JavaScript bundles. +- ``filename: 'remoteEntry.js'``: This makes Module Federation emit a file ``remoteEntry.js`` with the remote entry point. +- ``share: shareAll(...)``: This shares all packages found in the dependencies section of your ``package.json`` by default. +- ``sharedMappings``: If you skip the ``sharedMappings`` array, all local libs (aka mono repo-internal libs or mapped paths) are shared. Otherwise, only the mentioned libs are shared + +## Remarks on shareAll + +As mentioned above, withModuleFederationPlugin uses shareAll by default. This allows for a quick first setup that works. However, it might lead to too much shared bundles. Please also note, that shared dependencies cannot be tree shaken. You can optimize this by switching over from ``shareAll`` to the ``share`` helper shown above. \ No newline at end of file diff --git a/migration-guide.md b/migration-guide.md index 107d0fa1..c6ddd1cb 100644 --- a/migration-guide.md +++ b/migration-guide.md @@ -1,196 +1,6 @@ -# Migration Guide for Angular 13 +# Migration Guides -Beginning with version 13, the Angular CLI compiles emits EcmaScript modules. This also effects how entry points for Module Federation are generated. This guide shows how you can adjust to this. +Our goal is to introduce as little breaking changes as possible. However, sometimes we need to adjust to new developments. -## Big Thanks - -Big thanks to all the people, that helped with this migration: - -- [Tobias Koppers](https://twitter.com/wSokra), Founder of Webpack -- [Colum Ferry](https://twitter.com/ferrycolum), Senior Software Engineer -at NRWL -- [Thomas Sandeep](https://github.com/SandeepThomas) -- [Michael Zikes](https://twitter.com/MikeZks) - -## Upgrade to the Newest Version of @angular-architects/module-federation - -``` -yarn add @angular-architects/module-federation@14.0.0-rc.1 -``` - -## Upgrade to Angular and Angular CLI 13.1 (!) or higher - -As we need a newer webpack version, don't go with Angular 13.0 but with 13.1 or higher. - -## Update your Compilation Target - -In your ``tsconfig.json`` or ``tsconfig.base.json``, make sure, your compilation ``target`` is ``es2020`` or higher: - - -```json -{ - "compileOnSave": false, - "compilerOptions": { - [...] - "target": "es2020", - [...] - }, - [...] -} -``` - -## Adjust your webpack Configs - -Add the following setting to all your webpack configs: - -```diff -[...] -module.exports = { - [...] -+ experiments: { -+ outputModule: true -+ }, - plugins: [ - new ModuleFederationPlugin({ -+ library: { type: "module" }, - - [...] - }) - ] -}; -``` - -## Static Federation - -If you use static federation, you need to further adjust your shell's webpack config. As EcmaScript modules can be directly imported, there is no ``remoteName`` anymore. Before, this name was used as the name of a global variable that made the remote available. Hence, remove it from the values in your ``remotes`` section: - -```diff -[...] -module.exports = { - [...] - plugins: [ - new ModuleFederationPlugin({ - library: { type: "module" }, - - // For hosts (please adjust) - remotes: { - // Load as module -- "mfe1": "mfe1@http://localhost:3000/remoteEntry.js", -+ "mfe1": "http://localhost:3000/remoteEntry.js", - - }, - - [...] - }) - - ] -}; -``` - -## Dynamic Federation - -Adjust your usage of ``loadRemoteModule``, e. g. in your routing config: - -```diff -{ - path: 'flights', - loadChildren: () => - loadRemoteModule({ -+ type: 'module', - remoteEntry: 'http://localhost:3000/remoteEntry.js', -- remoteName: 'mfe1', - exposedModule: './Module' - }) - .then(m => m.FlightsModule) -}, -``` - -Also, adjust your usage of ``loadRemoteEntry``, e. g. in your ``main.ts``: - -```diff -- loadRemoteEntry('http://localhost:3000/remoteEntry.js', 'mfe1') -+ loadRemoteEntry({ type: 'module', remoteEntry: 'http://localhost:3000/remoteEntry.js'}) - .then(_ => import('./bootstrap').catch(err => console.error(err))) - -``` - -### Adjusting your angular.json - -To prevent issues with live reloads, you need to add a ``publicHost`` property to your remote's configuration in your ``angular.json``. Hence, adjust the section ``project/remote-project-name/architect/serve/options`` as follows: - -```diff -[...] -"options": { -+ "publicHost": "http://localhost:3000", - "port": 3000, - "extraWebpackConfig": "projects/mfe1/webpack.config.js" -} -[...] -``` - -### Deployment: Enable CORS - -As remotes are now loaded as EcmaScript modules, the same origin policy is in place. Hence, if your micro frontends and the shell are deployed to different origins, you need to enable CORS. The same holds true if you run your application after building it with a command line web server like ``serve`` (``serve``, e. g., has a ``--cors`` options). - -### Advanced: Dynamic Federation with Script-based Remotes - -If you also want to load (existing) script-based remotes into your shell, e. g. remotes built with Angular 12 used for a [Multi-Version/Multi-Framework setup](https://www.npmjs.com/package/@angular-architects/module-federation-tools), you can pass ``type: 'script'`` to both, ``loadRemoteModule`` and ``loadRemoteEntry``. In this case, you also need to pass a ``remoteName``. - -### Advanced: Static Federation with Script-based Remotes - -If you want to load (existing) script-based remote into your shell, e. g. such built with Angular 12, you can use the following syntax in the shell's ``webpack.config.js``. - -In the following example, ``mfe1`` is loaded as a module while ``mfe2`` is loaded as a script: - -```javascript -remotes: { - // Load as module: - mfe1": "http://localhost:3000/remoteEntry.js", - - // Load as script: - mfe2": "script mfe2@http://localhost:3000/remoteEntry.js", -} -``` - -### Advanced: Opting-out of Using EcmaScript Modules - -While moving forward with Modules and aligning with the CLI is a good idea, you might to temporarily opt-out of using them. This gives you some additional time for the migration as it brings back the behavior of Angular 12. For this, adjust your webpack configs as follows: - -```diff -module.exports = { - output: { - uniqueName: "dashboard", - publicPath: "auto", -+ scriptType: 'text/javascript' - }, - [...] -} -``` - -Also, don't use the settings introduced above for Angular 13.1+: - - -```diff -[...] -module.exports = { - [...] -- experiments: { -- outputModule: true -- }, - plugins: [ - new ModuleFederationPlugin({ -- library: { type: "module" }, - - [...] - }) - ] -}; -``` - -## SSR - -We have a sound solution including Schematics for SSR in Angular 12. However, because of a bug in Angular Universal 13, SSR is currently not supported for Angular 13. However, we are monitoring this situation and providing a solution as soon as these issues are fixed. - -## Example - -see https://github.com/manfredsteyer/mf-angular-13 +- [From version 12 and 13](./migration-guide-13.md) +- [From version 13 and 14](./migration-guide-14.md) \ No newline at end of file