11 KiB
Codemods — MUI React → @aphrody/m3-react
Transforms automatisés pour migrer du code MUI React (@mui/material) vers les
wrappers @aphrody/m3-react (web components <md-*> du fork material-web).
Contrat de référence :
../00-CONVENTIONS.md. Tout ce kit respecte §2 (nommage des wrappers), §3 (mapping canonique) et §4 (règles props/events). bun uniquement (§7.1). Aucun élémentmd-*inventé (§7.2).
Deux outils complémentaires :
| Outil | Rôle | Force |
|---|---|---|
transforms (transforms/) |
réécriture profonde : imports + JSX + props + slots, variant-aware | transformation complète, idempotente, testée |
ast-grep (rules/) |
détection/rapport des occurrences + quelques fix triviaux | inventaire rapide, zéro setup Node |
Installation
Aucune dépendance runtime : le moteur tourne sous Bun seul.
# ast-grep : pas d'install nécessaire, lancé via bunx @ast-grep/cli (binaire runtime)
Transforms
Les transforms partagent le moteur lib/engine.ts (table de mapping lib/mapping.ts,
helpers JSX lib/jsx-helpers.ts). Le source est lu par un scanner TSX (lib/tsx-scan.ts :
imports, prologue de directives, arbre JSX), modifié par éditions de texte positionnées
(lib/edits.ts) : le code non touché garde sa mise en forme. L'entrée doit être un TSX
valide pour Bun.Transpiler (sinon SyntaxError, fichier laissé intact).
| Transform | Périmètre | Usage |
|---|---|---|
transforms/orchestrator.ts |
tout le mapping (recommandé pour migration de masse) | bun scripts/codemod.ts orchestrator 'src/**/*.tsx' |
transforms/mui-imports.ts |
identique à l'orchestrateur (alias sémantique « imports génériques ») | bun scripts/codemod.ts mui-imports <fichiers> |
transforms/button.ts |
dédié Button / IconButton / Fab (variant-aware) |
bun scripts/codemod.ts button <fichiers> |
transforms/fields.ts |
dédié TextField / Select / NativeSelect (+ MenuItem) |
bun scripts/codemod.ts fields <fichiers> |
transforms/icons.ts |
dédié @mui/icons-material → <md-icon> (Material Symbols, ~96 %) |
bun scripts/codemod.ts icons <fichiers> |
--dry liste les fichiers qui changeraient sans les écrire. Chaque transform exporte
default (file: { path?, source }, api?, options?) => string.
Le transform icons.ts est séparé (l'orchestrateur ne touche pas @mui/icons-material) : convertit <CloseIcon/> → <md-icon>close</md-icon> via lib/icon-names.ts (PascalCase → snake_case, validé contre les 4253 glyphes officiels data/material-symbols-names.json). Logos de marque (GitHub, X…) gardés + TODO ; voir ../11-material-symbols.md. Le mapping consolidé (composants + icônes) est exporté en ../mui-m3-map.json (bun run scripts/export-map-json.ts).
Les transforms ciblés (button, fields) ne touchent que leurs composants ; les
autres imports/JSX MUI sont laissés intacts (migration par lots).
Ce qui est AUTOMATISÉ
- Imports (§2/§3) — réécriture
@mui/material→@aphrody/m3-react:- imports nommés
import { Button } from '@mui/material' - imports default sous-chemin
import Button from '@mui/material/Button' - alias
import { Button as Btn } from '@mui/material' - regroupe les wrappers en un seul
import { ... } from '@aphrody/m3-react'(trié) - ajoute l'import d'effet de bord
@aphrody/material-web/icon/icon.jssi un<md-icon>est généré
- imports nommés
- Choix du wrapper variant-aware (§3) :
Button variant="contained"|absent→MdFilledButton,"outlined"→MdOutlinedButton,"text"→MdTextButton,"elevated"→MdElevatedButton,"tonal"→MdFilledTonalButtonTextField variant="filled"|absent→MdFilledTextField,"outlined"→MdOutlinedTextFieldSelectfilled/outlined →MdFilledSelect/MdOutlinedSelect- la prop
variantest consommée (retirée) pour ces composants
- Renommage JSX des composants 1:1 (§3) :
Checkbox→MdCheckbox,Switch→MdSwitch,Dialog→MdDialog,MenuItem→MdMenuItem,Divider→MdDivider,Typography→MdType, etc. - Icônes (§4) :
startIcon={<X/>}/endIcon={<X/>}→<md-icon slot="icon">{<X/>}</md-icon>injecté en premier enfant (+ TODO pourendIcon, le slot trailing étant à repositionner). - Sous-composants slottés (§4) :
DialogTitle/DialogContent/DialogActions,CardContent/CardHeader/CardMedia→<div slot="headline|content|actions|media">(+ TODO). - Layout (§3/§6) :
Box/Container/Stack/Grid/Paper→<div>(+ TODO Tailwind). - Marqueurs
MIGRATION-TODOpour tout le non-automatisable, toujours en JSX valide ({/* ... */}entre enfants, commentaire de bloc ailleurs).
Ce qui reste MANUEL (signalé par MIGRATION-TODO)
sx— retirée : à convertir en classes Tailwind (host/layout) + tokens--md-sys-*. Le shadow DOM desmd-*n'est pas atteignable par Tailwind (§6).onChange(e, value)— material-web émet des events natifsinput/change: liree.target.value(le 2ᵉ paramètre disparaît, §4). Le corps du handler n'est pas réécrit automatiquement (risqué) — TODO posé quand une signature à 2 args est détectée.- Props sans équivalent retirées + TODO :
color,size,fullWidth,disableElevation,disableRipple(à gérer via tokens/density/thème). - Composants « gap » (§3 /
05-gap-analysis.md) :Avatar,Alert,Breadcrumbs,Rating,Skeleton,Backdrop,Modal/Popover/Popper,Link, transitions (Collapse/Fade/…),CssBaseline… NON transformés : le JSX et son import MUI sont conservés (pas de référence orpheline), un TODO signale le shim à écrire. - Imbrication des slots Dialog/Card : vérifier que les
<div slot>sont enfants directs du parentmd-*.
Limites connues
- Le moteur ne réécrit pas le corps des handlers ni la logique d'état (controlled).
- Les composants en notation membre (
<Foo.Bar/>) ne sont pas traités. - Pas de résolution de re-exports indirects (
import { Button } from './ui'). - Le renommage
Tab→MdPrimaryTabne distingue pas primary/secondary (choix par défaut). - Les enfants d'un élément renommé sont réimprimés sans les blancs JSX à saut de ligne
(cosmétique — passer
oxfmtaprès coup). - Un
startIcon/endIconsur un élément auto-fermant est retiré sans<md-icon>(TODO posé).
ast-grep — règles (rules/ + sgconfig.yml)
Complément léger pour inventorier ce qui reste à migrer et appliquer des fix triviaux.
@ast-grep/cli scan # rapport sur tout le projet (depuis ce dossier)
@ast-grep/cli scan src/ # rapport ciblé
@ast-grep/cli scan --filter '^fix-divider$' --update-all # applique le fix Divider
| Règle | Type | Effet |
|---|---|---|
detect-mui-imports |
rapport (warning) | signale tout import … from '@mui/material' (racine et sous-chemins) |
detect-gap-components |
rapport (error) | signale les composants « gap » sans équivalent md (migration manuelle) |
detect-sx-prop |
rapport (warning) | signale les props sx à convertir (Tailwind + tokens) |
fix-divider |
fixable | <Divider …/> → <md-divider …> (mapping 1:1 trivial ; l'import reste à gérer) |
ast-grep ne fait pas le choix variant-aware ni la gestion d'imports propre : pour une vraie migration, utiliser les transforms. Les règles servent surtout au rapport.
Tests / vérification
Fixtures avant/après dans __testfixtures__/ (préfixe icons- → transform icons,
sinon orchestrateur ; découverte automatique) :
bun test ./test # sortie TSX valide, idempotence, égalité oxfmt avec *.output.tsx
bun run scripts/run-fixtures.ts # même comparaison, en PASS/FAIL
bun run scripts/run-fixtures.ts --update # régénère les attendus (après modif volontaire d'un transform)
Les sorties sont comparées après oxfmt des deux côtés (binaire oxfmt requis dans le PATH).
Les attendus viennent du moteur jscodeshift/recast d'origine (capture avant migration) ;
seule exception, jsx-strings.output.tsx garde l'espace significatif que recast perdait.
État vérifié (dernière exécution)
bun test ./test→ 11 fixtures + scanner + rejet TSX invalide, 0 échec.tsgo --noEmit -p tsconfig.json→ 0 erreur.ast-grep scansur fichier d'épreuve → 4 règles déclenchées (2 imports, 1 gap, 1 sx, 2 divider).
Arborescence
codemods/
├── README.md ← ce fichier
├── package.json
├── sgconfig.yml ← config ast-grep
├── lib/
│ ├── mapping.ts ← table MUI→wrappers (source : 00-CONVENTIONS.md §3/§4)
│ ├── tsx-scan.ts ← scanner TSX (imports, directives, arbre JSX)
│ ├── edits.ts ← éditions de texte positionnées + validation Bun.Transpiler
│ ├── imports.ts ← filtrage/insertion d'imports
│ ├── jsx-helpers.ts ← utilitaires JSX (slots, TODO, props)
│ └── engine.ts ← moteur partagé (imports + JSX + props)
├── transforms/
│ ├── orchestrator.ts ← tout-en-un (recommandé)
│ ├── mui-imports.ts ← imports génériques (= orchestrateur)
│ ├── button.ts ← dédié Button/IconButton/Fab (variant-aware)
│ ├── fields.ts ← dédié TextField/Select/NativeSelect
│ └── icons.ts ← dédié @mui/icons-material → <md-icon>
├── rules/
│ ├── detect-mui-imports.yml
│ ├── detect-gap-components.yml
│ ├── detect-sx-prop.yml
│ └── fix-divider.yml
├── scripts/
│ ├── codemod.ts ← CLI d'application des transforms
│ ├── fixtures.ts ← découverte + exécution des fixtures
│ └── run-fixtures.ts ← test rapide avant/après
├── test/fixtures.test.ts
└── __testfixtures__/ ← *.input.tsx / *.output.tsx