MFELoader
Costruttore
class MFELoader {
constructor()
}
Nessun parametro. Crea due Map interne:
loadedModules: Map<string, MFEModule>— cache dei moduli caricatiloadingPromises: Map<string, Promise<MFEModule>>— dedup delle richieste concorrenti
mfeLoader è deprecato per evitare conflitti di cache.Metodi
loadRemote(config: MFEConfig): Promise<MFEModule>
Carica dinamicamente un MFE. Gestisce caching, dedup, validazione URL, CSS auto-loading, validazione modulo e chiamata di onLoad().
const mfe = await loader.loadRemote({
name: 'crm-module',
url: 'https://mfes.example.com/crm/index.js',
scope: 'crm',
module: './module',
})
console.log(await mfe.getManifest())
Comportamento:
- Se
loadedModules.has(name)→ ritorna il modulo cached - Se
loadingPromises.has(name)→ ritorna la promise già in corso - Altrimenti: avvia caricamento e mette la promise in
loadingPromises - Al completamento (success o fail) rimuove la promise da
loadingPromises - In caso di successo, salva il modulo in
loadedModules
Throw se:
- URL non valido (
new URL(url)fallisce) - Protocollo bloccato:
data:,javascript:,blob:,vbscript: - Protocollo non consentito (qualsiasi diverso da
http:/https:) - Network error durante HEAD del CSS sibling (network/CORS issue, non 404)
- Stylesheet HEAD ritorna status
>= 400diverso da 404 <link>eventonerror(CSS esiste ma non caricabile)- Modulo manca di
getManifest/mount/unmount - Hook opzionale presente ma non funzione
clearCache(name: string): void
Rimuove un singolo modulo dalla cache:
- elimina da
loadedModules - elimina da
loadingPromises - rimuove
<link[data-mfe="{name}"]>dal<head>
loader.clearCache('crm-module')
// La prossima loadRemote('crm-module') ricaricherà da rete
clearAllCache(): void
Rimuove tutti i moduli dalla cache:
- svuota
loadedModuleseloadingPromises - rimuove tutti i
<link[data-mfe]>dal<head>
isLoaded(name: string): boolean
if (loader.isLoaded('crm-module')) {
// già caricato e disponibile in cache
}
isLoading(name: string): boolean
if (loader.isLoading('crm-module')) {
// caricamento in corso (potrebbe essere quasi pronto)
}
Sicurezza URL
Il loader applica una whitelist sui protocolli:
static readonly ALLOWED_PROTOCOLS = new Set(['https:', 'http:'])
static readonly BLOCKED_PROTOCOLS = new Set([
'data:',
'javascript:',
'blob:',
'vbscript:',
])
Esempi:
// ✓ OK
loader.loadRemote({ url: 'https://mfes.example.com/crm/index.js', /* ... */ })
loader.loadRemote({ url: 'http://localhost:5173/crm/index.js', /* ... */ })
// ✗ Throw: "Protocollo 'data:' bloccato per sicurezza..."
loader.loadRemote({ url: 'data:application/javascript,alert(1)', /* ... */ })
// ✗ Throw: "Protocollo 'file:' non consentito..."
loader.loadRemote({ url: 'file:///etc/passwd', /* ... */ })
// ✗ Throw: "URL non valido..."
loader.loadRemote({ url: 'index.js', /* ... */ }) // URL relativa
CSS auto-loading
Convenzione: il bundle JS ha uno stylesheet sibling con lo stesso path ma estensione .css.
URL bundle: https://mfes.example.com/crm/index.js
URL CSS atteso: https://mfes.example.com/crm/index.css
Il loader:
- fa
fetch(cssUrl, { method: 'HEAD' }) - se status
404→ skip silenzioso (CSS opzionale, è OK) - se status
>= 400(e ≠ 404) → throwHTTP {status} loading stylesheet - altrimenti → inietta
<link rel="stylesheet" href="{cssUrl}" data-mfe="{name}">e aspettaonload/onerror
I CSS sono tracciati per nome: clearCache(name) rimuove anche il <link> corrispondente.
Validazione struttura modulo
Il loader accetta sia default export sia named exports:
interface DynamicModuleExports {
default?: Partial<MFEModule>
getManifest?: MFEModule['getManifest']
mount?: MFEModule['mount']
unmount?: MFEModule['unmount']
onLoad?: MFEModule['onLoad']
}
Risoluzione: se esiste module.default lo usa, altrimenti usa il modulo come oggetto.
Metodi obbligatori (throw se mancanti o non funzioni):
getManifest()mount(container, context)unmount()
Metodi opzionali (validati solo se presenti):
onLoadonBeforeUnmountonRouteChangegetViewComponent
Se presenti ma non funzioni: MFE "{name}" has invalid optional method "{method}": expected function, got {typeof}.
Pattern di errore
try {
const mfe = await loader.loadRemote(config)
} catch (err) {
// Il messaggio è sempre:
// 'Failed to load MFE "{name}" from {url}: {causa}'
console.error(err.message)
}
Test
Esempio Vitest con mock di import() e DOM:
import { describe, it, expect, vi } from 'vitest'
import { MFELoader } from '@pzeta/mfe-contracts/loader'
describe('MFELoader', () => {
it('dedup richieste concorrenti', async () => {
const loader = new MFELoader()
const config = { name: 'x', url: 'https://x.com/x.js', scope: 'x', module: './m' }
// entrambe le chiamate dovrebbero condividere la stessa Promise interna
const [a, b] = await Promise.all([
loader.loadRemote(config),
loader.loadRemote(config),
])
expect(a).toBe(b)
})
})