Migration vers Rspack et Vitest
Ce guide explique comment remplacer le bundler Webpack d’un module frontend par Rspack et son runner de tests Jest par Vitest . Dans les dépôts OpenMRS à paquet unique, ces deux migrations ont généralement été livrées dans des PR proches l’une de l’autre. Ce guide les traite donc comme une seule intervention. Les monorepos les ont parfois réalisées à des moments différents. Voir la section Considérations pour les monorepos.
Si votre module n’a pas encore été mis à niveau vers Core v6, commencez par cela. Voir Migration vers Core v6. Le guide Core v6 laisse volontairement votre module sur Jest et Webpack. Celui-ci reprend à partir de là.
Ce guide s’appuie sur les PR (chore) Migrate to rspack et (chore) Migrate from jest to vitest dans openmrs-esm-laboratory-app. Si vous préférez travailler à partir d’un diff réel plutôt que d’une checklist, ces deux PR constituent un bon modèle.
Pourquoi migrer
Pourquoi Rspack plutôt que Webpack
- Builds plus rapides. Rspack est basé sur Rust. Les notes de migration Core v6 rapportent environ 3 fois moins de temps de build en pratique et des builds sous la minute là où Webpack prenait plusieurs minutes. Les rebuilds incrémentaux en mode watch sont aussi nettement plus rapides.
- Builds séquentiels fiables. Le pipeline Webpack de Core v5 rencontrait des épuisements mémoire lors de builds répétés. Voir le contexte original dans le guide de migration Core v6. Rspack n’a pas cette pathologie.
- Configuration compatible Webpack. Rspack accepte les plugins et la forme de configuration de Webpack. La migration est donc mécanique plutôt qu’une réécriture.
- Prise en charge native des modules ES. Rspack gère les exports ESM du framework sans la tuyauterie de transformation personnalisée dont Webpack avait besoin.
Pourquoi Vitest plutôt que Jest
- Prise en charge native des modules ES. Core v6 publie le framework comme modules ES. Historiquement, Jest avait besoin de
transformIgnorePatterns, de règlestransformpersonnalisées et de fichiers de mock doublés (mock.tsxetmock-jest.tsx) pour s’en sortir. Vitest lit l’ESM nativement et supprime la plupart de cet échafaudage. - Exécution des tests plus rapide. Vitest s’appuie sur le pipeline de transformation de Vite et partage les caches de graphe de dépendances entre les runs. Le mode watch est donc nettement plus rapide que celui de Jest.
- Alignement avec le framework.
openmrs-esm-frameworka lui-même migré vers Vitest dansopenmrs-esm-core#1591. Les apps sur Vitest partagent la même configuration, les mêmes mocks et le même comportement CI que le framework dont elles dépendent. - Meilleure ergonomie développeur. Stack traces plus claires,
vi.mocked(fn)typé, relance en mode watch sous la seconde et couverture intégrée via@vitest/coverage-v8sans reporter Jest supplémentaire.
Pourquoi les faire ensemble
Les deux migrations sont indépendantes en théorie, mais elles vont souvent ensemble en pratique parce que:
- Elles sont toutes deux des remplacements d’outillage sans impact fonctionnel sur l’app.
- Les reviewers ont tendance à les considérer comme une seule modernisation de la toolchain.
- Rspack utilise le format de plugin et de configuration de Webpack. Le diff de configuration est donc petit, et l’associer au changement Vitest plus volumineux limite le nombre de PR.
Vous pouvez les livrer en une seule PR ou en deux. openmrs-esm-laboratory-app les a livrées en deux PR adjacentes, ce qui est plus facile à relire.
Partie 1: Webpack vers Rspack
La configuration Rspack par défaut vit dans @openmrs/rspack-config et reflète la configuration Webpack par défaut. La plupart des apps n’ont besoin que de mettre à jour les dépendances du bundler, renommer le fichier de configuration et mettre à jour leurs scripts.
Mettre à jour les dépendances du bundler
Ajoutez le paquet de configuration Rspack et les paquets CLI Rspack. Si votre module a déjà @openmrs/webpack-config ou une dépendance directe vers webpack, supprimez-les en même temps:
- "@openmrs/webpack-config": "next",
+ "@openmrs/rspack-config": "next",
- "webpack": "^5.99.9",
+ "@rspack/cli": "^1.7.10",
+ "@rspack/core": "^1.7.10",Gardez la dépendance de développement openmrs. yarn start passe toujours par openmrs develop.
Renommer le fichier de configuration
Renommez webpack.config.js en rspack.config.js et importez la nouvelle configuration par défaut. Le scaffold actuel de npm create @openmrs/o3-app@latest désactive aussi les avertissements de taille d’asset en production, car le budget générique de 244 KiB produit beaucoup de bruit pour les modules O3 qui partagent les dépendances framework et Carbon au runtime:
const config = require("@openmrs/rspack-config");
const base = config.default ?? config;
const disablePerformanceHints = (cfg) => ({
...cfg,
performance: { ...(cfg.performance ?? {}), hints: false },
});
module.exports =
typeof base === "function" ? (...args) => disablePerformanceHints(base(...args)) : disablePerformanceHints(base);Si votre app passe des overrides à la configuration par défaut, appliquez-les avant d’exporter la configuration finale. Leur forme ne change pas, car Rspack accepte le format de configuration de Webpack.
Les modules plus anciens peuvent encore utiliser openmrs/default-rspack-config, un export de compatibilité du paquet d’outillage openmrs. Préférez @openmrs/rspack-config pour les nouvelles migrations afin que la dépendance corresponde au scaffold publié par @openmrs/create-o3-app.
Mettre à jour les scripts package.json
Remplacez webpack par rspack dans build, serve et analyze:
"scripts": {
"start": "openmrs develop",
- "serve": "webpack serve --mode=development",
- "build": "webpack --mode production",
- "analyze": "webpack --mode=production --env.analyze=true",
+ "serve": "rspack serve --mode=development",
+ "build": "rspack --mode production",
+ "analyze": "rspack --mode=production --env.analyze=true",
}start reste sur openmrs develop. Le CLI OpenMRS détecte automatiquement quel bundler lancer en regardant les fichiers de configuration du module: si rspack.config.js est présent et que webpack.config.js a disparu, il démarre un serveur de développement Rspack. Sinon, il retombe sur Webpack. Renommer le fichier de configuration est donc ce qui fait basculer yarn start vers Rspack. Aucun flag n’est nécessaire. Vous pouvez aussi forcer le choix explicitement avec openmrs develop --use-rspack ou --use-rspack=false.
Supprimer la dépendance de types Webpack
Supprimez @types/webpack-env de devDependencies. Si votre code utilise require.context, généralement pour les imports dynamiques de traductions, déclarez les types localement. Ajoutez ceci à src/declarations.d.ts:
declare interface RequireContext {
keys(): string[];
(id: string): unknown;
<T>(id: string): T;
resolve(id: string): string;
id: string;
}
declare namespace NodeJS {
interface Require {
context(directory: string, useSubdirectories?: boolean, regExp?: RegExp, mode?: string): RequireContext;
}
}Vérifier
yarn install
yarn build
yarn startyarn build devrait se terminer nettement plus vite que l’équivalent Webpack, et yarn start devrait servir votre module comme auparavant.
Partie 2: Jest vers Vitest
Vitest est compatible avec l’API de Jest pour les usages courants, mais la forme de configuration et quelques imports diffèrent. Prévoyez de toucher package.json, vitest.config.ts, votre fichier setup-tests et chaque fichier de test. Les remplacements jest.* vers vi.* sont mécaniques.
Remplacer les dépendances
Supprimez les paquets Jest et ajoutez les équivalents Vitest:
- "@swc/jest": "^0.2.26",
- "@testing-library/dom": "^8.20.0",
- "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/dom": "^10.4.1",
+ "@testing-library/jest-dom": "^6.8.0",
- "@types/jest": "^28.1.8",
+ "@vitest/coverage-v8": "^4.1.2",
- "jest": "^28.1.3",
- "jest-cli": "^28.1.3",
- "jest-environment-jsdom": "^28.1.3",
+ "jsdom": "^28.0.0",
+ "vitest": "^4.1.2",Quelques notes sur ces versions:
@testing-library/jest-domdoit être en^6.x. Les versions précédentes n’exposent pas l’import/vitestdont vous aurez besoin danssetup-tests.ts.@testing-library/dompasse de 8.x à 10.x dans la même modernisation quejest-dom@6. Les deux n’ont pas de relation stricte de peer dependency,jest-dom@6n’en déclare pas, mais les apps OpenMRS migrées ont généralement mis les deux à jour ensemble.- Définissez
TZ=UTCpour rendre les assertions de date et d’heure déterministes. Le scaffold actuel le fait dansvitest.config.tsavecprocess.env.TZ = 'UTC'. Si votre dépôt utilise déjàcross-envpour définir des variables d’environnement dans les scripts, définirTZ=UTCdans les scripts de test reste aussi valable.
Mettre à jour les scripts de test
- "test": "jest --config jest.config.js --passWithNoTests --color",
- "coverage": "yarn test -- --coverage",
+ "test": "vitest run --passWithNoTests",
+ "test:watch": "vitest watch",
+ "coverage": "vitest run --coverage --passWithNoTests",Ajouter vitest.config.ts
Créez vitest.config.ts à la racine du dépôt:
import { fileURLToPath } from "node:url";
import { defineConfig } from "vitest/config";
process.env.TZ = "UTC";
const r = (relativePath: string) => fileURLToPath(new URL(relativePath, import.meta.url));
export default defineConfig({
resolve: {
alias: [{ find: /^.*\.s?css$/, replacement: "identity-obj-proxy" }],
},
test: {
environment: "jsdom",
globals: true,
clearMocks: true,
setupFiles: [r("./tools/setup-tests.ts")],
exclude: ["**/node_modules/**", "**/e2e/**", "**/dist/**"],
server: {
deps: {
inline: [/@openmrs/],
},
},
alias: [
{ find: /^@openmrs\/esm-framework$/, replacement: "@openmrs/esm-framework/mock" },
{ find: "react-i18next", replacement: r("./__mocks__/react-i18next.js") },
],
},
});Quelques points à comprendre plutôt qu’à copier sans réflexion:
environment: 'jsdom'. C’est le choix dominant dans les modules O3 migrés.laboratory-app,patient-chart,patient-management,form-builder,billing-appet plusieurs autres l’utilisent. L’alternative est'happy-dom', plus rapide mais avec une couverture d’API DOM plus étroite, notamment pour les APIs de layout, de mesure et de CSSOM.openmrs-esm-coreutilise les deux selon les packages. Le scaffold publié@openmrs/create-o3-apputilise jsdom, tandis que l’ancienne solution de secoursopenmrs-esm-template-apputilise encore happy-dom par défaut. En cas de doute, commencez avec jsdom. Passez à happy-dom seulement si le temps d’exécution devient un vrai problème et que vos tests ne dépendent pas d’une API DOM non implémentée par happy-dom.process.env.TZ = 'UTC'. Rend les tests sensibles aux dates déterministes sans obliger chaque script de test à envelopper Vitest aveccross-env TZ=UTC.- Alias CSS. La regex
^.*\.s?css$/ancre les deux extrémités afin que tout le chemin d’import soit remplacé paridentity-obj-proxy. Sans ces ancres, seule l’extension est capturée et Vitest n’arrive pas à résoudre le module. fileURLToPathpour les chemins du système de fichiers. Utilisez-le poursetupFileset les alias qui pointent vers des fichiers locaux. Utiliser directementnew URL(...).pathnamefonctionne sur macOS et Linux, mais sur Windows cela produit/C:/..., que le résolveur Vite rejette.fileURLToPathnormalise le chemin.server.deps.inline: [/@openmrs/]. Force Vite à traiter les paquets@openmrs/*dans son pipeline de transformation plutôt que de les traiter comme externes. Sans cela, les exports ES modules du framework ne se résolvent pas correctement dans les tests.- Fake timers. Omettez
fakeTimerspar défaut. Les fake timers par défaut de Vitest évitent déjànextTicketqueueMicrotask, les deux APIs qui casseraient le scheduler React. Si vos tests Jest existants dépendent d’une liste de timers plus étroite, ajoutez explicitementfakeTimers.toFake, mais n’incluez pasqueueMicrotaskninextTick. - Alias du mock framework. Le point d’entrée public du framework pointe vers
@openmrs/esm-framework/mock. Si vos tests importent encore le chemin interne (@openmrs/esm-framework/src/internal), ajoutez un second alias pour ce chemin pendant la migration. - Alias
react-i18next. Jest découvrait automatiquement__mocks__/react-i18next.jspar convention. Vitest ne le fait pas. L’alias est ce qui le reconnecte. Si le mock i18n d’un module disparaît après la migration, c’est presque toujours la cause. excludeet e2e. Les specs Playwright vivent généralement souse2e/et sont lancées par un scripttest-e2eséparé. L’entrée'**/e2e/**'évite que Vitest tente de les évaluer.
Mettre à jour setup-tests.ts
- import "@testing-library/jest-dom";
+ import "@testing-library/jest-dom/vitest";
+ import { vi } from "vitest";
// Mock ResizeObserver which is not available in jsdom
global.ResizeObserver = class ResizeObserver {
// ...
};L’import /vitest branche les matchers DOM sur expect de Vitest. React Testing Library enregistre automatiquement le cleanup dès qu’un afterEach global est disponible, ce qui est vrai avec Vitest et globals: true. Le scaffold actuel n’ajoute donc pas de hook manuel afterEach(cleanup). Ajoutez-en un seulement si un package désactive les globals ou définit RTL_SKIP_AUTO_CLEANUP=true.
L’emplacement du fichier de setup varie selon les modules: tools/setup-tests.ts est le plus courant, mais vous verrez aussi src/setup-tests.ts et test/setup.ts. Gardez le chemin déjà utilisé par votre module et mettez seulement l’import à jour. Seule l’entrée setupFiles de vitest.config.ts doit connaître son emplacement.
Convertir chaque fichier de test
Pour chaque fichier *.test.ts(x):
-
Ajoutez un import explicite depuis
vitest. Même avecglobals: true, les imports explicites donnent une meilleure autocomplétion et des types plus stricts:+ import { beforeEach, describe, expect, it, vi } from "vitest";Importez seulement les symboles réellement utilisés par le fichier.
-
Remplacez les appels d’API Jest:
Jest Vitest jest.fn()vi.fn()jest.mock(...)vi.mock(...)jest.mocked(...)vi.mocked(...)jest.spyOn(...)vi.spyOn(...)jest.requireActual(...)vi.importActual(...)(attention: maintenant async)jest.useFakeTimers()vi.useFakeTimers()Les quatre premiers sont de simples remplacements.
requireActualest celui à surveiller: il est synchrone dans Jest mais asynchrone dans Vitest. La factory environnante doit donc devenirasyncet l’appel doit êtreawaité.Pour les assertions de type sur les mocks, préférez
vi.mocked(fn)aux casts manuels quand c’est possible. Il conserve automatiquement la signature de la fonction d’origine. Si vous avez besoin d’un type explicite,jest.MockedFunction<typeof fn>devientMockedFunction<typeof fn>depuisvitest. Le type nujest.Mock<R, A>ne se traduit pas directement: leMockde Vitest prend un seul paramètre de type fonction (Mock<T extends Procedure | Constructable>). L’équivalent est doncMock<(...args: A) => R>, ou, mieux,vi.mocked(fn). -
Si le fichier utilise
jest.mock("@openmrs/esm-framework", () => ({ ...jest.requireActual(...), ... }))pour surcharger partiellement le mock framework, l’équivalent Vitest est:vi.mock("@openmrs/esm-framework", async () => { const actual = await vi.importActual<typeof import("@openmrs/esm-framework")>("@openmrs/esm-framework"); return { ...actual, useConfig: vi.fn(), }; });
Supprimer jest.config.js
Quand tout passe, supprimez jest.config.js. Le dossier __mocks__/ reste généralement en place, car la plupart de ses fichiers sont encore référencés par les entrées alias de vitest.config.ts, par exemple react-i18next.js. Supprimez seulement un fichier __mocks__/ si rien dans vitest.config.ts ni dans vos tests ne pointe encore vers lui.
Vérifier
yarn install
yarn test
yarn typescript
yarn lintLes quatre commandes doivent être vertes. Lancez aussi yarn test:watch pour confirmer que le mode watch fonctionne.
Pièges courants
- Les tests passent localement mais échouent dans CI sur Windows. C’est presque toujours un problème d’alias de chemin. Utilisez
fileURLToPathpour tout alias qui résout vers un chemin de fichier. Voir l’extraitvitest.config.tsci-dessus. expect(...).toBeInTheDocument is not a function. L’importjest-domdanssetup-tests.tsn’utilise pas le sous-chemin/vitest, ou@testing-library/jest-domest encore en 5.x.Cannot find module '@openmrs/esm-framework/mock'. Soit votre version de@openmrs/esm-frameworkest antérieure à la disposition de mocks doubles, voir Migration vers Core v6, soit votre aliasvitest.config.tscontient une faute.- Les tests React échouent avec des erreurs scheduler obscures après l’activation des fake timers. Vous avez probablement ajouté
queueMicrotaskounextTickàfakeTimers.toFakeà la main. Les deux cassent le scheduler React. Les fake timers par défaut de Vitest les excluent déjà. Il est donc généralement préférable de laisserfakeTimersnon défini. Si vous fournissez une liste explicite, gardez ces deux APIs hors de la liste. - Les imports
@openmrs/*se résolvent vers le CJS construit plutôt que l’ESM. Ajoutezserver.deps.inline: [/@openmrs/]à la configuration Vitest. - TypeScript se plaint que
require.contextn’existe plus après la suppression de@types/webpack-env. Rspack prend en chargerequire.contextà l’exécution exactement comme Webpack. Le site d’appel est donc correct. Ce qui a disparu, c’est la définition de type fournie par@types/webpack-env. Ajoutez la déclarationRequireContextde la Partie 1 danssrc/declarations.d.tspour restaurer les types.
Considérations pour les monorepos
Si vous migrez un monorepo basé sur Turbo, par exemple openmrs-esm-core, openmrs-esm-patient-chart ou openmrs-esm-patient-management, les étapes ci-dessus s’appliquent par package, avec quelques points supplémentaires:
-
La disposition des configs varie. Choisissez un modèle. Deux modèles existent dans les monorepos OpenMRS:
- Configurations par package (
openmrs-esm-core). Les packages qui ont besoin d’une configuration Vitest personnalisée la gardent localement. La plupart des packages avec tests ont leur proprevitest.config.ts, mais quelques-uns, par exemplepackages/framework/esm-context, déclarent un scriptvitest runet utilisent les valeurs par défaut. La racine orchestre seulement via Turbo.packages/apps/esm-login-app/vitest.config.tsest un exemple minimal. - Configuration unique à la racine (
openmrs-esm-patient-chart,openmrs-esm-patient-management). Un seulvitest.config.tsà la racine collecte les tests de tous les packages, avec seuils de couverture et alias partagés. Lespackage.jsondes packages déclarent simplement"test": "vitest run"et héritent du reste.
Les deux fonctionnent. Choisissez celui qui correspond au reste de votre outillage: par package si vos packages diffèrent beaucoup, configuration unique si vous voulez gérer les seuils et mocks partagés en un seul endroit.
- Configurations par package (
-
Inputs des tâches Turbo. Mettez à jour les
inputsde la tâchetestdansturbo.jsonpour que le cache soit invalidé quand la configuration Vitest ou le fichier de setup change. Le modèle dansopenmrs-esm-core/turbo.jsonlistevitest.config.ts,setup-tests.ts,__mocks__/**,mock.tsetmock-jest.ts*. Reprenez cette idée et retirez toute entrée restante versjest.config.js. -
Vérifier tout le workspace. Après migration, lancez
yarn turbo verifyouyarn turbo lint typescript testà la racine du dépôt pour confirmer qu’il n’y a pas de régression. -
Packages
@openmrs/*internes au workspace. L’entréeserver.deps.inline: [/@openmrs/]couvre déjà les packages workspace. Le protocole workspace de Yarn les résout sousnode_modules/@openmrs/*, et la règle inline force Vite à les transformer plutôt qu’à les traiter comme externes. Pas besoin de configuration supplémentaire. -
Yarn linker. Les monorepos OpenMRS utilisent Yarn 4 avec
nodeLinker: node-modules, pas PnP. Les règles de résolution Node standard s’appliquent et vous n’avez pas besoin de contournement PnP. L’ancientransformIgnorePatterns: ["/node_modules/(?!@openmrs|.+\\.pnp\\.[^\\/]+$)"]des configs Jest peut être supprimé. L’exception.pnpest inutile dans un workspace lié ennode-modules. -
Mocks partagés entre packages. Si un
vitest.config.tspar package doit aliaser un mock qui vit à la racine du dépôt ou dans un package voisin, utilisezfileURLToPath(new URL('../../__mocks__/...', import.meta.url))plutôt qu’un chemin relatif nu.import.meta.urlse résout par rapport à la configuration consommatrice, donc l’alias fonctionne quel que soit l’endroit d’où Vitest est lancé. -
Agrégation de couverture. Avec des configs par package, chaque package émet son propre rapport et les combiner demande
--merge-reportsde Vitest 4 depuis la racine. Voir la documentation Vitest sur merge-reports . Avec une configuration unique à la racine, la couverture est déjà globale et vous obtenez des seuils unifiés gratuitement. Voir les configs racine depatient-chartetpatient-managementpour des exemples avecstatements/branches/functions/lines: 80.
Voir la section PR de référence ci-dessous pour des migrations canoniques depuis openmrs-esm-core, openmrs-esm-patient-chart et openmrs-esm-patient-management.
PR de référence
Modules à paquet unique
Les deux PR de chaque paire ont été relues et fusionnées indépendamment:
openmrs-esm-laboratory-app#759- Webpack vers Rspackopenmrs-esm-laboratory-app#757- Jest vers Vitestopenmrs-esm-form-builder#1199et#1198- même paire, avec une forme d’app légèrement différente
Monorepos
openmrs-esm-core (configs par package):
openmrs-esm-patient-chart (un seul vitest.config.ts racine):
openmrs-esm-patient-management (un seul vitest.config.ts racine):
#2176- migration groupée, qui déplace aussi tous les packages vers Rspack. Le titre de la PR parle de la suppression depatient-common-lib, mais la migration Rspack est décrite dans le corps.#2273- nettoyage de suivi: suppression des dépendances directesrspacketwebpack#2532- Migration de Jest vers Vitest