La catégorie d'un plugin n'est pas une étiquette. C'est la règle de routage.
Soixante deux packages first party sont livrés aujourd'hui, répartis en exactement six catégories : 27 widget, 15 library, 10 ui, 5 graphics, 3 dbs et 2 framework. La catégorie n'est ni une convention de nommage ni un tag. C'est un champ plugin_type sur l'enregistrement du plugin, et trois systèmes distincts le lisent : l'éditeur décide dans quel emplacement le plugin apparaît, la validation décide quels fichiers il doit exporter, et le runtime décide sur laquelle des trois surfaces d'exécution son code s'exécute. Rien n'est câblé à la main après une installation. Un package du catalogue prouve que c'est le champ qui fait autorité et non le nom, car son répertoire et ses exports disent tous deux widget alors que son nom npm publié dit ui, et chaque surface de Studio le route selon le champ. Un nouveau projet démarre avec deux plugins déjà installés, le driver de données natif et le framework Tailwind. Tout ce qui vient après ces deux là est une installation, et la catégorie décide où elle atterrit.

Packages de plugins livrés, répartis en exactement six catégories
Méthodes déclarées par les 42 packages qui en exportent, sur 632 champs
Noms de fichiers que la plateforme sonde après une installation. La plupart en livrent deux
Packages qui livrent du code serveur. Aucun plugin widget ou ui ne le fait
Six catégories, et la catégorie décide des deux extrémités.
Installez un plugin et il apparaît au bon endroit sans rien à configurer, car un seul champ est lu par six éléments différents de l'éditeur et par trois points d'entrée d'exécution différents. Le schéma ci-dessous est le contrat, pas le sélecteur : à gauche, comment un plugin de cette catégorie est atteint, et à droite, l'export exact que le runtime appelle et avec quelle signature. Lisez-le comme six contrats indépendants suspendus à un seul champ plutôt que comme une interface de plugin unique déclinée en six variantes, car c'est bien ce que c'est. Un plugin widget et un plugin ui n'exportent pas la même forme, ne reçoivent pas les mêmes arguments et n'apparaissent pas au même endroit, et un plugin graphics ne livre même pas le fichier autour duquel les cinq autres s'articulent.
Un contrat, six formes, et les cellules vides sont l'argument.
Après l'installation npm, la plateforme sonde six noms de fichiers : index.mjs, setup.mjs, studio.mjs, runtime.mjs, server.mjs et style.css. Elle regarde d'abord à la racine du package puis dans dist, enregistre lequel des deux elle a trouvé pour que le runtime sache s'il doit aussi charger la feuille de style compilée, puis importe chaque fichier trouvé sauf style.css et server.mjs. Cet import est le véritable test est-ce que ça charge : un plugin dont l'index.mjs lève une erreur pendant l'import échoue la validation sur le champ, et le texte de l'erreur est stocké sur son enregistrement, ce qui explique pourquoi un plugin cassé vous montre une raison plutôt qu'un emplacement vide. Le tableau ci-dessous montre ce que livrent réellement les six catégories. Les croix portent autant l'argument que le texte : la colonne server.mjs est vide pour chaque catégorie côté navigateur, et c'est le fait sur lequel reposent à la fois les surfaces d'exécution et le verrou tiers. Un fichier est délibérément laissé hors du tableau. studio.mjs n'est livré que par un seul package de tout le catalogue, le driver de données natif, où il constitue une surface serveur distincte en temps de conception pour l'éditeur, ce qui lui vaut cette phrase plutôt qu'une colonne de cinq croix.
index.mjs doit exporter | runtime.mjs doit exporter | server.mjs | setup.mjs | |
|---|---|---|---|---|
| widget, 27 livrés | methods, plus des capabilities optionnelles | une fonction par méthode | ||
| library, 15 livrés | methods | un stub par méthode | 12 sur 15 | 12 sur 15 |
| ui, 10 livrés | properties | fn, et optionnellement destroy | ||
| graphics, 5 livrés | tabs | |||
| dbs, 3 livrés | tabs et public_filter | 3 sur 3 | ||
| framework, 2 livrés | tabs, core et les primitives UI | core, obligatoire. resources, optionnel |
Une installation est un véritable npm install dans un répertoire qui appartient à votre projet.
Rien ici n'est une métaphore d'une installation. Une entrée est écrite dans la carte des plugins achetés du projet, portant le drapeau installed, la version, le nom du plugin parent s'il en existe un, le manifeste de fichiers, le type de plugin et le drapeau background. Ensuite npm install s'exécute dans un répertoire plugins appartenant à ce seul projet, avec chaque argument échappé en shell. Puis ce répertoire est rsyncé vers les serveurs régionaux. Ensuite le package installé est scanné jusqu'à une profondeur de dix, et chaque fichier est lu dans une base CouchDB appartenant au projet, indexé par son chemin relatif, et une configuration existante est reportée lors d'une mise à jour plutôt qu'écrasée.
- Chaque projet a son propre arbre node_modules, et un package qui échoue à se télécharger ne supprime que lui-même. Le code contient une note expliquant pourquoi : une version antérieure supprimait l'arbre entier en cas d'échec, ce qui désinstallait tous les autres plugins du projet parce qu'un seul package n'avait pas pu être téléchargé.
- Désinstaller bascule le drapeau installed, exécute npm uninstall, et vide le document de plugin côté projet jusqu'à son id, sa révision et son setup. La configuration survit à une désinstallation et à une réinstallation ultérieure, ce qui fait toute la différence entre retirer un plugin et perdre les identifiants que vous y aviez saisis.
- Rien n'est intégré dans un build d'app. Le runtime récupère chaque fichier de plugin par session via une route /plugins, qui sert depuis l'arbre propre au projet et exige soit un app id résolvable, soit un token valide. Pour un fichier source non empaqueté, cette route réécrit les imports ESM relatifs à la volée, afin qu'un navigateur puisse les charger. Ce que le déploiement empaquette, c'est la carte de configuration, rassemblée dans les données de démarrage de l'app.
- En session de développement, le runtime essaie src avant dist pour tout fichier .mjs et retombe sur dist, si bien que modifier le code source du plugin se répercute sans rebuild. Partout ailleurs, c'est le manifeste qui décide, et là où il enregistre que c'est la copie compilée qui a été trouvée, dist l'emporte.
- L'enregistrement est écritinstalled, version, nom du parent, manifeste de fichiers, type de plugin et drapeau background
- npm install s'exécuteDans le répertoire plugins propre à ce projet, chaque argument échappé en shell
- L'arbre est synchroniséCe répertoire de plugins du projet est poussé vers les serveurs régionaux distants
- Le package est capturé en instantanéChaque fichier lu dans la base de données du projet, indexé par son chemin relatif
Un plugin enfant nomme son parent. Le parent doit nommer l'enfant en retour.
Un plugin peut déclarer un seul parent, et la plateforme refuse de l'installer à moins que deux déclarations distinctes ne concordent. L'enfant nomme le parent via un champ de dépendance sur son propre enregistrement. Le parent doit lister l'enfant en retour, dans un tableau authorized plugins sur l'enregistrement du parent. Aucune déclaration seule ne suffit. Les vérifications s'exécutent dans l'ordre et chaque échec revient sous forme d'un refus numéroté plutôt que d'une trace d'appels, et toutes s'exécutent avant que npm ne soit invoqué, si bien qu'une installation rejetée ne laisse rien sur le disque. Les deux mêmes vérifications sont rejouées plus tard, à chaque lecture et chaque écriture d'une configuration de plugin, et à nouveau sur le serveur avant qu'une méthode de plugin ne soit autorisée à s'exécuter.
Le plugin décide de ce que sa propre configuration peut révéler au navigateur.
La configuration d'un plugin n'est pas conservée dans le registre partagé. Elle vit comme un document setup à l'intérieur de la base CouchDB du projet qui l'a configuré, écrit par le formulaire de configuration et relu par les deux côtés. Quand la lecture est publique, la plateforme importe le plugin et appelle une fonction que le plugin exporte lui-même, et ne renvoie que ce que cette fonction a retourné. Il n'existe aucune liste blanche de clés sûres à l'échelle de la plateforme, et c'est à la fois la force de ce dispositif et sa limite : c'est l'auteur du plugin qui sait lequel des champs est le secret, et c'est l'auteur du plugin qui écrit le filtre.
Trois surfaces, et celle du serveur est vérifiée avant l'import, pas après.
Le code de plugin s'exécute en exactement trois endroits. Dans le navigateur, comme widget qui possède un conteneur que le runtime a créé pour lui, comme plugin ui décorant un élément déjà existant, comme framework, ou comme plugin background accroché au cycle de vie de l'app. Sur un service de plugins dédié, son propre processus sur le port 3008, pour les méthodes serveur library. Et sur un dispatch séparé pour les drivers de données, qui ne passe jamais par ce service et met en cache sa connexion active par app et par plugin. Les deux chemins serveur s'exécutent sous un plafond de soixante secondes.
- Une méthode library, ce sont deux fichiers qui font deux travaux. Le stub navigateur convertit les champs déclarés, impose ceux qui sont obligatoires et poste ; le fichier serveur fait le travail et détient le secret. Cette séparation est toute la raison pour laquelle une clé secrète Stripe n'a aucun chemin navigateur : elle est lue dans server.mjs, à partir du document setup remis, et nulle part ailleurs.
- Avant que le service ne charge un fichier serveur de plugin, il vérifie si l'éditeur est first party, et il le fait avant l'import plutôt qu'après. Importer un module exécute déjà son code de premier niveau, donc une vérification placée après ne vérifie rien. Les méthodes serveur tierces sont refusées par défaut et l'interrupteur qui les autoriserait est réglé sur false dans les deux configurations, si bien que les plugins tiers restent aujourd'hui une surface produit côté navigateur uniquement.
- Un plugin background reçoit cinq hooks de cycle de vie, déclenchés dans l'ordre beforeInit, initialized, beforeMounted, mounted et systemReady autour du démarrage de l'app, chacun recevant la session, l'objet app, l'api et les données de setup. La case run in background n'est proposée que sur un plugin library, et l'enregistrement est entièrement sauté en mode slim bundle.
- Les champs qu'une méthode déclare font double usage. Sur un Full Stack VPS, la plateforme parcourt les plugins installés et enregistre chaque méthode comme outil MCP, nommé plugin puis method, avec son schéma d'entrée construit à partir de ces mêmes champs déclarés et sa description tirée de l'infobulle que la méthode portait déjà.
- L'étape de workflow se déclencheNommant un plugin, une méthode et les propriétés à lui passer
- Le stub navigateur valideChamps convertis vers leurs types déclarés, les obligatoires imposés
- Il poste au service de pluginsLe token vérifié contre une session active dans votre propre base de projet, jamais depuis un cache
- Le fichier serveur s'exécute, avec un plafondSoixante secondes, avec le document setup remis
Un plugin navigateur reçoit un seul objet, et c'est tout ce qu'il a.
Le runtime construit un objet de paramètres par appel et le remet en second argument. Un widget ne va pas chercher une variable globale pour trouver la plateforme, et il n'y a pas d'API de plugin séparée à apprendre au-delà de ce qui arrive dans cet objet. L'hôte crée le div conteneur, lit l'id du plugin et le nom de la méthode sur le nœud, importe index.mjs et vérifie les capabilities déclarées face à l'environnement courant, résout et convertit chaque champ déclaré et impose les obligatoires, charge style.css et dist/runtime.css quand le manifeste les a enregistrés, récupère le setup du plugin, et ce n'est qu'alors qu'il importe runtime.mjs et attend la méthode. Une erreur levée dans cette méthode est interceptée et signalée plutôt que de faire planter la page. La forme ci-dessous est celle du widget. Un plugin ui reçoit quelque chose d'assez différent pour mériter d'être imprimé à côté, car cette différence constitue toute la distinction entre les deux catégories : un widget reçoit un conteneur qu'il possède, un plugin ui reçoit un élément déjà existant.
Le chemin du développeur, c'est npm, et rien là-dedans n'est fait maison.
Il n'y a ici aucune chaîne d'outils propriétaire et aucun CLI de plugin à apprendre. Tout le chemin repose sur un package sandbox publié contre lequel vous développez, un simple build de librairie Vite, et npm. La plateforme ne rencontre votre plugin qu'à la toute fin, quand vous enregistrez le nom du package.
- 01
Développer contre le sandbox
Le harnais est un véritable package publié, @xuda.io/plugin-sandbox en version 1.0.27. Il construit une page à deux volets : un formulaire Plugin Configurator à gauche, généré à partir des champs que votre méthode déclare, un redimensionneur par glisser au milieu, et un volet Live Preview à droite avec le div conteneur que votre méthode recevra. Un menu déroulant choisit laquelle de vos méthodes tester. Tout champ déclarant le type de rendu editor reçoit un véritable éditeur Monaco relié à son événement de changement, avec le langage tiré des paramètres de rendu et javascript par défaut. L'adoption est partielle, et il vaut mieux le dire clairement : treize des soixante deux packages livrés développent contre ce harnais, et vingt livrent leur propre page de test sous tests.
- 02
Compiler avec Vite, vers dist
Chaque package qui utilise le sandbox porte la même configuration vite. Le point d'entrée de la librairie est src/runtime.mjs, le format est ES, les imports dynamiques sont inlinés, la sortie atterrit dans dist, toute feuille de style émise est renommée en runtime.css, et une petite étape de copie déplace tout ce qui se trouve dans src, hormis runtime.mjs, vers dist à ses côtés. Cette convention explique exactement pourquoi la plateforme enregistre un drapeau dist et un drapeau css par fichier quand elle sonde votre package, et pourquoi le runtime sait qu'il doit charger dist/runtime.css en plus du module.
- 03
Publier, puis l'enregistrer
Les soixante deux fichiers package.json livrés portent tous le même script de publication : npm version patch, puis npm publish. Du semver simple sans rien ajouté par-dessus, et les records donnent une idée de l'usage que ça reçoit. Le driver de données natif en est à 1.0.320, le framework Tailwind à 1.0.128, le générateur d'app par IA à 1.0.74. S'enregistrer auprès de la plateforme est une étape distincte qui récupère le manifeste en direct depuis npm, prend la version de là plutôt que de votre formulaire, et prend l'icône marketplace dans le champ svg_icon de votre package.json si vous avez laissé l'icône vide dans le formulaire.
Ou décrivez-le, et le système de plugins construit un plugin avec un plugin.
Le builder n'est pas un outil que l'assistant de chat peut atteindre de lui-même. Ses deux méthodes sont toutes deux marquées privées, donc rien ne les expose comme outils d'agent ; l'onglet plugins prépare l'action et l'assistant l'exécute dans le cadre de cette tâche. Ce qui fait le travail est lui-même un plugin library livré, le studio AI app builder, appelé de la même façon que n'importe quelle autre méthode library. Chaque contrainte ci-dessous se trouve dans le générateur plutôt que dans un document de politique, et l'état final est un plugin privé, délibérément.
Cinq actions, et aucune de deux n'est verrouillée de la même façon.
Deux de ces éléments sont couramment décrits comme une seule et même chose, et ils ne le sont pas. Un membership Agency verrouille l'enregistrement même d'un plugin, et il porte sur la méthode d'upload. Un compte vendeur Stripe Connect actif verrouille la mise en public, et il porte sur la méthode publish, que quiconque est connecté peut appeler. Ils arrêtent des personnes différentes à des moments différents. Installer un plugin sur un projet n'est verrouillé par aucun des deux : cela nécessite une habilitation admin sur ce projet, et si le plugin est payant, cela passe d'abord par une étape de paiement.
Habilitation | Plan de membership | L'autre verrou | |
|---|---|---|---|
| Installer ou désinstaller sur un projet | Admin du projet | Un plugin payant passe d'abord par une étape de paiement | |
| Lire ou écrire une configuration de plugin | Admin du projet | La chaîne parent est revérifiée avant que quoi que ce soit ne soit touché | |
| Enregistrer un plugin, tout court | Admin du projet | Agency | Propriété appliquée à chaque mise à jour |
| Le publier sur le marketplace | Tout utilisateur connecté | Un compte vendeur Stripe Connect actif, ou un 403 | |
| L'archiver | Admin du projet | Propriétaire uniquement |
Publier est une action distincte de mettre à jour le code, délibérément.
Téléverser une nouvelle version, ou laisser le builder IA en reconstruire une, ne repasse jamais par la revue. Le cycle de vie existant est reporté tel quel : un plugin qui était privé reste privé, un plugin qui était public reste public, et l'état de revue qu'il avait déjà est celui qu'il conserve. Devenir public est une méthode distincte avec une habilitation distincte, exécutée une fois, par le propriétaire, délibérément, et c'est le seul appel qui déclenche le verrou. Tout ce qui suit dépend de cet appel unique.
Neuf choses que le système de plugins ne fait pas.
- Les plugins tiers ne peuvent pas exécuter de code serveur aujourd'hui. La politique provisoire est le refus, c'est un interrupteur de configuration, et cet interrupteur est désactivé dans les deux configurations, si bien que tout ce que vous construisez pour qu'un tiers l'installe reste une surface côté navigateur uniquement.
- Le contrat widget porte une capability headless, pour du code de plugin qui s'exécute là où il n'y a pas de document. Aucun des 62 packages livrés ne la déclare. Le contrat existe, la preuve livrée non.
- Le runtime prend en charge un export destroy optionnel sur un plugin ui. Aucun des dix plugins ui livrés n'en exporte un.
- Deux des trois drivers de données sont en lecture seule tels que livrés. Seul le natif implémente les six services. MySQL et CouchDB exportent un read et un test de connexion et rien d'autre, et demander autre chose à l'un ou l'autre lève une erreur nommant le service manquant.
- Le sélecteur d'icônes cherche à l'intérieur du kit sélectionné, pas à travers les kits installés. Et un plugin graphics peut déclarer au plus une dépendance npm, ce qui explique pourquoi chaque kit livré est un unique package amont et son arbre de répertoires.
- Installer un composant partagé ne vérifie que deux types de dépendance de plugin, le framework d'interface sur un programme et le driver de données sur une table. Un composant qui utilise un plugin widget ne le déclare pas, si bien que cette installation peut réussir alors même que le widget manque toujours.
- Le builder IA est verrouillé avant que le code ne soit empaqueté, jamais pendant son exécution. Treize motifs interdits et une énumération fixe de noms de fichiers, tous deux appliqués au texte. Aucune isolation d'exécution ne se cache derrière.
- Rien dans la plateforme ne décide quelles clés de configuration sont sûres à publier. Ce jugement est entièrement délégué à l'auteur du plugin, et un plugin qui ne pose aucun jugement remet tout son document de configuration au navigateur.
- Le chemin d'enregistrement accepte deux chaînes de type de plus que ce que le produit expose, et rien dans le catalogue n'en livre aucune des deux. Les six du sélecteur sont les six qui mènent quelque part.
Les questions que vous êtes en train de vous poser
Cinq de ces six réponses se terminent sur une limite. Elles sont ici parce que vous les rencontreriez de toute façon dans la première semaine.
Regardez un plugin s'installer et apparaître dans son emplacement.
Installez deux plugins et vous avez une couche de données et un système de design.
C'est littéralement ce avec quoi un nouveau projet est livré : le driver de données natif et le framework Tailwind, et rien d'autre. Tout ce qui vient après ces deux là est une installation, et la catégorie sur l'enregistrement décide où elle apparaît, que ce soit le panneau d'insertion, le sélecteur d'icônes, le sélecteur de driver de table, un switch sur un élément, ou la liste des actions de workflow.