Skip to content

Registre et CLI ​

Pourquoi copier les composants ​

Les composants applicatifs changent souvent selon le produit. Les copier permet de les personnaliser sans forker une bibliothèque entière, tout en conservant une base commune et documentée.

Flux d'installation ​

Commandes ​

bash
pnpm dlx @sia-ui/cli init                    # sia-ui.json et la feuille de styles
pnpm dlx @sia-ui/cli list                    # le catalogue, avec versions
pnpm dlx @sia-ui/cli list --outdated         # ce qui a bougé depuis l'installation
pnpm dlx @sia-ui/cli add button page-header  # installer, dépendances comprises
pnpm dlx @sia-ui/cli update [nom...]         # mettre à jour, sans écraser les retouches
pnpm dlx @sia-ui/cli diff [nom...]           # ce qui a été retouché localement
pnpm dlx @sia-ui/cli remove <nom...>         # désinstaller
pnpm dlx @sia-ui/cli doctor                  # l'état de tout ce qui est installé
OptionEffet
--overwriteadd : écraser un fichier déjà présent
--forceupdate : écraser aussi les fichiers retouchés ; remove : passer outre ses refus
--dry-runupdate : montrer ce qui changerait, sans rien écrire
--registry <url>lire un registre servi en HTTP

Après la copie ​

Un composant copié cesse de suivre les paquets : il vit dans le dépôt de qui l'a installé, qui le modifie comme il veut. C'est la contrepartie du modèle, et c'est aussi ce qui rendait le registre sans retour — installer, puis plus rien. Impossible de corriger ou de déprécier une entrée après coup.

sia-ui.lock.json, écrit à chaque add, referme cette impasse :

json
{
  "format": 1,
  "items": {
    "badge": {
      "version": "0.1.0",
      "installedAt": "2026-09-22T21:15:12.846Z",
      "files": {
        "src/components/Badge/index.tsx": "sha256:24616e…"
      }
    }
  }
}

L'empreinte est celle du fichier tel qu'il a été copié. Comparée au disque, elle dit si l'utilisateur l'a retouché; comparée à la source du registre, elle dit si l'entrée a bougé depuis. C'est de là que les quatre commandes tirent tout ce qu'elles savent.

Les chemins y sont écrits en barres obliques, jamais à la mode Windows : le fichier est versionné dans le dépôt de l'utilisateur, et deux machines ne doivent pas décrire le même fichier avec deux clés différentes.

remove refuse deux choses par défaut : retirer une entrée dont une autre dépend encore, et supprimer un fichier retouché depuis l'installation. --force lève les deux — le travail de quelqu'un ne s'efface pas sans qu'il le demande.

Mettre à jour ​

bash
pnpm dlx @sia-ui/cli@latest list --outdated   # ce qui a une version plus récente
pnpm dlx @sia-ui/cli@latest update --dry-run  # ce qui changerait
pnpm dlx @sia-ui/cli@latest update            # l'appliquer

@latest compte : le registre embarqué est figé au jour où la CLI a été publiée. Un projet branché sur le registre HTTP (registry dans sia-ui.json, ou --registry) le voit à jour sans changer de CLI.

Seules les entrées dont la version a changé sont concernées. update compare la version notée dans le verrou à celle du registre ; les numéros des paquets npm n'y jouent aucun rôle. Pour chaque fichier, l'empreinte du verrou dit s'il a été retouché :

Fichierupdateupdate --force
intact depuis l'installationremplacéremplacé
retouché localementgardé, signaléremplacé
nouveau dans cette versionajoutéajouté
présent mais inconnu du verrougardéremplacé
plus fourni par la nouvelle versionlaissé, signalélaissé, signalé

Une entrée dont un fichier a été gardé reste en retard dans le verrou : la question se reposera au prochain update, au lieu d'être tenue pour réglée. sia-ui diff <nom> montre l'écart pour reprendre la modification à la main. Les dépendances de registre apparues avec une nouvelle version sont installées, les paquets npm annoncés, et la feuille commune suit.

Une modification doit changer la version ​

Puisque les projets ne voient une mise à jour que par la version de l'entrée, modifier components/Pagination/index.tsx sans monter pagination publie une correction que personne ne reçoit. pnpm check:registry-versions, lancé par la CI, l'interdit : il compare chaque fichier d'entrée à la dernière publication et échoue quand un fichier a changé alors que la version de son entrée est restée la même.

La référence est le commit qui a fixé la version publiée dans packages/registry/package.json — pas le tag seul, qui peut pointer sur le commit d'avant quand on publie depuis un arbre non commité. Entre deux releases, une entrée peut changer autant de fois qu'il faut : une montée de version suffit. Une entrée nouvelle passe toujours.

text
Entrées modifiées depuis b388ce8 (@sia-ui/registry@0.5.0) sans nouvelle version :
  crud-page@0.3.0
    packages/react-web/src/components/CrudPage/index.tsx

Monter la version dans registry.json, puis expliquer le changement dans packages/registry/CHANGELOG.md — pnpm check:changelog refuse une version sans explication. Les deux contrôles se complètent : l'un exige qu'un changement porte une version, l'autre qu'une version porte une explication.

Le cycle de vie d'une entrée ​

add avertit sur une entrée deprecated et refuse une entrée removed, en nommant celle qui prend la suite quand elle existe. doctor signale les deux sur ce qui est déjà installé — c'est le seul moyen qu'une dépréciation atteigne quelqu'un qui a copié le fichier il y a six mois.

Chaque version d'entrée doit figurer dans packages/registry/CHANGELOG.md, publié avec le paquet : pnpm check:changelog le vérifie, et la CI le lance. Sans quoi list --outdated dirait « mettez à jour » sans que personne puisse savoir de quoi.

Le registre servi en HTTP ​

La copie embarquée dans la CLI fige le catalogue au jour de sa publication : corriger une entrée obligerait chacun à mettre la CLI à jour. Le registre est donc aussi servi en ligne, versionné par l'URL :

bash
sia-ui add badge --registry https://beatjo.github.io/sia-ui-site/r/v1/

r/v1/ est un contrat. Une forme incompatible naîtrait sous r/v2/, et les deux seraient servis côte à côte le temps que les projets suivent — c'est ce que veut dire « une CLI ancienne continue de répondre ». Le champ format du document dit la même chose depuis l'intérieur : une CLI qui lit le format 1 refuse le format 2 au lieu de l'interpréter de travers.

L'URL peut aussi vivre dans sia-ui.json, sous registry. Du plus explicite au plus implicite : l'option, puis le fichier de configuration, puis la copie embarquée.

Structure d'une entrée ​

json
{
  "name": "accordion",
  "description": "Collapsible sections with animated height.",
  "type": "component",
  "version": "0.1.0",
  "status": "stable",
  "source": "package",
  "files": [
    "components/Accordion/index.tsx",
    "components/Accordion/styles.css"
  ],
  "dependencies": ["@sia-ui/headless", "@sia-ui/utils"],
  "registryDependencies": ["icons"]
}

version est celle de l'entrée, pas celle des paquets. status vaut experimental, stable, deprecated ou removed; les deux derniers acceptent replacedBy et deprecatedReason.

Le fichier est tenu à la main, et rien dans le typage ne le protège — il est importé puis transtypé, donc un status: "stabel" passerait le build pour n'échouer que chez l'utilisateur. packages/registry/src/registry.test.ts vérifie chaque entrée; packages/cli/src/install.test.ts vérifie que les fichiers annoncés sont réellement servis.

Ce même essai vérifie aussi que chaque import relatif d'une entrée vise un fichier que add <nom> apporte : les siens, ou ceux de ses registryDependencies, transitivement. Dans le dépôt, tout le dossier components/ est présent et tout compile ; chez l'utilisateur, seul ce que l'entrée annonce arrive. C'est cet écart qui a laissé drawer — et avec lui app-shell — ininstallable : un ./context absent de files, un ../Icons sans dépendance à icons. Oublier un fichier ou une dépendance fait désormais échouer la CI.

Composants disponibles ​

NomNiveauUtilité
typographyPrimitiveTitres, textes et code inline
buttonPrimitiveAction accessible avec chargement
icon-buttonPrimitiveAction icône avec libellé accessible
inputPrimitiveChamp texte et état invalide
search-inputComposantRecherche contrôlée ou non avec effacement
date-pickerComposantSélection native d'une date sérialisable
time-pickerComposantSélection native d'une heure sérialisable
textareaPrimitiveSaisie multiligne
overlayPrimitivePortail positionné avec collisions et fermeture
selectComposantSélection recherchable locale ou distante
checkboxPrimitiveChoix booléen avec description
radio-groupPrimitiveChoix exclusif groupé
switchPrimitiveActivation booléenne contrôlée ou non
badgePrimitiveStatut compact et sémantique
statisticComposantNombre, montant ou pourcentage localisé
relative-timeComposantDate relative sémantique auto-actualisée
paginationComposantPagination contrôlée et accessible
alertComposantFeedback sémantique local
skeletonPrimitivePlaceholder de chargement
spinnerPrimitiveIndicateur de chargement accessible
modalComposantDialogue accessible dans un portail
confirm-dialogPatternConfirmation d'une action sensible
drawerComposantPanneau latéral accessible
tabsComposantNavigation par onglets et clavier
tooltipComposantAide contextuelle au survol et focus
cardPrimitiveConteneur de contenu composé
dividerPrimitiveSéparateur horizontal ou vertical
avatarPrimitiveImage ou initiales d'un utilisateur
containerPrimitiveLargeur responsive et empilement
fieldComposantContrat universel des champs de formulaire
form-fieldAlias dépréciéCompatibilité temporaire vers field
page-headerPatternEn-tête cohérent pour les pages
entity-metaPatternMétadonnées compactes d'une entité
error-statePatternErreur récupérable avec nouvelle tentative
filters-barPatternRecherche, filtres actifs et actions
stat-cardPatternIndicateur métier dans une carte
kpi-gridPatternGrille responsive d'indicateurs
audit-metaPatternHistorique chronologique d'audit
empty-statePatternÉtat vide avec action optionnelle
async-statePatternChargement, erreur et état vide
config-providerComposantValeurs par défaut globales ou locales des composants

Vagues 3 et 4 ​

GroupeEntrées installables
Donnéesdata-table, descriptions, timeline, tree, tree-select, transfer
Financecurrency-input, amount-display
Date et tempsdate-range-filter, date-range-picker, time-range-picker, date-time-picker, calendar, mini-calendar, event-calendar, timer, countdown, month-picker, year-picker, week-picker, duration-display
Applicationcommand-palette, app-shell, auth-layout, crud-page, permission-gate, unsaved-changes-guard
Interactionfile-upload, tour, resizable-panel, qr-code

Le registre contient désormais 88 entrées, dépendances techniques incluses.

Utilitaires locaux copiés ​

L'entrée utils sert de façade locale pour les composants copiés. Elle évite de forcer une dépendance externe juste pour des besoins transverses simples.

Exemples actuellement fournis:

  • cn
  • composeEventHandlers
  • formatDate
  • formatTimeAgo
  • formatCurrency
  • truncate
  • normalizeIdentifier

Ce que le projet hôte doit fournir ​

  • sia-ui.css, écrit par init, pose les variables de thème et un reset box-sizing: border-box de spécificité nulle (:where(*)). Les composants supposent border-box : sans lui, un width: 100% plus un padding déborde — 32 px de défilement horizontal sous AppShell. Toute règle du projet l'emporte sur ce reset.

    La feuille suit le registre, comme un composant : son empreinte est notée dans sia-ui.lock.json (styles), et elle est lue à la même source que les composants. Sans ce suivi, les composants se mettaient à jour sans la feuille dont ils dépendent — le reset box-sizing n'arrivait jamais.

    État de la feuilleinitaddupdateupdate --forcedoctor
    absentecopiéecopiéecopiéecopiéesignalée
    à jour—————
    non retouchée, registre plus récentlaisséemise à jourmise à jourmise à joursignalée
    retouchée localementlaisséelaisséelaisséeremplacéesignalée
    copiée avant le suivi (CLI ≤ 0.5)laisséelaisséelaisséeremplacéesignalée

    init initialise, et c'est tout ; un projet dont la feuille date d'une CLI 0.5 ou antérieure la remet d'aplomb une fois avec sia-ui update --force.

  • eslint-plugin-react-hooks, si le projet lance ESLint sur les composants copiés. Deux d'entre eux (chart, require-session) portent un eslint-disable-next-line react-hooks/exhaustive-deps justifié : sans le plugin, ESLint refuse une règle qu'il ne connaît pas. Avec sa configuration recommended, les composants ne produisent aucun avertissement exhaustive-deps. Les règles du React Compiler (refs, purity, set-state-in-effect, activées par recommended depuis la version 7) signalent encore une vingtaine de lignes dans une dizaine de composants — Prévu : les réécrire, ou documenter chaque exception.

Limites actuelles ​

La CLI n'installe pas les dépendances npm : elle les annonce, et laisse le gestionnaire de paquets du projet décider. Elle ne fusionne pas non plus les alias TypeScript, ni un fichier retouché avec sa nouvelle version — diff montre l'écart, la fusion reste manuelle.

Publié sous licence MIT.