Migrazione alla 0.5
La 0.5 corregge bug che cambiavano il comportamento osservabile, quindi alcune di queste correzioni rompono il codice scritto per la 0.4. Questa pagina elenca ogni breaking change per pacchetto, con il codice da modificare. Aggiorna insieme tutti i pacchetti tuvix.js e @tuvix.js/* a ^0.5.0: non è supportato mescolare adapter 0.4 e 0.5 nella stessa pagina.
pnpm up --latest "tuvix.js" "@tuvix.js/*"I progetti creati con create-tuvix-app prima della 0.5 dipendono da ^0.1.0, che non si risolve mai nella 0.4 o nella 0.5. Modifica quei range a mano.
Core
L'orchestratore usa il bus globale
Nella 0.4 ogni orchestratore aveva un bus privato, separato da getGlobalBus(). Nella 0.5 il bus dell'orchestratore è getGlobalBus(), a meno che tu non passi le opzioni eventBus, e destroy() lascia attivo il bus globale.
// 0.4: two buses; apps listening on getGlobalBus() missed orchestrator events
const orchestrator = createOrchestrator();
orchestrator.getEventBus() === getGlobalBus(); // false// 0.5
import { createOrchestrator, getGlobalBus } from 'tuvix.js';
const shared = createOrchestrator();
console.log(shared.getEventBus() === getGlobalBus()); // true
const isolated = createOrchestrator({ eventBus: {} }); // private bus, as in 0.4
console.log(isolated.getEventBus() === getGlobalBus()); // falseSe esponevi il bus dell'orchestratore su window perché le app potessero trovarlo, puoi eliminare quel codice e usare il bus del contesto di mount o getGlobalBus().
I cambi di route non toccano le app manuali
La 0.4 smontava al cambio di route successivo un'app priva sia di activeWhen sia di una route del router, e lo stesso valeva per le app con mountWhenVisible. Nella 0.5 la riconciliazione tocca solo le app controllate da activeWhen o da una route. Se contavi su una navigazione per rimuovere un'app montata manualmente, smontala tu:
import { createOrchestrator } from 'tuvix.js';
const orchestrator = createOrchestrator();
await orchestrator.unmountApp('cookie-banner');sandbox: true esegue il rendering in una shadow root
Nella 0.4 l'opzione sandbox di register non faceva nulla. Nella 0.5 sandbox: true monta l'app dentro una shadow root e carica lì gli styles della sua entry. Il CSS della pagina non raggiunge più l'app, e gli stili della entry dell'app non raggiungono più la pagina. Rimuovi sandbox: true dalle app che dipendono dagli stili della pagina. Vedi Sandboxing.
prefetch: 'hover'
La 0.4 eseguiva il prefetch di tutte le app al primo movimento del mouse in qualsiasi punto della pagina. La 0.5 esegue il prefetch di un'app quando il puntatore entra in un link dello stesso origin il cui percorso la attiva, o quando quel link riceve il focus. Usa 'idle' o 'immediate' per scaricare subito tutte le app.
OrchestratorConfig.shared è stato rimosso
L'orchestratore non leggeva mai shared. Eliminalo e condividi le dipendenze con @tuvix.js/module-federation o con le import map.
Pattern di activeWhen
Le stringhe di activeWhen ora usano matchPath. Una query string o un hash non smontano più un'app, i segmenti :param funzionano e caratteri come . e ( vengono confrontati letteralmente.
// 0.4: `.` was a regex wildcard and matched '/v1x0'
activeWhen: '/v1.0/*'
// 0.4: query strings broke matching, so people wrote functions
activeWhen: (path) => path.split('?')[0].startsWith('/dashboard')import { createOrchestrator } from 'tuvix.js';
const orchestrator = createOrchestrator();
orchestrator.register({
name: 'dashboard',
entry: () => import('./apps/dashboard'),
container: '#app',
activeWhen: '/dashboard/*', // also active on /dashboard?tab=1
});Router
La route '/' e RouteConfig.exact
I percorsi delle route corrispondono ovunque all'intero percorso. Una route '/' ora si attiva solo su /; nella 0.4 getActiveApps() la trattava come corrispondente a ogni percorso, mentre match() la trattava come esatta. RouteConfig.exact è stato rimosso, perché non cambiava mai nulla.
// 0.4
routes: [{ path: '/', app: 'shell-nav' }, { path: '/settings', app: 'settings', exact: true }]// 0.5
import { createRouter } from 'tuvix.js';
const router = createRouter({
routes: [
{ path: '/*', app: 'shell-nav' }, // every path
{ path: '/settings', app: 'settings' }, // /settings only
],
});
void router;activeWhen: '/' su register corrisponde ancora a ogni percorso, perché le stringhe di activeWhen usano il confronto per prefisso.
push e replace aggiungono la base
// 0.4 with base: '/app'
router.push('/app/settings'); // you had to add the base yourself// 0.5
import { createRouter } from 'tuvix.js';
const router = createRouter({ base: '/app', routes: [] });
await router.push('/settings'); // URL: /app/settingsSe nella 0.5 passi /app/settings, l'URL scritto è /app/app/settings. Anche una guard che annulla una navigazione avanti/indietro ripristina l'URL con la base.
Altre modifiche al router
baseviene rimossa solo al confine di un segmento. Conbase: '/app',/applicationresta/application.pathToRegex()restituisce unaRegExpin cache: non modificarla. Il suo secondo argomentoexactè stato rimosso.- I caratteri speciali delle regex nei pattern vengono confrontati letteralmente. Un
*in mezzo a un pattern corrisponde a un solo segmento. matchRoute()eisPathActive()ignorano un#hashnel percorso.
Loader
Opzioni rimosse
LoaderOptions.fetch e EntryConfig.html sono stati rimossi. Il loader non li leggeva mai: eliminali dalla tua configurazione.
// 0.4
new ModuleLoader({ fetch: myFetch });
orchestrator.register({ name: 'x', entry: { scripts: ['/x.js'], html: '<div></div>' }, container: '#x' });// 0.5
import { ModuleLoader } from '@tuvix.js/loader';
const loader = new ModuleLoader({ timeout: 10000 });
void loader;Gli script modulo vengono eseguiti una volta per URL
Dopo unload(name), un'app registrata da uno script type: 'module' non si carica di nuovo dallo stesso URL, perché i browser eseguono uno script modulo una sola volta per URL. Per le app che scarichi e ricarichi usa una entry con funzione loader:
import { createOrchestrator } from 'tuvix.js';
const orchestrator = createOrchestrator();
orchestrator.register({
name: 'editor',
entry: () => import('./apps/editor'),
container: '#editor',
});Per un modulo in cache, load() ora riporta la sua duration reale invece di 0.
Module Federation
federatedEntry e createFederatedEntry restituiscono una funzione loader invece di { scripts: [remoteEntry.js] }. La entry della 0.4 caricava lo script del container e poi falliva con "no exports found".
// 0.4
orchestrator.register({
name: 'dashboard',
entry: federatedEntry({ name: 'dashboard', url: 'https://cdn.example.com/dashboard/remoteEntry.js' }),
container: '#main',
});// 0.5
import { createOrchestrator } from '@tuvix.js/core';
import { createFederatedLoader, federatedEntry } from '@tuvix.js/module-federation';
const orchestrator = createOrchestrator();
orchestrator.register({
name: 'dashboard',
entry: federatedEntry(
{ name: 'dashboard', url: 'https://cdn.example.com/dashboard/remoteEntry.js' },
'./App', // exposed module path
),
container: '#main',
activeWhen: '/dashboard/*',
});
const remotes = createFederatedLoader({
remotes: [{ name: 'settings', url: 'https://cdn.example.com/settings/remoteEntry.js' }],
shareScope: {}, // replaces 0.4 `shared`
});
orchestrator.register({
name: 'settings',
entry: remotes.createFederatedEntry('settings', './App'),
container: '#main',
activeWhen: '/settings/*',
});FederatedLoaderConfig.sharedè sostituito dashareScope. La 0.4 non leggeva maishared.loadRemoteContainerrestituisceFederatedContainerinvece diany.
React
- L'HTML server di
TuvixReactAppora contiene i marker di commento di Suspense (<!--$-->...<!--/$-->). Esegui rendering e hydration con@tuvix.js/react0.5 su entrambi i lati. mountviene rifiutata quando il primo rendering lancia un errore. Nella 0.4 l'errore arrivava al gestore degli errori della root di React emountsi risolveva. Ora l'orchestratore mostra ilfallbackdell'app e chiamaonError.unmountsu un container che il modulo non ha mai montato non fa nulla. La 0.4 lo svuotava.
Vue
update() non scrive più app.config.globalProperties.$tuvixProps. Ora le props sono reattive:
<!-- 0.4 -->
<template>{{ $tuvixProps.theme }}</template><!-- 0.5 -->
<script setup lang="ts">
defineProps<{ theme?: string }>();
</script>
<template>{{ theme }}</template>I discendenti possono usare inject('tuvixProps'), che restituisce lo stesso oggetto reattivo.
VueSsrMicroAppConfignon accetta piùssr. Lo impostacreateSsrVueMicroApp.unmountsu un container che il modulo non ha mai montato non fa nulla.
Svelte
- Senza
ssr,createSvelteMicroAppsvuota il container prima del rendering. La 0.4 aggiungeva i componenti Svelte 3/4 dopo il markup esistente. UsacreateSsrSvelteMicroAppper mantenere il markup del server ed eseguirne la hydration. unmountsu un container che il modulo non ha mai montato, o il cui mount è fallito, non fa nulla. La 0.4 lo svuotava. Ora tutti e quattro gli adapter si comportano così.renderSvelteToStringnon ripiega più suApp.renderquandosvelte/servernon si carica. Se passirenderusa Svelte 5; altrimenti segue il percorso di Svelte 5 quandosvelteesportamount, e chiamaApp.rendersolo per Svelte 3/4. Un errore di import o di rendering di Svelte 5 arriva fino a te.
I componenti Svelte 5 funzionano nella 0.5; nella 0.4 createSvelteMicroApp falliva con loro. Con Svelte 5, update() rimonta il componente a meno che tu non passi nella configurazione SvelteMap da svelte/reactivity, che aggiorna le props sul posto:
import { SvelteMap } from 'svelte/reactivity';
import { createSvelteMicroApp } from '@tuvix.js/svelte';
import Profile from './Profile.svelte';
export default createSvelteMicroApp({ name: 'profile', App: Profile, SvelteMap });Angular
Nuova forma della configurazione
// 0.4
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
export default createAngularMicroApp({
name: 'admin',
module: AppModule,
platform: platformBrowserDynamic,
selector: 'app-root',
});// 0.5
import { createAngularMicroApp } from '@tuvix.js/angular';
import { AppComponent } from './app.component';
import { AppModule } from './app.module';
export default createAngularMicroApp({
name: 'admin',
component: AppComponent, // required
module: AppModule, // only when AppComponent is declared in an NgModule
});platform e compilerOptions sono stati rimossi. Togli import '@angular/compiler' dalla tua entry: l'adapter carica il compilatore per i componenti JIT.
Zoneless per impostazione predefinita
Le app girano senza zone.js a meno che tu non passi zone: true. Su Angular 15-17 gli eventi DOM non attivano più la change detection: chiama ChangeDetectorRef.detectChanges() negli handler, oppure importa zone.js nella tua entry e passa zone: true. Angular 18+ pianifica la change detection senza zone.js. Vedi Angular.
Le props sono input
update() non scrive più window.__TUVIX_ANGULAR_PROPS__. Dichiara invece proprietà @Input(): l'adapter le imposta con setInput.
// 0.4
ngOnInit() {
this.userId = (window as any).__TUVIX_ANGULAR_PROPS__?.admin?.userId;
}// 0.5
import { Component, Input } from '@angular/core';
@Component({ selector: 'app-admin', standalone: true, template: '{{ userId }}' })
export class AdminComponent {
@Input() userId = '';
}Altre modifiche ad Angular
createSsrAngularMicroAppnon aggiunge piùprovideClientHydration(). Esegue di nuovo il rendering nell'elemento host del server.unmountsu un container che il modulo non ha mai montato non fa nulla.
Sandbox
Sandbox({ css: false }).activate(container)restituiscenulle non collega alcuna shadow root.ISandbox.activaterestituisceShadowRoot | null: controllanullprima di chiamareaddStyle.- In modalità strict,
JsSandboxlancia unTypeErrorsu una scrittura bloccata. La 0.4 registrava un avviso e salvava comunque il valore. Aggiungi adallowedGlobalsle variabili globali che il tuo codice scrive.
import { createSandbox } from '@tuvix.js/sandbox';
declare const container: HTMLElement;
const sandbox = createSandbox({ css: true, js: false });
const shadow = sandbox.activate(container);
if (shadow) sandbox.css.addStyle(shadow, ':host { display: block; }');Server
createServerRenderer().render()restituiscestatusCode: 502quando almeno un'app corrisponde al percorso e tutti i fetch dei frammenti falliscono. Restituisce ancora 200 quando nessuna app corrisponde o quando almeno un frammento va a buon fine.createMiddlewareinvia quello status. Il renderer in streaming invia sempre 200.createMiddlewareecreateStreamingMiddlewarechiamanonext(error)invece dinext()quando il rendering lancia un errore, così Express instrada la richiesta al tuo gestore degli errori.
Correzioni non breaking da conoscere
require('@tuvix.js/core')e gli altri entry point CommonJS funzionano. Nella 0.4 cinque pacchetti facevano puntarerequirea file che la build non generava mai.getGlobalBus()conserva la sua istanza suwindow, quindi micro app con bundle separati condividono un unico bus.- Gli adapter e
defineMicroAppemettonotuvix:mounted,tuvix:unmountedetuvix:error. Vedi Eventi del ciclo di vita. - Un
bootstrap()che lancia un errore viene eseguito di nuovo al mount successivo. La 0.4 da quel momento lo saltava. register()dopostart()monta subito l'app quando il percorso corrente la attiva, e inizia a osservare un'app conmountWhenVisible.- Ogni errore del ciclo di vita emette
tuvix:app:errorsul bus con{ name, error, phase }. La 0.4 lo emetteva solo per gli errori di caricamento e di mount. maxListenersavvisa conconsole.warnuna volta per evento, con o senzadebug.orchestrator.connectRouter()sostituisce i bridge basati susetTimeout+reconcile(). Vedi Router esterno.