Xuda Studio

Une app Xuda est un ensemble de documents typés, et un seul interpréteur les exécute.

Studio ne génère pas une base de code que vous devez ensuite maintenir. Un projet est un ensemble plat de documents, chacun estampillé docType studio, répartis en douze types de nœuds : composants, tables, unités de données, routes, agents et le reste. L'arborescence du panneau de gauche est dérivée de ces documents au chargement, à partir d'un pointeur parent porté par chacun, et n'est jamais stockée comme une arborescence à part. Ce sont ces mêmes documents qu'un aperçu demande par id, que parcourt un build, et qu'exécute le runtime quand quelqu'un ouvre l'app. Rien entre les deux ne les compile en autre chose.

Douze types de nœuds, une seule forme de document
Aucun document arbre nulle part
Chaque nœud est une URL partageable
Une app Xuda est un ensemble de documents typés, et un seul interpréteur les exécute.
12

Types de nœuds à partir desquels un projet est construit

11

Types d'événements auxquels un composant peut réagir

16

Types de déploiement pour un même projet

62

Plugins first-party, répartis en six catégories

La forme d'un projet

Cinq arborescences distinctes, et aucun document arbre nulle part.

Le panneau de gauche ne contient pas une arborescence. Il en contient cinq, et un segment à cinq icônes au-dessus décide laquelle est montée : tables, écrans, état partagé, URLs et agents sont ainsi cinq espaces d'adressage distincts plutôt que cinq branches d'un seul et même arbre. Aucune des cinq n'est stockée comme une arborescence non plus. Chaque nœud est un document portant un unique pointeur parentId et une clé de tri order_ts, et un web worker reconstruit toute la structure au chargement en cherchant les enfants niveau par niveau, en n'admettant que les documents dont le stat vaut 3. Déplacer un nœud vers le haut ou le bas n'est pas un déplacement de fichier : cela enregistre exactement deux documents dont les valeurs order_ts sont échangées. Supprimer une sous-arborescence écrit stat 4 sur chaque document qu'elle contient et les laisse tous en stockage, ce qui explique pourquoi l'arborescence cesse de les afficher et l'index de recherche cesse de les indexer au même instant. Ouvrez un composant et passez à son onglet UI, et une seconde arborescence apparaît sous la première, l'arbre d'éléments de ce composant, avec une poignée déplaçable entre les deux dont la position est mémorisée.

Une limite qui mérite d'être dite. La reconstruction scanne chaque document une fois par niveau, et c'est exactement pourquoi elle tourne dans un worker et non sur le thread principal.
Une correction

Quatre onglets, c'est vrai pour un composant. Chaque autre type ouvre autre chose.

La version de cette page que vous avez peut-être lue disait que les écrans, les tâches de fond et les points de terminaison API sont le même genre d'objet avec les mêmes quatre onglets. La première moitié est vraie, pas la seconde. L'éditeur construit sa barre d'onglets en fonction du type de nœud, et l'enveloppe se rétrécit ou s'agrandit : un composant obtient quatre onglets, un batch ou une unité de données en obtient trois et n'a aucune UI, une unité API en obtient quatre mais le quatrième est Sortie plutôt que UI, et cinq des douze types n'ouvrent qu'un seul onglet, voire aucun : alert, javascript, route et ai_agent n'ouvrent chacun qu'un seul éditeur, et un dossier n'ouvre rien du tout. La barre n'est affichée que lorsqu'un nœud a plus d'un onglet, donc ces cinq-là arrivent sans barre au-dessus. Quel onglet est ouvert est un paramètre de requête sur l'URL de l'éditeur, et le dernier onglet consulté sur un nœud donné est mémorisé par nœud, donc y revenir vous replace là où vous l'aviez laissé. La deuxième colonne est l'autre moitié de la même grammaire. Le panneau Créer propose un ensemble de types différent selon le nœud sur lequel vous avez fait un clic droit, et un dossier hérite simplement de l'ensemble de la section dans laquelle il se trouve.

Ce que ça ouvre
Où ça peut être créé
ComposantSource de données, Champs, Événements, UISous Programmes, ou à l'intérieur d'un autre composant
APISource de données, Champs, Événements, SortieSous Programmes ou toute autre unité, jamais à l'intérieur d'un composant
BatchSource de données, Champs, Événements. Pas d'onglet UIProgrammes, à l'intérieur d'un composant, ou de toute autre unité
Récupérer des données (get_data)Source de données, Champs, Événements. Pas d'onglet UIProgrammes, à l'intérieur d'un composant, ou de toute autre unité
Écrire des données (set_data)Source de données, Champs, Événements. Pas d'onglet UIProgrammes, à l'intérieur d'un composant, ou de toute autre unité
AlerteUn onglet, AlerteSous Programmes ou à l'intérieur d'un composant, nulle part ailleurs
JavaScriptUn onglet, ÉditeurSous Programmes ou à l'intérieur d'un composant, nulle part ailleurs
TableDonnées, Champs, IndexSous Base de données, où Créer se grise ensuite sur la table elle-même
GlobalesChamps, ÉvénementsPas depuis l'arborescence : la racine Globales refuse tout nouvel enfant
RouteUn onglet, RouteSous Routes, le seul type que cette section propose
Agent IA (ai_agent)Un onglet, Agent IASous Agents IA, le seul type que cette section propose
DossierRien du tout. Ouvrir un dossier ressort immédiatementSous Programmes ou Base de données, ou à l'intérieur d'un composant ou de toute autre unité
Générateur d'app par IA

L'assistant écrit dans la même arborescence que vous modifiez à la main, et un mode n'écrit rien du tout.

Décrivez ce que vous voulez dans le panneau IA d'un projet ouvert, et le générateur propose des tables, des composants, des champs et des événements comme des documents ordinaires dans l'arborescence ordinaire, ce qui explique que vous puissiez ensuite ouvrir le résultat et modifier un champ. Les deux commutateurs du panneau ne sont pas le même commutateur : la portée décide de quoi parle la conversation, le mode décide si le tour a le droit d'écrire. À l'intérieur de Studio, le compositeur masque aussi le sélecteur de modèle et le sélecteur d'agent que le reste du tableau de bord affiche, donc il n'y a ni modèle ni agent à choisir.

  • Le mode Plan n'écrit rien. Le panneau décrit Auto comme s'exécutant de bout en bout sans interruption, et Plan comme cherchant et proposant une approche sans rien modifier. Le choix voyage dans la requête sous le nom plan_mode, et le serveur le transforme en une méthode différente, avec la sauvegarde désactivée pour tout le tour.
  • La portée décide de ce à quoi le tour est lié. En portée générale, la conversation référence l'app ; en portée nœud, elle référence le nœud que vous avez ouvert, comme un studio_node plutôt que comme une app. Changer de portée démarre une nouvelle conversation et recharge la liste des conversations, car les anciennes conversations sont récupérées selon ce qu'elles référencent.
  • Les puces de contexte sont des bascules, pas des étiquettes. Jusqu'à trois apparaissent : le programme ouvert dans l'éditeur, un nœud différent sélectionné dans l'arborescence, et l'élément UI sélectionné pendant que l'onglet UI est ouvert. Décochez-en une et elle disparaît de la requête. Décochez les trois et le tour est envoyé sans aucun contexte.
  • Le générateur est un seul paquet npm avec des méthodes nommées, et deux de ces méthodes écrivent des plugins. app_builder enregistre des documents studio, app_planner ne le fait pas, et create_plugin_via_ai et modify_plugin_via_ai sont exportées du même paquet. Ce que ce paquet fait en interne n'est pas dans le code source de la plateforme, donc cette page décrit le point d'appel et s'arrête là.
Un tour
  1. Vous décrivez
    Dans le panneau IA d'un projet ouvert, en portée générale ou liée à un nœud
  2. Les puces lient le contexte
    Programme ouvert, un nœud sélectionné ailleurs dans l'arborescence, l'élément UI sélectionné
  3. Les 12 derniers messages font partie du voyage
    Chacun tronqué à 2000 caractères avant envoi
  4. app_builder enregistre, app_planner non
    Accepter un plan le renvoie avec une instruction de l'exécuter de bout en bout
Ce qui revient

Un tour de construction rend un reçu qui nomme ce qu'il a changé.

La réponse n'est pas le modèle qui raconte ce qu'il a fait. Un formateur dans la plateforme lit le résultat structuré et rédige le message : il compte les documents, les regroupe par type de menu, en liste jusqu'à douze par leur nom avec l'id du document entre crochets, et indique s'ils ont été enregistrés. Un tour de plan affiche des champs entièrement différents, un aperçu, jusqu'à douze unités proposées, huit étapes d'ordre de construction et huit vérifications d'acceptation, et indique toujours qu'il a maintenu le tour en mode planification. Quand l'assistant est bloqué sur quelque chose dont il a besoin de votre part, il ne le demande pas en prose non plus : il émet un bloc de question structuré que le panneau affiche comme un stepper, deux ou trois options par question, avec une ligne Autre en texte libre que l'interface ajoute elle-même. Pendant que le tour s'exécute, le texte de statut alterne parmi une liste fixe de trois messages, une liste par mode, changeant toutes les 1800 millisecondes.

réponse de l'assistant · studio_method : app_builder
2 composants générés, 1 table
puis les unités créées, jusqu'à 12 :
composant : <menuName> [<_id>]
composant : <menuName> [<_id>]
table : <menuName> [<_id>]
et une ligne indiquant s'ils ont été enregistrés
Test de fumée du navigateur d'aperçu en direct réussi pour 2 composants.
cette ligne n'apparaît que si le tour a exécuté le test
un tour de plan indique ceci à la place, n'ayant rien créé :
Tour maintenu en mode planification, aucun document studio enregistré.
Les étiquettes en gris sont les nôtres. Ce que le formateur fixe, c'est la ligne de comptage, une ligne par unité créée avec son id, la ligne de test de fumée et la ligne de mode planification. Les plafonds sont réels aussi : au-delà de douze unités, la liste s'arrête, donc un très gros tour est compté intégralement mais listé partiellement.
Source de données

Deux moteurs se cachent derrière un seul contrôle segmenté.

Sur une source de données table, le mode de filtre est Index ou Query, et quand le réglage est absent, le runtime déduit Index si un index est sélectionné et Query sinon. Ce ne sont pas deux habillages d'un même planificateur. Le mode Index lit une clé composée que la table a déjà écrite sur chaque enregistrement. Le mode Query lit un sélecteur assemblé au moment de la requête. Ce que vous pouvez exprimer, et ce qui vous est garanti, change selon la position de ce contrôle.

La limite honnête se trouve du côté de l'index, et elle est silencieuse. Si l'index sélectionné n'est plus sur la table, ou si l'index a des clés mais que chaque cellule de plage a été laissée vide, la construction du filtre abandonne et la lecture s'élargit à un scan de toute la table. Le même filet rattrape aussi tout ce qui est levé pendant l'assemblage du filtre, journalisé comme avertissement et rien de plus. Un index mal configuré se traduit par de la lenteur, pas par une panne.
L'onglet UI

Il n'y a pas de fiche de propriétés. Chaque propriété est un attribut.

Sélectionnez un élément dans l'arborescence et le panneau de propriétés se remplit de lignes. Ces lignes n'écrivent pas dans un blob de propriétés privé attaché au nœud. Un élément est un id, un nom de balise, une carte d'attributs, des enfants et un contenu texte, et chaque réglage du panneau est littéralement un attribut dans l'espace de noms xu-. Activez Bind et le balisage gagne xu-bind. Tapez ce que vous voulez dans Ajouter un attribut, et ça se place à côté des autres comme un attribut ordinaire. C'est pourquoi une classe écrite à la main, un attribut que vous avez inventé et un contrôle du panneau finissent tous au même endroit, et pourquoi Modifier le HTML vous montre toute la vérité sur un élément plutôt qu'un rendu de celui-ci.

  • Le panneau s'ouvre avec trois lignes, xu-show, xu-bind et xu-ref, et s'étoffe à mesure que vous utilisez les accordéons en dessous. Quinze autres noms sont exclus de la liste d'attributs libres car chacun possède déjà son propre contrôle dédié, si bien qu'aucun attribut n'est modifiable à deux endroits à la fois.
  • Show et Render ne sont pas deux mots pour dire masquer, et le runtime le dit lui-même dans son propre commentaire. xu-show est une bascule d'affichage et l'élément reste monté. Une porte de rendu fait basculer toute une sous-arborescence dans ou hors du document, donc l'élément est tout simplement absent du DOM.
  • Tout attribut a un jumeau calculé : écrivez xu-exp: devant le nom, ou appuyez sur le bouton fx à côté du champ, et la valeur devient une expression qui se recalcule quand ses dépendances changent. C'est générique plutôt qu'une liste soignée de propriétés liables, donc ça marche sur src, sur un attribut que vous avez tapé il y a une minute, et sur des champs qu'un plugin a déclarés. Une valeur et son jumeau calculé occupent le même emplacement plutôt qu'un défaut plus une surcharge, donc Studio signale un élément qui porte les deux et propose de retirer la version statique. Une exception documentée : xu-exp:class remplace l'attribut class statique au lieu de le fusionner.
  • La limite, clairement : il n'y a pas d'UI de style. Pas de contrôles d'espacement, pas de contrôles de typographie, pas de sélecteur de couleur, pas d'éditeur de modèle de boîte. Le style, c'est une rangée de puces de classe plus un éditeur CSS brut, donc une valeur de padding est une chaîne de classe Tailwind ou Ionic que vous tapez ou collez, et une classe conditionnelle est un nom de classe mappé à une expression à l'intérieur de xu-class.
Un seul espace de noms, et tout est livré
xu-ref, le nom sous lequel l'élément s'enregistre
xu-bind, le champ qu'il lit et écrit
xu-show, une bascule d'affichage qui le laisse monté
xu-if, xu-else-if, xu-else, une seule chaîne de branches
xu-for, avec xu-for-val et xu-for-key nommant les variables de boucle
xu-class, des noms de classe mappés à des expressions, stockés en JSON
xu-script, xu-style et xu-cdn, attachés à un seul élément
xu-exp: devant n'importe lequel des précédents, calculé plutôt que littéral
Canevas et runtime

Le canevas est une surface de mise en page. Le runtime n'est qu'un bouton plus loin.

Les deux surfaces lisent le même arbre d'éléments, et elles en font des choses différentes. Le canevas de conception rend votre balisage pour que vous puissiez composer et sélectionner, mais il ne fait pas tourner votre app : les composants et widgets imbriqués s'affichent comme des espaces réservés étiquetés, et les conditions sont comparées comme du texte plutôt qu'évaluées. Le bouton bascule de la barre d'outils remplace le canevas par l'aperçu en direct, qui est le vrai runtime, avec une fenêtre Props pour injecter des valeurs de test. Savoir sur quelle surface on se trouve explique la plupart des moments où un écran paraît correct à un endroit et se comporte différemment à l'autre.

Les deux surfaces sont honnêtes une fois qu'on sait laquelle est laquelle. Dans le canevas de conception, xu-panel et xu-widget n'affichent jamais leur contenu, et les attributs de condition sont comparés comme du texte plutôt qu'évalués. Tout ce qui s'exécute réellement s'exécute de l'autre côté du bouton bascule.
Exécution

Un workflow est une file de jobs, pas une liste d'awaits.

Un événement n'exécute pas ses actions les unes après les autres. Il parcourt le workflow une fois et pousse un job pour chaque étape activée dans un unique tableau tenu sur la session, et une boucle de vidage distincte, qui tourne chaque milliseconde, prend le premier job qui n'est pas déjà marqué occupé et l'exécute. L'ordre est l'ordre du tableau, de haut en bas. Ce qui déclenche l'étape suivante n'est pas la fin de l'étape précédente, c'est la suppression par l'étape précédente de son propre job. Le drapeau async n'est qu'une ligne à l'intérieur de cette boucle.

Ce qui n'est pas dans ce tableau : aucune branche, aucune boucle et aucun bloc parallèle. La répétition vient d'un événement d'intervalle côté client ou de l'appel d'un autre programme, et le seul primitif de concurrence sur la page est ce drapeau async par action. L'imbrication est une insertion plutôt qu'une pile : une étape qui appelle un autre programme voit les jobs de ce programme insérés directement derrière les siens, donc ils s'exécutent avant l'étape suivante de l'appelant. Aucune limite supérieure sur les actions async concurrentes n'apparaît nulle part dans le code source.
Le catalogue d'actions

Vingt-quatre actions en neuf groupes, et l'argument que chacune lit.

Le sélecteur Ajouter une action est construit à partir d'un seul fichier catalogue, et en dehors des entrées IA, chaque action s'accorde de trois façons : le catalogue, la carte des gestionnaires dans le runtime, et le registre du validateur qui enregistre la clé de charge utile que chaque action lit. Ce registre est la colonne de droite. Deux choses avant le tableau. Invoke Action propose onze valeurs dans son menu déroulant et neuf d'entre elles fonctionnent, car act_close et act_back sont commentées à la fois dans le runtime et dans la liste blanche du validateur. Et sept actions d'éléments UI plus anciennes sont carrément rejetées comme dépréciées plutôt que tolérées silencieusement. Vous pouvez aussi créer une action sans le sélecteur : glissez un nœud de l'arborescence sur le workflow et le type de nœud décide de l'action, donc un nœud get data devient Get Data et un composant devient Call Page. Déposez-le sur une ligne qui existe déjà et cela reciblera cette ligne plutôt que d'en ajouter une nouvelle.

Les actions du groupe
Ce que l'action lit
Navigation, 3Call Page, Call Modal, Call PopoverChacune nomme le composant à ouvrir, et c'est toute la charge utile.
Base de données, 3Get Data, Set Data, BatchL'unité de source de données à exécuter. Set Data et Batch nomment aussi le champ où atterrit leur résultat.
JavaScript, 4Call Native JS, Call Evaluate JS, Execute Native JS, Execute Evaluate JSLes deux actions Call nomment une unité programme JavaScript de l'arborescence. Les deux actions Execute portent le code lui-même, tapé directement dans l'action.
Gestionnaires d'événements, 4Raise Event, Emit Event, Invoke Action, DelayRaise Event nomme un événement plus ses paramètres, chacun une valeur littérale ou une expression. Invoke Action lit l'une des neuf valeurs autorisées. Delay lit un nombre de millisecondes.
Utilitaires UI, 4Call Alert, Alert, Loader On, Loader OffCall Alert nomme une unité alerte. Alert porte son propre mode d'affichage, type, titre et corps. Les deux actions loader ne lisent rien.
API, 3Call Project API, Call System API, Call External APIUne API projet nomme une unité de votre propre arborescence, une API système nomme une méthode, une API externe prend une URL et une méthode HTTP.
Dataset, 1UpdateUne valeur, écrite comme « @champ : expression ; @autre : expression », ou comme un objet JSON5.
Plugins, 1Call LibraryLe nom du plugin et la méthode dessus, donc une bibliothèque achetée se lit comme n'importe quelle autre action de ce tableau.
Commentaire, 1CommentRien. Le registre le décrit comme documentation uniquement, et le gestionnaire du runtime ne fait rien d'autre que supprimer son propre job.
Jusqu'au vrai code

L'échappatoire est un chemin, pas une case de texte.

Sept endroits vous laissent arrêter de configurer et commencer à écrire, et le plus profond d'entre eux n'est même pas une action : un programme JavaScript est sa propre unité dans l'arborescence, avec sa propre sortie de build et un ensemble documenté de choses qui lui sont fournies. Deux limites à mentionner dans le même souffle. Le validateur ne parse pas le corps JavaScript, donc une erreur de syntaxe surgit du build comme une compilation échouée plutôt que du vérificateur, qui ne fait qu'avertir quand le script est vide. Et un script en ligne ou un attribut style sur un élément ne doit contenir aucun caractère guillemet, aucun.

  • Le corps d'un programme reçoit dix arguments nommés : params, xu, SESSION_ID, SESSION_OBJ, jobNoP, $container, func, glb, APP_OBJ et evt. Sans import ni export de premier niveau, il est enveloppé dans une fonction async, donc le top level await fonctionne.
  • Écrivez un import ou un export et il devient un vrai module ES à la place. Les spécificateurs relatifs sont réécrits par rapport au dossier scripts de ce programme, donc une unité peut importer ses propres fichiers frères.
  • Enregistrer une unité JavaScript déclenche un build Vite sans bloquer la sauvegarde. À l'exécution, le moteur préfère le module compilé, avec cache invalidé par build, ne charge une feuille de style que si le module en exporte une, et se rabat sur l'exécution de la source avec le code d'avertissement RUN_MSG_JS_010.
  • L'objet xu qu'un script reçoit est une API à quinze méthodes : lire et écrire un champ, invoquer un événement, quatre appels base de données, appeler un autre script, trois sortes d'appel API, alerter, journaliser, et lire ou écrire sur le drive. Le code en ligne a une forme différente encore, enveloppé comme une fonction async et appelé avec l'élément et l'événement navigateur.
Chaque porte, toutes livrées
Un emplacement d'expression, y compris les deux emplacements de condition
Une unité programme JavaScript, construite à l'enregistrement
Call JavaScript, exécutant cette unité depuis un workflow
Execute JavaScript, code en ligne recevant l'élément et l'événement
xu-script sur un élément, exécuté dès qu'il devient visible
xu-style et xu-style-global, scopé ou non, sur un élément
xu-cdn, une URL CSS ou JS externe chargée avant le rendu du nœud
Aperçu en direct

L'aperçu est un pair de votre éditeur, et il a le droit de renoncer à vous.

Une session d'aperçu s'ouvre sur un canal nommé d'après votre jeton de session et demande à votre onglet ouvert des documents un id à la fois. Studio répond depuis la mémoire avec une copie du document présent sur votre écran, donc il n'y a aucun bundler nulle part sur ce chemin, et un changement enregistré est présent dans la session en cours immédiatement. Il y a aussi une échéance. Si votre onglet n'a pas répondu en cinq secondes, cette session bascule vers la copie compilée sur le serveur et ne vous redemandera plus jamais pour le reste de sa vie.

Une correction à ce que ce schéma disait auparavant. La relance à l'enregistrement n'est pas document_changed. Un changement de code enregistré voyage sous studio_doc_saved et porte le document entier, ce qui explique pourquoi l'aperçu redessine les panneaux liés à ce document plutôt que de recharger la page. document_changed est un tout autre canal, relayé depuis la salle socket du serveur, et tout ce qu'il fait est réinterroger les sources de données temps réel dont l'id de table correspond. Le verrou des cinq secondes est le coût honnête de tout cet arrangement, et c'est pourquoi Rechargement forcé est un bouton de première classe plutôt qu'un ajout de dépannage.
La console

La boîte de commande, c'est eval, exécuté à l'intérieur de la page que vous prévisualisez.

Il n'y a pas de vocabulaire de commandes à apprendre, car il n'y a pas d'ensemble de commandes. Ce que vous tapez est envoyé à l'aperçu comme une seule chaîne, évalué là-bas, et le résultat est renvoyé et affiché comme une arborescence sur deux niveaux de profondeur. Tout ce que la page en cours peut voir, vous pouvez le voir : l'objet session, les sources de données, les utilitaires du runtime. Une erreur levée revient avec un drapeau d'erreur et s'affiche en rouge. Inspect est l'autre moitié : activez-le, chaque élément portant un id ui se contourne au survol, et cliquer sur l'un renvoie le composant qui le possède et le nœud à l'intérieur, avec un bouton qui ouvre ce composant sur son onglet UI et sélectionne le nœud dans l'arborescence. C'est un pixel jusqu'à un nœud en deux clics.

... connexion à la session
$ new URLSearchParams(location.search).get("engine_mode")
"live_preview"
$ typeof func.index.delete_pouch
"function"
$ SESSION_OB.root_element
SESSION_OB is not defined error: true
inspect activé . chaque élément avec un id ui se contourne au survol
cliquez sur un pixel dans l'aperçu
live_preview_element_reference_result { prog_id, node } [ Ouvrir ]
Ouvrir . le composant propriétaire s'ouvre sur son onglet UI, et ce nœud est sélectionné dans l'arborescence
get session . live_preview_session_result
{ __type: "Element", tagName, id, className } "[Circular]" "[Function name]"
root_element, WORKER_OBJ et DS_UI_EVENTS_GLB sont retirés avant l'envoi
L'invite est un signe dollar une fois que la session se déclare prête, et trois points pendant qu'elle se connecte encore. La flèche du haut parcourt les commandes tapées, en mémoire uniquement, donc un rechargement vous fait repartir avec un historique vide.
Enregistrement

Un enregistrement est une écriture locale. La version est écrite par autre chose, après coup.

Il n'y a pas d'enregistrement automatique. Appuyer sur enregistrer remet le document à un worker, qui l'estampille et l'écrit dans une base de données à l'intérieur de votre navigateur, et c'est tout ce que fait votre frappe de touche. Tout ce qui vient après relève d'une autre machinerie : une diffusion vers vos autres onglets, une réplication vers la base de données de l'app, et un processus serveur qui surveille cette base de données, lequel est ce qui frappe réellement une version. L'ordre compte, car il indique ce qui survit à un réseau coupé et ce qui ne survit pas.

La conséquence est la partie honnête. Votre travail est à l'abri d'un réseau coupé parce que la première écriture est locale, mais le versionnement n'est pas à vous de le chronométrer : la pastille peut afficher v et une ellipse un instant pendant que la capture arrive, et la fenêtre repasse en sondage sur un délai fixe et croissant de 1200, 3000, 6000, 12000 et 24000 millisecondes jusqu'à résolution. La même architecture a une contrepartie. Comme la capture surveille la base de données plutôt que votre éditeur, une modification écrite par un plugin ou par un processus serveur est versionnée exactement de la même manière qu'une modification que vous avez tapée.
Historique

La pastille a quatre états, et l'un d'eux dit que votre onglet n'est pas à jour.

L'étiquette à côté de l'éditeur est dérivée, pas stockée : elle compare la révision que vous avez enregistrée au dernier instantané sur le serveur. À jour signifie qu'un instantané exact existe pour votre révision, ou que la différence n'est que des métadonnées de build. Enregistrement en cours signifie que votre révision est en avance et que la capture n'a pas encore atterri. En retard signifie que votre révision enregistrée est plus ancienne que le dernier instantané, donc cette session de navigateur a dormi pendant des révisions, et la fenêtre le dit dans une bannière plutôt que de prétendre que vous êtes à jour. Inconnu signifie même génération, contenu différent, ce que la source appelle une branche divergente. Sélectionner une ligne plus ancienne la prévisualise sur place, votre copie de travail étant conservée par référence, et pendant qu'un aperçu est affiché, les deux chemins d'enregistrement refusent le document purement et simplement. Restaurer est la seule action qui écrit : elle relit d'abord la révision en direct depuis le serveur, demande avant d'écraser un changement arrivé pendant que vous regardiez, redemande avant d'abandonner vos modifications non enregistrées, puis se chaîne sur la révision connue la plus récente afin que la restauration atterrisse proprement en tête.

Aperçu
aperçu sur place, enregistrement bloqué, échap remet votre copie de travail
Restaurer
restaurer enregistre en avant, donc la restauration est réversible aussi

enregistrer, capturé sur le serveur un instant plus tard

Ce qui n'est pas là, intégralement : pas de branches, pas de vue comparaison ou diff, pas de fusion, pas de messages de commit. L'historique est par programme plutôt que par projet. Les numéros de version sont positionnels, donc le v3 d'aujourd'hui n'est pas le v3 de la semaine prochaine. Cinquante instantanés sont conservés par document et le reste est supprimé plutôt qu'archivé. La capture est asynchrone et côté serveur, donc un enregistrement frais se lit comme enregistrement en cours jusqu'à ce qu'il atterrisse. Et l'annulation est un système différent qui ne partage rien avec celui-ci : cinquante étapes par document, regroupées à quatre cents millisecondes pour qu'un glissement ou une rafale de frappe soit une seule étape, cantonnée au nœud sélectionné, tenue en mémoire navigateur uniquement, et perdue au rechargement. Après un rechargement, ce rail est le seul chemin de retour.

Deux éditeurs

Il y a un verrou. Il n'y a pas de fusion.

Le verrou est un check-in, et il est par nœud. L'enregistrement en dépend : un nœud verrouillé par quelqu'un d'autre est refusé avec le nom de qui le détient, et un nœud que personne n'a verrouillé est refusé avec Veuillez faire un check-in, après avoir remonté la chaîne des parents pour voir si quelque chose au-dessus est détenu. Les brouillons sont exemptés, tout comme une app qui n'a jamais été enregistrée. Ce qui n'est pas verrouillé, c'est le résultat une fois qu'une révision de quelqu'un d'autre atteint effectivement un document que vous avez ouvert. Studio tranche entre exactement trois cas, et il calcule si vous avez des modifications non enregistrées avant de mettre à jour sa base de référence, ce qui est l'ordre qui rend la réponse fiable.

  1. 01

    Votre copie est propre, donc vous prenez la leur en entier

    Rien de ce qui est à vous n'est en jeu, donc Studio avance directement le document ouvert jusqu'à leur révision en bloc et rafraîchit le drapeau d'onglet immédiatement plutôt que d'attendre l'observateur limité. Vous le découvrez parce que le contenu a changé sous vos yeux, pas parce qu'une boîte de dialogue vous l'a demandé.

  2. 02

    Vous avez des modifications, et leur écriture était le compilateur

    Studio compare les deux documents avec la liste de révisions, les métadonnées studio, le résultat de build, l'erreur de build, les trois horodatages et les dépendances de script retirés. Quand ce qui reste est identique, la révision arrivante est le worker de build enregistrant sa propre sortie. Vos modifications restent exactement là où elles sont, seuls la révision et les champs de build sont adoptés, et on ne vous en informe pas, car rien ne vous est arrivé.

  3. 03

    Vous avez des modifications, et leur contenu diffère vraiment

    Chacune de vos modifications est conservée et seule la révision est incrémentée, donc votre prochain enregistrement se chaîne sur le leur plutôt que d'entrer en collision. Puis vous êtes averti pendant douze secondes : un collaborateur a enregistré ce document, et vos modifications non enregistrées écraseront les siennes si vous enregistrez. L'avertissement a exactement une action, abandonner les miennes et utiliser les leurs, qui remplace votre copie de travail par le document serveur. Ignorez-le et enregistrez, et c'est votre version qui survit. Il n'y a pas de fusion au niveau des champs, pas de diff, pas de présence et pas de curseurs partagés.

Validation

Un seul fichier de vérification, exécuté dans votre navigateur ou sur le serveur selon où vous êtes.

La validation n'est pas un service ailleurs. C'est un seul fichier, runtime/js/modules/xuda-studio-checker.mjs, 4714 lignes, importé directement par le front-end de Studio et importé par chemin sur le serveur par le module app. Studio décide au moment de l'appel quel hôte l'exécute. En ligne, sur un projet qui n'est pas un brouillon, il déclenche run_programs_checker comme tâche de fond. Hors ligne, ou dans un brouillon, il exécute la même vérification sur chaque programme dans le navigateur et valide un résultat de même forme. Les constats se répartissent en erreurs et avertissements sur rien de plus qu'un champ type valant E ou W. Puis la partie honnête : cela ne bloque pas un build. build_app prend ignore_errors avec un défaut à true, aucun appelant du produit n'envoie jamais false, et le total d'erreurs est écrit dans la release elle-même, donc chaque version enregistrée porte le nombre d'erreurs avec lesquelles elle a été livrée.

  • Dix-huit fonctions de vérification se répartissent sur onze types de programmes : globals, table, component, javascript, get_data, set_data, batch, api, alert, route et ai_agent. Un dossier est un no-op délibéré, et tout ce qui n'est pas reconnu lève CHK_MSG_GEN_050.
  • Une exécution serveur écrit un document de vérification par programme, indexé sur la révision de ce programme, donc un programme auquel personne n'a touché depuis le dernier passage est carrément sauté plutôt que revérifié.
  • Chaque code du catalogue généré porte les mêmes champs : titre, gravité, domaine, catégorie, résumé, causes, étapes de résolution, un slug d'aide, des schémas hérités et des codes liés. Il est généré à partir du registre d'erreurs, pas écrit à la main.
  • Les deux hôtes ne produisent pas une sortie identique. L'exécution serveur est initialisée avec la liste des déploiements et peut marquer un programme comme utilisé par un déploiement. L'exécution navigateur ne l'est pas, donc le verdict non utilisé diffère entre les deux.
Plus grandes familles de codes du vérificateur, par références dans le code source
CHK_MSG_OBJ_GEN
81 références
Règles générales d'objet, de loin la famille que vous rencontrez en premier
CHK_MSG_PRG_TRG
48 références
Déclencheurs de workflow, la deuxième surface la plus chargée de l'arborescence
CHK_MSG_PRG_FLD
17 références
Champs, y compris ce à partir de quoi un champ peut être calculé
CHK_MSG_PRG_AGN
17 références
Agents, à égalité avec les champs pour la troisième place sur dix-sept familles
Livraison

Un build et un déploiement sont des objets différents, et une release se trouve entre les deux.

Construire écrit un instantané numéroté. Déployer pointe une machine vers l'un d'eux. Le numéro sur le badge, #37, est app_build_id, un compteur par projet qui s'incrémente de un à chaque exécution de build_app. Le 1.4.0 juste à côté est une chaîne distincte que vous tapez, validée par un simple motif majeur, mineur, patch dans le badge lui-même. Ils évoluent indépendamment, et tous deux sont estampillés sur le document de déploiement afin qu'un déploiement enregistre toujours exactement quel instantané il sert.

Quand le build #38 atterrit, chaque déploiement du projet sous le code de statut 4 voit update_required passer à true et reçoit une notification new_version_available portant le nouvel id de build. Rien n'est copié sur aucune machine par cela, et ce déploiement continue de servir #37 jusqu'à ce que quelqu'un le déplace. Déplacer un déploiement vers un nouveau build, c'est update_app_version, un job distinct en dix étapes à l'habilitation admin. Il se reconnecte en SSH avec jusqu'à dix tentatives à dix secondes d'intervalle, resynchronise le drive, recalcule quels programmes et tables ce déploiement peut atteindre, supprime et recopie à la fois le dossier de programme et le dossier runtime, réinstalle le paquet de plugins, exécute pm2 delete all, redémarre le routeur websocket et l'écosystème, purge le CDN et écrit un nouveau bundle de démarrage. Les processus s'arrêtent et redémarrent. Deux déploiements d'un même projet peuvent rester indéfiniment sur des numéros de build différents, et rien ne les réconcilie pour vous.
Provisionnement

Appuyer sur déployer n'installe rien.

L'appel de déploiement est la première moitié d'une poignée de main. Il crée des enregistrements, demande une machine et retourne pendant que la machine est encore en cours de création. L'installation démarre quand la machine elle-même rappelle la plateforme, ce qui explique pourquoi la barre de progression peut rester immobile un moment au milieu et pourquoi l'échelle de statuts a bel et bien un échelon d'attente : 1 créé, 2 en installation ou en attente, 3 actif, 4 échoué ou terminé.

  1. 01

    Le formulaire retourne avant que quoi que ce soit existe

    deploy_app écrit le document de déploiement, éventuellement un document datacenter, demande à l'hôte de créer une machine de façon asynchrone, et vous retourne le contrôle. Rien n'a été installé et rien n'a été copié. Le déploiement se trouve au code de statut 2 sous la description serveur de ferme en attente d'installation, et il n'y a vraiment rien de plus à montrer avant que l'étape suivante se produise d'elle-même.

  2. 02

    La machine rappelle la plateforme

    Le premier démarrage sur la nouvelle machine interroge un webhook sur la plateforme portant l'id de l'app. Ce gestionnaire rouvre le même job que vous surveillez déjà, remplace sa liste d'étapes par la liste d'installation à vingt-quatre entrées, et exécute install_hosting_resources. Le job que vous avez démarré pour la phase un est le job qui rend compte de la phase deux, ce qui explique pourquoi les noms d'étapes changent sous vos yeux en cours de route.

  3. 03

    L'installation s'exécute, réessaie si nécessaire, et atterrit au statut 3

    L'installation est enveloppée dans quatre tentatives avec un délai de trente secondes, car une machine qui vient d'être créée peut encore redémarrer quand la première connexion SSH arrive. Elle écrit le fichier écosystème pm2, exécute pm2 delete puis pm2 start dessus, et se termine au code de statut 3, serveur prêt. Cette page ne cite aucune durée pour tout cela, car rien dans le code source n'en enregistre une et nous ne l'avons pas mesurée.

Types de déploiement

Seize types de déploiement, et ce que chacun exécute réellement.

Le même projet se déploie en seize types de déploiement déclarés, en quatre groupes : six web, cinq serveur, trois desktop et deux mobile. Chaque type déclare aussi son propre ensemble d'onglets à partir d'une liste de base de quatorze onglets, donc un déploiement cron abandonne launch, domain, firewall, analytics et user assist et se résout à neuf onglets, tandis qu'une MPA garde les quatorze et les cinq types artefact insèrent un onglet publish à la place. Regroupés selon ce que l'installation écrit réellement dans le fichier écosystème pm2, quinze d'entre eux se ramènent à cinq formes. Le seizième, mcp, est déclaré aux côtés des autres et n'a pas d'entrée écosystème propre que nous ayons pu tracer.

Ce qui tourne sur la machine
Ce que ça lance
Web : spa, mpa, pwa, embedUne app pm2 nommée webserver, mode d'exécution cluster avec instances réglées sur max, plus une unité systemd pour le routeur websocketLe bundle runtime estampillé, en HTTPS sur le port 443, lié sur un socket double pile
Web : previewLa même app webserver, et une seconde app pm2 nommée previewUne sortie serveur Nuxt sur le port 3023 à côté de l'app elle-même
Web : user groupLa même app webserver, et une seconde app pm2 nommée user_groupUne sortie serveur Nuxt sur le port 3022 pour la surface groupe
Serveur : cron, api, node, emitterUne seule app pm2 en mode d'exécution fork, exécutant le fichier d'entrée de ce type, xuda-cron.mjs ou xuda-api.mjs et ainsi de suiteLe bundle serveur pour ce build. Un déclenchement cron forke un enfant, attend qu'il se déclare prêt, lui envoie l'exécution, et journalise ce qui revient
Artefacts : ios, android, macos, windows, linuxRien. Le chemin macOS contourne délibérément l'appel de déploiement et écrit un document de déploiement sans machine d'hébergement derrièreUn onglet publish, et un envoi à un service de build externe. Nous avons tracé assez pour savoir que c'est un pipeline différent, pas assez pour le décrire ici
Le runtime

Un interpréteur, deux côtés, un seul appel.

Ce qu'un déploiement livre réellement n'est ni du code source généré ni un blob compilé. C'est un bundle interpréteur plus votre arborescence de programmes en JSON, et le même interpréteur tourne des deux côtés du fil. Dans le navigateur, c'est une balise script et un constructeur. Sur le serveur, c'est un import dynamique du bundle frère et un drapeau. Quand une page est rendue côté serveur, le serveur demande au worker d'exécuter un programme jusqu'à une chaîne, et récupère à la fois du balisage et une charge utile. La charge utile est intégrée dans la page, et l'élément de montage est marqué avec le mode de rendu et l'activation client que le serveur a résolus pour lui.

Deux limites à retenir de ce tableau. Il n'y a pas de tree shaking par app : chaque déploiement livre le bundle complet pour son build et seul le JSON des programmes diffère, et bien que des bundles slim et mini existent dans le dossier runtime, les moteurs de rendu de déploiement référencent le bundle complet. Et si la voie serveur existe du tout est un réglage par déploiement résolu à chaque rendu de page : le mode computing se résout en main, worker ou server, et main comme worker forcent tous deux le rendu client sans aucune activation serveur, ce qui laisse le navigateur tout faire.
Plugins

La catégorie dans le nom du paquet décide où un plugin apparaît.

Les 62 plugins first-party sont tous nommés de la même façon, et la catégorie dans ce nom se trouve aussi sur l'enregistrement du plugin. C'est toute la règle de routage : installez-en un et il apparaît dans le sélecteur de source de données, la barre d'outils d'insertion, le sélecteur d'icônes ou la liste d'actions de workflow, sans rien à raccorder. Une installation n'est pas non plus un simple drapeau de fonctionnalité. Le serveur écrit un enregistrement sur le projet portant la version, le type et le manifeste de fichiers, exécute un vrai npm install dans le dossier de plugins pour ce projet, synchronise ce dossier vers les serveurs, et copie tout l'arbre de fichiers du paquet dans la base de données du projet, indexé par chemin relatif.

Deux des quatre refus concernent cette paire : moins trois quand le parent nommé n'est pas installé dans ce projet, moins quatre quand l'enregistrement du parent ne liste pas cet enfant. Les deux autres sont moins un pour un plugin qui n'existe pas et moins deux quand l'appelant a demandé uniquement des plugins gratuits. Le consentement va plus loin que la simple commodité, car un fichier serveur de plugin fusionne dans le même objet script que son parent, donc un plugin first-party qui dépend d'un plugin tiers est déclassé au niveau de confiance du tiers.
Limites honnêtes

Neuf choses que Studio ne fait pas.

  • La boîte de prompt sur l'écran de démarrage de Studio ne construit rien : elle envoie une simple requête de chat plafonnée à 1024 jetons, et avec les messages masqués, la réponse n'est même pas affichée, car le générateur vit dans le panneau IA d'un projet déjà ouvert.
  • Choisir Modèle n'ouvre pas une galerie dans l'éditeur : cela ouvre l'assistant de création du tableau de bord dans un second onglet de navigateur, et le projet qu'il crée par fork est un instantané sans aucun lien vers sa source par la suite.
  • Un brouillon n'est pas sur un serveur : il vit dans un profil de navigateur sur un appareil jusqu'à ce que vous appuyiez sur Enregistrer, et effacer les données du site y met fin.
  • La revendication de check-in qui conditionne l'enregistrement n'est pas un bail serveur : c'est un champ sur le document, donc un enregistrement qui n'atteint jamais le serveur ne le libère jamais, et rien ne fusionne deux modifications pour vous.
  • Rien ne peut faire échouer un build sur des erreurs de validation : le vérificateur s'exécute d'abord et son total d'erreurs est écrit dans la release, mais aucun appelant ne demande au build de refuser.
  • Un build terminé ne touche à rien de ce qui est déjà en ligne, et deux déploiements d'un même projet peuvent rester sur des numéros de build différents aussi longtemps que vous les laissez.
  • Il n'y a pas de déploiement progressif : le badge de build peut afficher une bannière en préproduction mais pas en ligne, mais un build normal attribue au numéro en préproduction et au numéro en ligne la même valeur, donc cette bannière ne peut jamais s'afficher.
  • Il n'y a pas d'allègement du runtime par app, donc chaque déploiement livre le même bundle interpréteur complet pour son build et seul le JSON des programmes diffère.
  • Un déploiement planifié ne fera tourner aucun écran, seulement des programmes batch, get_data et set_data, et une étape JavaScript côté serveur est stoppée à soixante secondes.
Avant de vous engager

Les questions que vous êtes en train de vous poser

Chacune de ces sept réponses se termine sur une limite. Elles sont ici parce que vous rencontreriez de toute façon les sept dès la première semaine.

Regarder

Voyez Studio ouvrir un projet et remplir l'arborescence.

Ouvrez un projet vide et regardez ce qui s'écrit.

Deux documents, une route, et l'éditeur est ouvert. Rien n'atteint un serveur avant que vous enregistriez, et rien n'atteint un visiteur avant que vous construisiez un build, puis déplaciez délibérément un déploiement vers ce build.