Migrer vers 0.5
La version 0.5 corrige des bugs qui modifiaient le comportement observable, et certains de ces correctifs cassent du code écrit pour 0.4. Cette page liste chaque changement cassant par paquet, avec le code à modifier. Mettez à jour tous les paquets tuvix.js et @tuvix.js/* vers ^0.5.0 en même temps : mélanger des adaptateurs 0.4 et 0.5 sur une même page n'est pas pris en charge.
pnpm up --latest "tuvix.js" "@tuvix.js/*"Les projets créés avec create-tuvix-app avant 0.5 dépendent de ^0.1.0, qui ne se résout jamais vers 0.4 ni vers 0.5. Modifiez ces plages à la main.
Core
L'orchestrateur utilise le bus global
En 0.4, chaque orchestrateur avait un bus privé, distinct de getGlobalBus(). En 0.5, le bus de l'orchestrateur est getGlobalBus() sauf si vous passez des options eventBus, et destroy() laisse le bus global en vie.
// 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()); // falseSi vous exposiez le bus de l'orchestrateur sur window pour que les apps le trouvent, vous pouvez supprimer ce code et utiliser le bus du contexte de montage ou getGlobalBus().
Les changements de route ne touchent plus les apps manuelles
En 0.4, une app sans activeWhen ni route du routeur était démontée au changement de route suivant, de même que les apps mountWhenVisible. En 0.5, la réconciliation ne touche que les apps contrôlées par activeWhen ou par une route. Si vous comptiez sur une navigation pour retirer une app montée manuellement, démontez-la vous-même :
import { createOrchestrator } from 'tuvix.js';
const orchestrator = createOrchestrator();
await orchestrator.unmountApp('cookie-banner');sandbox: true affiche l'app dans un shadow root
En 0.4, sandbox dans register ne faisait rien. En 0.5, sandbox: true monte l'app dans un shadow root et y charge les styles de son entrée. Le CSS de la page n'atteint plus l'app, et les styles d'entrée de l'app n'atteignent plus la page. Retirez sandbox: true des apps qui dépendent des styles de la page. Voir Sandbox.
prefetch: 'hover'
En 0.4, chaque app était préchargée au premier mouvement de souris n'importe où sur la page. En 0.5, une app est préchargée quand le pointeur entre sur un lien de même origine dont le chemin l'active, ou quand ce lien reçoit le focus. Utilisez 'idle' ou 'immediate' pour charger toutes les apps dès le départ.
OrchestratorConfig.shared n'existe plus
L'orchestrateur ne lisait jamais shared. Supprimez-le, et partagez les dépendances avec @tuvix.js/module-federation ou des import maps.
Motifs activeWhen
Les chaînes activeWhen utilisent désormais matchPath. Une query string ou un hash ne démonte plus une app, les segments :param fonctionnent, et des caractères comme . et ( correspondent littéralement.
// 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
});Routeur
La route '/' et RouteConfig.exact
Les chemins de route correspondent au chemin entier partout. Une route '/' ne s'active plus que sur / ; en 0.4, getActiveApps() la traitait comme correspondant à tous les chemins, alors que match() la traitait comme exacte. RouteConfig.exact n'existe plus, car il ne changeait jamais rien.
// 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: '/' dans register correspond toujours à tous les chemins, car les chaînes activeWhen utilisent une correspondance par préfixe.
push et replace ajoutent 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/settingsEn 0.5, passer /app/settings écrit /app/app/settings. Un guard qui annule une navigation arrière ou avant restaure aussi l'URL avec la base.
Autres changements du routeur
basen'est retirée que sur une frontière de segment. Avecbase: '/app',/applicationreste/application.pathToRegex()renvoie uneRegExpmise en cache ; ne la modifiez pas. Son second argumentexactn'existe plus.- Les caractères d'expression régulière dans les motifs correspondent littéralement. Un
*au milieu d'un motif correspond à un seul segment. matchRoute()etisPathActive()ignorent un#hashdans le chemin.
Loader
Options supprimées
LoaderOptions.fetch et EntryConfig.html n'existent plus. Le loader ne les lisait jamais ; supprimez-les de votre configuration.
// 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;Les scripts module s'exécutent une fois par URL
Après unload(name), une app dont le script type: 'module' s'est enregistré lui-même ne se recharge pas depuis la même URL, car les navigateurs exécutent un script module une seule fois par URL. Utilisez une entrée sous forme de fonction de chargement pour les apps que vous déchargez puis rechargez :
import { createOrchestrator } from 'tuvix.js';
const orchestrator = createOrchestrator();
orchestrator.register({
name: 'editor',
entry: () => import('./apps/editor'),
container: '#editor',
});load() pour un module en cache indique désormais sa vraie duration au lieu de 0.
Module Federation
federatedEntry et createFederatedEntry renvoient une fonction de chargement au lieu de { scripts: [remoteEntry.js] }. L'entrée de 0.4 chargeait le script du conteneur puis échouait avec « 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.sharedest remplacé parshareScope. 0.4 ne lisait jamaisshared.loadRemoteContainerrenvoieFederatedContainerau lieu deany.
React
- Le HTML serveur de
TuvixReactAppcontient désormais des marqueurs de commentaire Suspense (<!--$-->...<!--/$-->). Faites le rendu et l'hydratation avec@tuvix.js/react0.5 des deux côtés. mountrejette sa promesse quand le premier rendu lève une erreur. En 0.4, l'erreur atteignait le gestionnaire d'erreurs racine de React etmountse résolvait. L'orchestrateur affiche maintenant lefallbackde l'app et appelleonError.unmountsur un conteneur que le module n'a jamais monté ne fait rien. 0.4 le vidait.
Vue
update() n'écrit plus app.config.globalProperties.$tuvixProps. Les props sont maintenant réactives :
<!-- 0.4 -->
<template>{{ $tuvixProps.theme }}</template><!-- 0.5 -->
<script setup lang="ts">
defineProps<{ theme?: string }>();
</script>
<template>{{ theme }}</template>Les composants descendants peuvent appeler inject('tuvixProps'), qui renvoie le même objet réactif.
VueSsrMicroAppConfign'accepte plusssr.createSsrVueMicroApple définit.unmountsur un conteneur que le module n'a jamais monté ne fait rien.
Svelte
- Sans
ssr,createSvelteMicroAppvide le conteneur avant le rendu. 0.4 ajoutait les composants Svelte 3/4 après le balisage existant. UtilisezcreateSsrSvelteMicroApppour conserver et hydrater le balisage serveur. unmountsur un conteneur que le module n'a jamais monté, ou dont le montage a échoué, ne fait rien. 0.4 le vidait. Les quatre adaptateurs se comportent désormais ainsi.renderSvelteToStringne se replie plus surApp.renderquandsvelte/serverne se charge pas. Sirenderest passé, il utilise Svelte 5 ; sinon, il prend le chemin Svelte 5 quandsvelteexportemount, et n'appelleApp.renderque pour Svelte 3/4. Une erreur d'import ou de rendu Svelte 5 vous parvient.
Les composants Svelte 5 fonctionnent en 0.5 ; en 0.4, createSvelteMicroApp échouait avec eux. Avec Svelte 5, update() remonte le composant, sauf si vous passez SvelteMap depuis svelte/reactivity dans la configuration, ce qui met à jour les props sur place :
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
Nouvelle forme de configuration
// 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 et compilerOptions n'existent plus. Retirez import '@angular/compiler' de votre entrée ; l'adaptateur charge le compilateur pour les composants JIT.
Sans zone.js par défaut
Les apps s'exécutent sans zone.js sauf si vous passez zone: true. Sur Angular 15-17, les événements DOM ne déclenchent plus la détection de changements ; appelez ChangeDetectorRef.detectChanges() dans les gestionnaires, ou importez zone.js dans votre entrée et passez zone: true. Angular 18+ planifie la détection de changements sans zone.js. Voir Angular.
Les props sont des inputs
update() n'écrit plus window.__TUVIX_ANGULAR_PROPS__. Déclarez plutôt des propriétés @Input() ; l'adaptateur les définit avec 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 = '';
}Autres changements Angular
createSsrAngularMicroAppn'ajoute plusprovideClientHydration(). Il refait le rendu dans l'élément hôte rendu par le serveur.unmountsur un conteneur que le module n'a jamais monté ne fait rien.
Sandbox
Sandbox({ css: false }).activate(container)renvoienullet n'attache aucun shadow root.ISandbox.activaterenvoieShadowRoot | null; vérifieznullavant d'appeleraddStyle.- En mode strict,
JsSandboxlève uneTypeErrorsur une écriture bloquée. 0.4 affichait un avertissement et stockait quand même la valeur. Ajoutez àallowedGlobalsles globales que votre code écrit.
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; }');Serveur
createServerRenderer().render()renvoiestatusCode: 502quand au moins une app correspondait au chemin et que toutes les récupérations de fragments ont échoué. Il renvoie toujours 200 quand aucune app ne correspondait ou qu'un fragment a réussi.createMiddlewareenvoie ce statut. Le renderer en streaming envoie toujours 200.createMiddlewareetcreateStreamingMiddlewareappellentnext(error)au lieu denext()quand le rendu lève une erreur, et Express transmet donc la requête à votre gestionnaire d'erreurs.
Correctifs non cassants à connaître
require('@tuvix.js/core')et les autres points d'entrée CommonJS fonctionnent. En 0.4, cinq paquets faisaient pointerrequirevers des fichiers que le build ne générait jamais.getGlobalBus()conserve son instance surwindow, donc des micro apps empaquetées séparément partagent un seul bus.- Les adaptateurs et
defineMicroAppémettenttuvix:mounted,tuvix:unmountedettuvix:error. Voir Événements de cycle de vie. - Un
bootstrap()qui lève une erreur s'exécute de nouveau au montage suivant. 0.4 l'ignorait ensuite. register()aprèsstart()monte l'app immédiatement quand le chemin courant l'active, et commence à observer une appmountWhenVisible.- Chaque échec du cycle de vie émet
tuvix:app:errorsur le bus avec{ name, error, phase }. 0.4 ne l'émettait que pour les échecs de chargement et de montage. maxListenersavertit avecconsole.warnune fois par événement, avec ou sansdebug.orchestrator.connectRouter()remplace les pontssetTimeout+reconcile(). Voir Routeur externe.