Ce guide decrit la migration d'applications existantes vers une plateforme micro-frontend basee sur Native Federation, les modules ESM et l'Application Builder Angular.
L'architecture retenue dans ce repository utilise un seul contrat d'integration : chaque remote expose une API neutre mount / unmount, possede son framework et demarre sa propre application dans un conteneur DOM fourni par foundation.
Le shell ne charge jamais les routes, composants, providers ou instances Angular d'un remote. Cette decision permet notamment a un shell Angular 20 de charger une application Angular 16, 17, 18 ou 19, ou encore une application React, sans partager leur runtime de framework.
- Les remotes et le shell sont construits et deployes independamment.
- Chaque remote possede son framework, son routeur et ses dependances.
- Aucun package Angular, React, Router ou RxJS n'est partage par Native Federation.
- Le contrat d'integration ne contient que des types et valeurs JavaScript neutres.
- Le shell ne connait que
mount(container, options)etunmount(). - Le shell obtient les URL des remotes depuis une configuration runtime.
- Un remote reste executable en standalone.
- Un repository produit un seul build Native Federation, deploye une seule fois et utilisable a la fois comme SPA standalone et comme remote de
foundation. - Les communications transversales passent par des contrats navigateur neutres, jamais par l'injection de dependances d'un framework.
+-------------------------+
| cloud-config-repo |
| |
| runtime-config.json |
| menu.json |
| mf-manifest.json |
+------------+------------+
|
v
+------------------------+ charge au demarrage
| foundation |<------------------------+
| |
| menu + routeur shell |
| conteneur generique |
+-----------+------------+
|
| loadRemoteModule(name, './Application')
| application.mount(container, options)
|
+----+--------------------+
| |
v v
+-------------------+ +-------------------+
| inventory-remote | | billing-remote |
| runtime Angular A | | runtime Angular B |
| routeur interne | | routeur interne |
| mount / unmount | | mount / unmount |
+-------------------+ +-------------------+
Dans le PoC actuel :
foundationest le shell, servi surhttp://localhost:4200;inventory-remoteexpose./Application, servi surhttp://localhost:4201;billing-remoteexpose./Application, servi surhttp://localhost:4202;cloud-config-reposert la configuration runtime surhttp://localhost:4300;menufournit le composant de navigation de UI kit consomme au build parfoundation;state-bridgefournit un contrat navigateur neutre pour la demonstration d'etat transversal.
Les trois applications du PoC utilisent actuellement Angular 19 pour simplifier le developpement local. L'architecture ne depend toutefois pas d'un runtime Angular partage : le manifest genere de chaque remote contient "shared": [].
Les remotes activent actuellement le mode zoneless avec provideExperimentalZonelessChangeDetection(). Sous Angular 19, cette API est experimentale : sa signature et son comportement peuvent changer dans une version corrective. Le mode zoneless et provideZonelessChangeDetection() sont stables a partir d'Angular 20.2. Cette stabilisation constitue un argument important pour faire evoluer les applications remotes vers Angular 20.2 ou une version ulterieure, surtout lorsqu'une politique de support stable est requise. L'isolation des runtimes permet cette migration application par application sans imposer simultanement la meme version a foundation et aux autres remotes.
foundation/
inventory-remote/
billing-remote/
menu/
state-bridge/
cloud-config-repo/
Chaque application deployable garde son cycle de version, de build et de publication. Les packages distribues au build doivent utiliser un registre et des versions reproductibles dans un environnement multi-repository. Les chemins file:../... du PoC sont reserves au developpement local.
Pour Angular 19 :
ng add @angular-architects/native-federation@19 --project foundation --port 4200 --type dynamic-host
Utiliser la version du package adaptee a la version Angular du repository. La version de Native Federation d'un remote n'impose pas la version de son framework au reste de la plateforme; son artefact public reste un module ESM decrit par remoteEntry.json.
Native Federation utilise des import maps pour associer des noms de modules a des URL ESM. es-module-shims complete le chargeur natif du navigateur lorsque le support des import maps, des import maps multiples ou de leur ajout dynamique est absent ou incomplet.
Il ne transpile pas toute l'application et ne rend pas Internet Explorer compatible. Le navigateur doit au minimum supporter les modules ESM et import(). Sur un navigateur moderne, le shim detecte les fonctions natives et laisse essentiellement le navigateur charger les modules directement.
Dans foundation, le shim est copie comme asset et charge directement par index.html. Il ne doit pas etre ajoute au tableau polyfills d'Angular : son implementation contient des imports dynamiques intentionnels que Vite ne peut pas analyser statiquement, ce qui produit des avertissements inutiles en mode developpement.
Le target public Native Federation enveloppe un target Angular base sur l'Application Builder :
{
"build": {
"builder": "@angular-architects/native-federation:build",
"configurations": {
"production": {
"target": "foundation:esbuild:production"
},
"development": {
"target": "foundation:esbuild:development",
"dev": true
}
}
},
"esbuild": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"browser": "src/main.ts",
"polyfills": ["zone.js"],
"assets": [
{ "glob": "**/*", "input": "public", "output": "/" },
{
"glob": "es-module-shims.js",
"input": "node_modules/es-module-shims/dist",
"output": "/"
}
]
}
}
}Le head de src/index.html charge ensuite cet asset sans le faire passer par le pipeline Vite :
<script async src="es-module-shims.js"></script>Le target serve Native Federation delegue de la meme facon au serveur de developpement Angular.
La migration ne requiert ni un second repository, ni un second build, ni un second deploiement. Le build Native Federation produit dans le meme repertoire :
- le
index.htmlet les bundles necessaires a la SPA standalone; remoteEntry.jsonet le module./Applicationnecessaires afoundation.
Le target build existant devient le wrapper Native Federation. Il appelle un target interne de l'Application Builder, comme dans les applications de ce PoC :
{
"build": {
"builder": "@angular-architects/native-federation:build",
"configurations": {
"production": {
"target": "inventory-remote:esbuild:production"
},
"development": {
"target": "inventory-remote:esbuild:development",
"dev": true
}
}
},
"esbuild": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"browser": "src/main.ts",
"outputPath": "dist/inventory-remote",
"polyfills": ["es-module-shims"]
}
}
}Une seule commande construit tous les artefacts :
npm run build
Une seule publication de dist/inventory-remote/browser alimente ensuite les deux modes :
https://inventory.example.com/
L'application historique continue d'ouvrir cette URL comme une SPA normale.
https://inventory.example.com/remoteEntry.json
Le manifest de foundation pointe vers cette seconde URL sur le meme deploiement.
main.ts conserve uniquement le demarrage standalone. Quand foundation charge ./Application, Native Federation importe directement le fichier expose application.ts : le main.ts du remote n'est ni charge ni execute. application.ts reutilise la meme fonction de bootstrap pour monter l'application dans le conteneur fourni par le shell. Les composants, services, routes, assets et configurations metier ne sont pas dupliques.
La pipeline existante continue donc a construire et publier une seule Static Web App. La migration modifie sa chaine de build et ajoute les artefacts de federation, mais ne cree pas une nouvelle variante a gerer.
La configuration du shell est explicite :
const { withNativeFederation } = require('@angular-architects/native-federation/config');
module.exports = withNativeFederation({
name: 'foundation',
shared: {}
});shared: {} est essentiel. Sans ce champ, Native Federation peut appliquer son partage automatique. L'isolation exige que le shell et chaque remote embarquent leurs propres dependances de framework.
Cette architecture n'utilise pas singleton, strictVersion, requiredVersion ou shareAll pour Angular. La compatibilite entre Angular 16 et Angular 20 ne depend donc pas d'une negotiation de versions Angular : les deux applications ne s'echangent aucun objet Angular.
La route / porte la vue d'ensemble du shell. Une route configurée pour un remote utilise un composant hote generique. Ce composant :
- Cree un conteneur DOM.
- Charge
./ApplicationavecloadRemoteModule. - Verifie la presence de
mount. - Appelle
mount(container, { basePath }). - Appelle
unmountlorsque l'utilisateur quitte la route. - Affiche une erreur et permet un nouvel essai si le remote est indisponible.
Le shell utilise un UrlMatcher de prefixe pour laisser le remote posseder les segments sous son chemin. Par exemple, /inventory/alerts garde le conteneur Inventory actif, tandis que le routeur interne du remote traite alerts.
ng add @angular-architects/native-federation@19 --project inventory-remote --port 4201 --type remote
Executer cette commande a la racine du repository du remote. La schematic cree ou modifie principalement :
federation.config.jsa la racine du repository;angular.jsona la racine du repository;tsconfig.federation.jsona la racine du repository;src/main.ts;src/bootstrap.ts.
Relire ces fichiers apres l'execution : les sections suivantes remplacent le contrat Angular genere par le contrat d'application neutre retenu par cette architecture.
Creer ou modifier federation.config.js a la racine du repository :
const { withNativeFederation } = require('@angular-architects/native-federation/config');
module.exports = withNativeFederation({
name: 'inventoryRemote',
exposes: {
'./Application': './src/app/remote-entry/application.ts'
},
shared: {}
});Creer ensuite src/app/remote-entry/application.ts. Ce fichier est le seul point d'integration charge par foundation. Il expose une API JavaScript neutre :
export interface RemoteApplicationOptions {
basePath?: string;
}
export interface MountedRemoteApplication {
unmount(): void | Promise<void>;
}
export function mount(
container: HTMLElement,
options?: RemoteApplicationOptions
): Promise<MountedRemoteApplication>;
export function unmount(): void | Promise<void>;Dans src/app/remote-entry/application.ts, mount cree l'element racine dans le conteneur, appelle la fonction de bootstrap exportee par src/bootstrap.ts et conserve son ApplicationRef. unmount appelle ApplicationRef.destroy() puis retire l'element racine.
Le fichier expose doit appartenir au programme TypeScript utilise par l'Application Builder. Si tsconfig.app.json limite explicitement ses fichiers a src/main.ts, ajouter aussi l'entree exposee :
{
"files": [
"src/main.ts",
"src/app/remote-entry/application.ts"
]
}Sans cette inclusion, la construction des artefacts de federation echoue avec le message File '.../application.ts' is missing from the TypeScript compilation. Il vaut mieux ajouter precisement l'entree exposee que remplacer aveuglement la configuration par include: ["src/**/*.ts"], car cette derniere peut aussi incorporer les fichiers de tests au build applicatif.
Le PoC fournit les implementations completes suivantes comme references :
inventory-remote/src/app/remote-entry/application.ts;billing-remote/src/app/remote-entry/application.ts.
Les remotes Angular du PoC utilisent la detection de changements zoneless et n'embarquent pas zone.js dans leurs polyfills. Ils ne dependent donc pas du Zone eventuellement installe par le shell. Sous Angular 19, cette isolation repose toutefois sur l'API experimentale provideExperimentalZonelessChangeDetection(), qui ne fournit pas les memes garanties de stabilite qu'une API publique stable. Pour une cible de production conservatrice, migrer le remote vers Angular 20.2 ou une version ulterieure permet d'utiliser l'API stable provideZonelessChangeDetection(). Une application Angular plus ancienne qui depend de Zone doit plutot valider explicitement cette dependance globale ou adapter l'application a un fonctionnement sans Zone avant son montage dans la meme page.
Pour React, la meme interface peut appeler createRoot(container).render(...), puis root.unmount(). Le shell n'a pas besoin de connaitre le framework utilise.
Modifier src/bootstrap.ts pour exporter une fonction de bootstrap reutilisable, par exemple bootstrapInventory(basePath). Cette fonction appelle bootstrapApplication et fournit le chemin recu avec APP_BASE_HREF.
Modifier ou conserver src/app/app.routes.ts pour declarer toutes les routes internes du remote. Ces routes ne sont jamais exposees dans federation.config.js et ne sont jamais importees par foundation.
Le shell transmet seulement le chemin de base, par exemple /inventory/. La navigation interne produit ainsi /inventory/alerts, mais seul le routeur du remote decide quel composant afficher sous ce prefixe.
Fichiers concernes :
src/bootstrap.ts: bootstrap Angular,APP_BASE_HREF, providers et detection de changements;src/app/app.routes.ts: routes internes de l'application;- les composants de pages deja presents dans
src/app/.
Dans ce PoC, consulter inventory-remote/src/bootstrap.ts et inventory-remote/src/app/app.routes.ts pour un exemple complet.
Les styles declares dans les composants Angular sont charges avec les chunks de ces composants. Ils fonctionnent donc dans le mode standalone et dans le mode monte.
En revanche, le fichier global configure dans angular.json, par exemple src/styles.css, est reference automatiquement par le index.html standalone. Le shell ne charge pas le index.html du remote lorsqu'il importe ./Application; il ne faut donc pas supposer que ce stylesheet global sera present dans le mode monte.
Pour migrer une SPA existante :
- Conserver dans le stylesheet global uniquement les regles propres au document standalone, comme
htmletbody. - Deplacer les styles requis par l'application montee vers les
stylesoustyleUrlsde son composant racine et de ses composants enfants. - Encapsuler les selecteurs sous l'element racine du remote afin d'eviter les collisions avec
foundationet les autres applications. - Servir les images, fontes et autres assets depuis l'origine du remote avec des URL resolues par son build.
- Verifier visuellement les deux modes a partir des artefacts deployes, pas uniquement avec le serveur de developpement.
Dans ce PoC, inventory-remote/src/styles.css et billing-remote/src/styles.css ne contiennent que les valeurs par defaut du document standalone. Les styles necessaires au contenu monte sont declares dans les composants sous src/app/.
Les fichiers de configuration propres a un remote, par exemple assets/app-config.json, font egalement partie de ses assets. Le remote doit les charger depuis sa propre origine de deploiement, et non relativement a la page de foundation. Le configBaseUrl de foundation sert uniquement a charger la configuration du shell (menu.json et mf-manifest.json); il n'est pas la base des assets internes des remotes.
Une URL commencant par / est resolue depuis l'origine de la page. Elle n'est donc correcte que si foundation et les assets du remote partagent volontairement cette origine et ce chemin public. Pour un remote deploye sur une origine distincte, son build ou son bootstrap doit produire une URL absolue ou relative a son propre deploiement. Le shell ne doit pas connaitre la structure interne des assets du remote.
Modifier src/main.ts pour importer dynamiquement src/bootstrap.ts, puis appeler la fonction de bootstrap avec / comme chemin de base. Cette entree est reservee au mode standalone :
import('./bootstrap')
.then(({ bootstrapInventory }) => bootstrapInventory())
.catch((error) => console.error('Inventory remote bootstrap failed.', error));Le shell initialise Native Federation avant de charger le remote. Il importe ensuite directement ./Application et n'execute jamais le main.ts du remote.
Un remote qui declare shared: {} n'a aucune dependance partagee a enregistrer dans une import map pour son demarrage standalone. Son main.ts peut donc demarrer directement l'application, comme ci-dessus. Un appel standalone a initFederation() reste possible lorsqu'il est necessaire, mais il doit alors pouvoir resoudre remoteEntry.json depuis l'URL publique courante. Pour une SPA historique dont la page, le baseHref et le repertoire de bundles sont differents, cet appel peut chercher remoteEntry.json au mauvais emplacement et ne doit pas etre ajoute sans configuration explicite.
Conserver l'element racine de l'application dans src/index.html, par exemple <inv-root></inv-root>. L'application peut ainsi etre ouverte directement sur son propre port sans foundation.
Fichiers concernes :
src/main.ts: entree du mode standalone;src/bootstrap.ts: bootstrap reutilise par les deux modes;src/index.html: element racine du mode standalone;src/app/remote-entry/application.ts: entree du mode monte dansfoundation.
Executer le build a la racine du repository :
npm run build
Le build doit produire les fichiers suivants sous le repertoire configure par outputPath dans angular.json :
dist/inventory-remote/browser/remoteEntry.json
dist/inventory-remote/browser/Application-<hash>.js
Le remoteEntry.json doit contenir :
{
"name": "inventoryRemote",
"shared": [],
"exposes": [
{
"key": "./Application",
"outFileName": "Application-<hash>.js"
}
]
}Une liste shared non vide pour Angular indique une rupture du modele d'isolation.
La migration reste progressive :
- Conserver les composants, services et routes metier.
- Extraire le bootstrap dans une fonction reutilisable.
- Ajouter le module
application.tsqui geremountetunmount. - Transmettre un
basePathau routeur interne. - Ajouter
federation.config.jsavecshared: {}. - Publier le build Native Federation sur la Static Web App existante.
- Tester sur ce meme deploiement le mode standalone et le mode monte dans
foundation.
Il ne faut pas exposer des composants Angular, des Routes, des injecteurs, des Observables RxJS ou des services de framework dans le contrat public.
Le menu du shell est pilote par configuration et rendu par CloudMenuComponent, fourni par le package de UI kit @poc-cloud/menu. foundation integre sa propre copie de cette dependance au build; le composant ne devient pas un singleton runtime de federation et n'est pas transmis aux remotes.
Pour supporter plusieurs frameworks et plusieurs versions :
- preferer des tokens CSS, SVG et assets neutres;
- accepter qu'une bibliotheque Angular de composants soit compilee independamment dans chaque application Angular;
- ne jamais transmettre une instance de composant ou de service UI au travers du contrat
mount; - isoler les styles avec une convention de classes, des couches CSS ou Shadow DOM si necessaire.
Le serveur de configuration contient :
public/config/runtime-config.json
public/config/menu.json
public/config/mf-manifest.json
Exemple de menu :
[
{
"id": "inventory",
"label": "Inventory",
"route": "/inventory",
"description": "Warehouse stock and replenishment",
"remoteName": "inventoryRemote",
"exposedModule": "./Application"
}
]Exemple de manifest riche :
{
"inventoryRemote": {
"type": "module",
"remoteEntry": "https://inventory.example.com/remoteEntry.json",
"version": "1.0.0"
}
}Le shell transforme ce document en map Native Federation :
{
"inventoryRemote": "https://inventory.example.com/remoteEntry.json"
}remoteEntry cible toujours remoteEntry.json, jamais l'ancien remoteEntry.js webpack.
L'ordre de demarrage est :
- Charger la configuration runtime.
- Charger et valider le menu et le manifest.
- Transformer le manifest riche en map d'URL.
- Appeler
initFederation. - Demarrer Angular dans
foundation. - Creer les routes top-level depuis le menu.
- Monter un remote seulement lorsque sa route devient active.
Le rafraichissement runtime peut rappeler initFederation avec la nouvelle map, mettre a jour le routeur du shell et signaler les nouvelles versions disponibles.
- Ajouter Native Federation au repository existant.
- Conserver son framework et son routeur internes.
- Implementer
mountetunmount. - Exposer
./Application. - Definir explicitement
shared: {}. - Tester le remote en standalone.
- Construire et publier
dist/<remote>/browser. - Ajouter son
remoteEntry.jsonau manifest. - Ajouter sa route, son nom et
./Applicationau menu. - Tester le montage, la navigation interne, le demontage et le remontage.
Un remote React ou d'un autre framework suit exactement le meme contrat public.
Le contrat mount peut recevoir des donnees serialisables et des callbacks neutres :
interface RemoteApplicationOptions {
basePath: string;
context?: {
locale: string;
userId: string;
};
navigate?: (url: string) => void;
}Pour les evenements continus, utiliser un canal fourni par le navigateur :
CustomEventsurwindow;- un
EventTargetneutre; BroadcastChannelsi plusieurs onglets sont concernes;- une API HTTP ou un backend d'evenements pour l'etat durable.
Le state-bridge du PoC est un package TypeScript sans Angular. Chaque application en embarque sa propre copie, mais les copies retrouvent le meme client de demonstration au moyen d'une cle sur globalThis. Il n'est pas partage par Native Federation et ne doit pas exposer de types Angular ou RxJS.
Les donnees sensibles ne doivent pas etre placees aveuglement sur window. En production, definir un contrat versionne, valider chaque message et limiter les capacites transmises au remote.
Publier le contenu de :
dist/<projet>/browser
Configurer CORS pour remoteEntry.json, les modules ESM, les chunks et les assets. Les fichiers JavaScript doivent etre servis avec un type MIME JavaScript et les fichiers JSON avec un type JSON.
Le fallback SPA ne doit pas reecrire les artefacts de federation vers index.html. Une requete manquante pour un chunk doit retourner une erreur HTTP, pas du HTML avec un statut 200.
Politique de cache recommandee :
remoteEntry.json, manifest, menu etindex.html: cache court ou revalidation;- chunks avec hash : cache long et
immutable; - conservation temporaire des anciens chunks pour permettre un rollback.
- Construire et tester le remote en standalone.
- Construire et tester son contrat
mount/unmount. - Publier ses artefacts hashes.
- Verifier l'URL publique de
remoteEntry.jsonet sa listesharedvide. - Publier le manifest et le menu mis a jour.
- Tester le remote depuis
foundation. - Conserver l'ancien artefact pendant la fenetre de rollback.
Les mises a niveau Angular n'exigent pas de mise a niveau coordonnee du shell et des autres remotes, car aucune instance Angular n'est partagee. Chaque repository peut evoluer selon son propre calendrier, sous reserve de conserver le contrat JavaScript public.
foundation peut utiliser SSR et hydration selon la documentation de sa version Angular. Les remotes montes par le navigateur sont, par defaut, des ilots rendus cote client apres l'hydration du shell.
Rendre aussi ces ilots sur le serveur demande un contrat supplementaire propre au framework du remote. Ce PoC n'implemente pas ce contrat et ne pretend pas fournir du SSR pour les contenus distants.
- Le shell charge la configuration avant son bootstrap.
- Le shell expose un conteneur generique, pas un injecteur Angular.
- Le shell appelle
unmounta la sortie d'une route. - Les erreurs et timeouts de montage sont geres.
-
foundation/federation.config.jscontientshared: {}.
- Le remote expose
./Application. -
mountaccepte unHTMLElementet un objet neutre. -
unmountdetruit le runtime et nettoie le DOM. - Le routeur interne utilise le
basePathfourni. - Le remote fonctionne en standalone.
-
remoteEntry.jsoncontient"shared": [].
- Le manifest cible
remoteEntry.json. - CORS et les types MIME sont corrects.
- Le fallback SPA exclut les artefacts.
- Les fichiers d'entree sont revalides.
- Les chunks hashes sont conserves pour le rollback.
Executer une premiere fois :
- Dans
state-bridge:npm install, puisnpm run build. - Dans
menu:npm install, puisnpm run buildpour produire le package consomme parfoundation. - Dans les trois applications et
cloud-config-repo:npm install.
Demarrer quatre terminaux :
cloud-config-repo:npm startinventory-remote:npm startbilling-remote:npm startfoundation:npm start
Ouvrir http://localhost:4200, tester /inventory, /inventory/alerts, /billing et /billing/approvals, puis revenir a / pour verifier le demontage. Ouvrir aussi les remotes directement sur leurs ports.
Pour les builds :
npm run build
- un host Native Federation pilote par configuration;
- deux applications distantes montees avec
mount/unmount; - un runtime Angular prive dans chaque application;
- aucune dependance partagee par Native Federation;
- un routeur interne et un mode standalone pour chaque remote;
- un contrat d'etat transversal sans dependance Angular;
- un manifest externe enrichi;
- une base permettant d'ajouter des remotes Angular de versions differentes ou des remotes React.
- Native Federation pour Angular : https://github.com/native-federation/angular-adapter
- ES Module Shims : https://github.com/guybedford/es-module-shims
- Migration Angular vers le nouveau build system : https://angular.dev/tools/cli/build-system-migration
- Migration Module Federation vers Native Federation : https://github.com/angular-architects/module-federation-plugin/blob/main/libs/native-federation/docs/migrate.md