Outils JavaScript — explication
De quoi s'agit-il ?
Un menu JavaScript dont les commandes viennent d'un fichier TOML, chacune exécutée là où l'outil l'a demandé — un popup, une fenêtre de terminal ou une fenêtre d'édition — et les fichiers ouverts relus ensuite. Cette page explique pourquoi chacun de ces choix est ce qu'il est.
Pourquoi la sortie a trois destinations, et un popup par défaut
La première version de ce mécanisme, dans Turbo Go, mettait chaque commande dans une fenêtre de terminal, et c'était le mauvais défaut pour la plupart d'entre elles.
Un terminal est la bonne réponse quand le programme est interactif ou long : node main.js sur un script qui lit process.stdin doit pouvoir recevoir une réponse, et npm start sur un projet dont le script start est un serveur HTTP doit pouvoir être interrompu par Ctrl-C. Rien de cela n'est vrai de node --test, qui imprime son rapport et se termine. Lui donner une fenêtre entière — qu'il faut ensuite fermer, sur un bureau où les fenêtres se chevauchent et sont numérotées — c'est plus de cérémonie que le résultat ne mérite.
Un popup est la bonne réponse pour une commande qu'on lance, qu'on lit et qu'on écarte. Il est modal, ce qui est un vrai coût et est nommé dans le guide : un npm install dont vous n'attendiez pas la lenteur — il télécharge la moitié du registre — retient l'éditeur jusqu'à ce qu'il finisse ou que vous appuyiez sur Échap. Ce coût a été accepté à dessein, parce que l'alternative — un dialogue qui surgit trois secondes plus tard — avale ce qu'on était en train de taper à l'instant où il arrive.
Le popup s'ouvre donc immédiatement et se remplit. Vous voyez la progression, rien ne vous surprend, et Échap le ferme et arrête la commande, ce qui est la seule façon d'interrompre quelque chose dont la sortie n'est pas dans un terminal.
Une fenêtre d'édition est la bonne réponse pour une sortie que vous allez parcourir : un long rapport de tests, ou le JSON que npm ls --json imprime. C'est un tampon ordinaire, Ctrl-F le fouille et Save as le conserve. Elle est remplie une fois la commande terminée plutôt qu'au fil de l'eau, parce qu'un tampon qui grandit sous le curseur pendant qu'on le fouille est le contraire de ce pour quoi ce mode existe.
Aucune des trois ne convient à tout, c'est pourquoi output est dans le fichier plutôt que dans le code. Run est l'exemple travaillé : il dit terminal, et le commentaire à côté dit pourquoi. Start aussi, parce que ce qu'il y a derrière npm start est presque toujours un serveur.
Pourquoi Install vient en premier
Dans le fichier de départ de Turbo Golo le premier outil exécute le script, parce que pour un langage de script sans dépendances « qu'est-ce que ça imprime ? » est la question posée le plus souvent. Un projet Node a des dépendances avant d'avoir quoi que ce soit d'autre : un clone frais ne fait rien — ni exécuter, ni tester, ni linter — tant que npm install n'a pas écrit node_modules/, et chaque autre commande du menu suppose que c'est fait. La première ligne du menu JavaScript installe donc, et les autres suivent dans l'ordre où l'on travaille d'habitude sur un projet : formater, linter, tester, exécuter.
Pourquoi il n'y a pas de Build
JavaScript n'a pas d'étape de compilation, et une entrée de menu qui prétendrait le contraire lancerait quelque chose. Le premier outil de Turbo MoonBit est moon check et le troisième de Turbo Rust est cargo build, parce que ces langages ont un compilateur qui a un avis. Node lit le source. Ce qui s'en approche le plus — node --check main.js, qui parse un fichier sans l'exécuter — est réel et peu coûteux, et c'est laissé au projet de l'ajouter, parce que le serveur de langage marque déjà chaque erreur de syntaxe dans la gouttière pendant que vous tapez, ce qui est la même vérification plus tôt.
Un projet avec un bundler ou une étape TypeScript a un build, et c'est une ligne dans le fichier : npx vite build, npx tsc. Le fichier de départ ne peut pas savoir laquelle, il n'en nomme donc aucune.
Pourquoi une fenêtre de terminal est toujours là
L'éditeur en avait déjà une — un vrai pseudo-terminal avec un émulateur VT, construit pour les fenêtres F8 — si bien qu'output = "terminal" coûte un champ dans ses options et achète pour rien les couleurs, la pagination, Ctrl-C, la saisie clavier et le défilement arrière, parce que ce sont les mêmes mécanismes qu'utilise n'importe quel terminal. Les outils de Node eux-mêmes utilisent ces couleurs : node --test dessine des coches vertes et des croix rouges, et npm install dessine une barre de progression.
La fenêtre reste après la fin de la commande, et c'est le but : la sortie est ce que vous avez demandé, et une fenêtre qui disparaîtrait avec elle serait inutile.
Pourquoi le code de sortie est toujours dans le titre
npx prettier --write . sur un projet déjà formaté n'imprime rien du tout, et npx eslint . sur un projet propre non plus. Un popup au corps vide et au titre neutre ne se distingue pas d'un popup dont la commande n'a pas démarré, et le lecteur est laissé à deviner la seule chose qu'il voulait savoir.
Le titre porte donc le verdict — — ok ou — exit 1 — et un corps vide dit (no output) une fois la commande terminée. Pendant qu'elle tourne encore le corps reste vide, parce que « (no output) » est un verdict et qu'une commande en cours n'en a pas encore atteint.
Pourquoi les commandes sont dans un fichier
Six commandes câblées dans l'éditeur auraient répondu à la demande. Elles auraient aussi été fausses en une semaine.
Chaque commande du fichier de départ passe par node, npm ou npx, et aucune n'a donc besoin de quoi que ce soit d'installé au-delà de Node lui-même. C'est un défaut défendable et ce n'est la réponse universelle de personne. Un projet sur pnpm veut pnpm install et pnpm test, un projet sur Bun veut bun run, un projet sur Deno ni l'un ni l'autre. Un projet à un seul point d'entrée veut node server.js sans qu'on lui demande quel script. Un projet qui teste avec Vitest veut npx vitest run et jamais node --test. Un projet qui tourne sous Docker veut docker compose up. Rien de cela n'est connaissable d'ici, et tout cela est une ligne dans un fichier.
Les six sont donc des défauts, pas du code : c'est le contenu du fichier de départ que JavaScript ▸ Create tools file écrit, et en changer une revient à éditer un fichier plutôt qu'à reconstruire un éditeur. Le fichier est lu à chaque ouverture du menu, pour la même raison que le menu Snippets : une modification doit prendre effet aussitôt, et le fichier est souvent ouvert dans la fenêtre derrière le menu.
Les commandes vont à sh -c — cmd.exe /S /C sous Windows — plutôt que d'être découpées en argv ici. Le fichier appartient à l'utilisateur, les tubes, les globs et && sont donc des fonctionnalités plutôt que des dangers, et une entrée peut être npx eslint . && node --test. Découper un argv reviendrait à inventer des règles de citation pour une chaîne que quelqu'un a écrite à la main.
Pourquoi il n'y a pas de fichier d'outils au niveau utilisateur
Les snippets sont lus dans deux fichiers — le vôtre et celui du projet — parce que vos snippets sont vos habitudes et doivent vous suivre d'un projet à l'autre.
Les outils ne sont pas ainsi. Ils appartiennent à l'outillage propre d'un projet : un fichier d'outils global proposerait npm install dans un dépôt qui n'a jamais entendu parler de Node, et un projet sur pnpm aurait npm start dans son menu à côté de ses propres commandes. Le fichier est par projet, et c'est toute la règle.
Pourquoi un outil peut nommer son propre menu
Un menu appelé JavaScript qui contient docker compose up est un mensonge sur ce qu'est le menu. Le premier fichier d'outils qu'on écrit dépasse JavaScript, parce que les commandes d'un projet ne portent pas toutes sur le langage dans lequel il est écrit : conteneurs, bases de données, déploiements, une cible de Makefile ajoutée en 2019.
Deux formes ont été envisagées. Un second menu fixe appelé Tools — tout ce qui est JavaScript dans JavaScript, tout le reste dans Tools — c'est une clé dans le format et aucun problème de nommage, mais cela ne fait que déplacer le mensonge : un menu Tools contenant docker compose up, psql et un script de déploiement est tout aussi indifférencié, et dès qu'il y a dix entrées personne n'en retrouve une. Et un second fichier, menus.toml, garde le fichier d'outils simple au prix de deux fichiers qui doivent s'accorder sur les outils existants.
Le menu est donc un nom libre sur l'outil, dans le seul fichier : menu = "Docker". Un nom que rien d'autre n'utilise crée le menu ; omettre la clé signifie JavaScript. Il n'y a pas de liste de noms autorisés, parce qu'une liste serait la liste des projets de quelqu'un d'autre.
JavaScript lui-même reste fixe sur la barre plutôt que de devenir un nom de plus venu du fichier. JavaScript ▸ Create tools file doit être atteignable dans un projet qui n'a aucun fichier d'outils — c'est exactement le projet qui en a besoin — et un menu qui n'existe qu'une fois le fichier là ne peut pas proposer de l'écrire.
Pourquoi la touche chaude n'est pas au fichier de la choisir
L'auteur d'un fichier d'outils ne peut pas savoir quelles lettres sont libres. Il voit File, Edit, Search, Run, Code, Options, Window, Snippets, Agent, JavaScript et Help sur la barre, mais seulement en comptant les soulignés, et un projet partagé entre plusieurs personnes dépendrait alors de ce que personne n'ajoute un menu qui entre en collision.
Les collisions ici sont silencieuses, et c'est ce qui rend la conception nécessaire. La barre répond au premier menu dont la touche correspond ; un second menu revendiquant la même lettre n'est pas une erreur et se dessine normalement — il ne s'ouvre simplement jamais. Ce piège s'est déjà refermé une fois dans cette famille : Snippets et Search voulaient tous deux S, Snippets était l'inatteignable, et tous les tests passaient. La correction fut alors de déplacer Snippets sur N à la main. Laisser un fichier nommer des menus en fait un danger permanent plutôt qu'une erreur ponctuelle, l'attribution est donc faite par l'éditeur : la première lettre du nom que rien d'autre ne revendique.
Les tildes écrits dans le nom sont honorés quand la lettre est libre, et remplacés sans bruit quand elle ne l'est pas. Refuser le fichier était l'alternative, et c'est pire : la collision dépend des menus existants, un fichier d'outils qui marchait casserait donc le jour où une version de l'éditeur ajoute un menu — comme l'a fait celle où le menu Agent a pris A. Entre un menu sur une lettre que vous n'avez pas demandée et un menu que vous ne pouvez pas ouvrir, le premier est la moindre perte.
Quand toutes les lettres d'un nom sont prises, le menu n'a aucune touche chaude. F10, les flèches et la souris l'atteignent toujours, et l'alternative — prendre une lettre qui n'est pas dans le nom — mettrait un souligné sous rien.
Pourquoi la barre est reconstruite à partir d'un stat
Menu.OnOpen regarnit les entrées d'un menu juste avant qu'il se déroule, et c'est ainsi que les menus JavaScript et Snippets suivent leurs fichiers sans redémarrage. Cela ne peut pas aider ici : l'ensemble des menus fait partie de la barre, pas d'un menu en particulier, et ajouter menu = "Docker" au fichier doit mettre Docker sur la barre.
Lire et parser le fichier à chaque tour de la boucle d'événements le ferait, et ce serait aussi du travail pour rien à chaque frappe dans un fichier que personne n'a modifié. La barre porte donc la taille et la date de modification du fichier d'outils à partir duquel elle a été construite, et un stat par tour décide s'il faut la reconstruire. Éditer le fichier dans la fenêtre devant soi, l'enregistrer, et voir la barre changer, c'est le cas pour lequel c'est fait.
Pourquoi les fichiers ouverts sont relus, et seulement certains
npx prettier --write . réécrit le fichier devant vous. npm install écrit node_modules/ et package-lock.json dans le répertoire. npx eslint --fix . réécrit tout ce qu'il peut. Sans rien de plus, l'éditeur resterait sur une copie périmée d'un fichier qu'une autre commande a changé, et le F2 suivant écrirait votre copie par-dessus le travail de la commande — défaisant le formatage que vous venez de demander.
Quand une commande se termine, l'éditeur relit donc tous les fichiers ouverts. La partie intéressante est ceux qu'il refuse de toucher.
Un fichier avec des modifications non enregistrées est laissé tranquille, et la barre d'état dit combien ont été ignorés. Le recharger jetterait un travail que l'utilisateur n'a pas enregistré, ce qu'aucune commodité ne justifie. Et le conflit est réel : la commande et la modification non enregistrée ne sont pas d'accord sur ce que le fichier doit dire, et l'éditeur n'est pas en position de trancher. Le nommer et s'arrêter est l'issue honnête.
Deux décisions plus petites à l'intérieur :
- Le curseur reste où il était, ramené dans ce que le fichier contient désormais.
- L'historique d'annulation est jeté. Annuler au-delà d'un rechargement restaurerait un texte que le fichier n'a plus, ce qui est pire que ne pas pouvoir annuler du tout.
Pourquoi le rechargement se fait sur la boucle d'événements
La fin de la commande est remarquée sur la goroutine qui lit le terminal, laquelle n'a pas le droit de toucher un tampon ou le bureau. Elle pose donc un drapeau, et le rechargement s'exécute au début du tour suivant de la boucle d'événements.
C'est la quatrième chose dans la bibliothèque construite ainsi — l'annonce du serveur de langage, les redessins du terminal, l'échéance de l'enregistrement automatique, et maintenant ceci. La règle qu'elles partagent vaut d'être redite : le réveil peut se perdre, l'état ne doit pas se perdre. PostEvent laisse tomber ce qui n'entre pas dans sa file, tout ce qui dépend de l'arrivée d'un message est donc un bogue qui attend un moment d'affluence. Un drapeau que la boucle vérifie elle-même ne peut pas manquer.
Pourquoi une commande peut demander une valeur, et pourquoi entre doubles accolades
node a besoin d'un script. Un projet Node n'a pas de point d'entrée unique que l'éditeur pourrait connaître : package.json peut nommer un main, un bin, un script start ou aucun des trois, et le fichier en cours d'édition est aussi souvent un module qu'un programme. Un outil qui ne peut pas demander est un outil qu'il faut éditer avant chaque usage, ce qui n'est pas un outil.
Un {{libellé}} dans une commande est donc une valeur que l'éditeur demande d'abord, dans une boîte titrée du nom de l'outil. Run l'utilise, et c'est délibéré : une fonctionnalité démontrée dans le fichier que tout le monde reçoit est une fonctionnalité que les gens trouvent, et une décrite seulement dans un commentaire ne l'est pas. Les cinq autres n'ont besoin d'aucune valeur, parce que npm et npx lisent package.json et savent quoi faire — ce qui est la différence entre un langage avec un manifeste et un langage sans, et la raison pour laquelle le fichier de départ de Turbo Golo demande six fois là où celui-ci demande une fois.
Les accolades simples étaient l'orthographe évidente et elles sont fausses. awk '{print $1}' et find . -exec rm {} + sont des choses ordinaires à mettre dans un fichier d'outils, et lire la première comme un champ transforme une commande qui marche en une boîte demandant « print $1 ». Les doubles accolades n'entrent en collision avec presque rien — et avec rien dans une commande shell, où le ${…} propre à JavaScript n'apparaît jamais.
La valeur est citée par défaut, parce que l'alternative échoue en silence. Un chemin avec un espace, substitué brut, devient deux arguments et la commande se plaint d'un fichier qui n'existe pas. Citer fait marcher ce cas et rend l'autre — « mets ces trois options à la fin » — impossible, si bien que ... dans les accolades demande la valeur telle quelle. Deux comportements, tous deux documentés, plutôt qu'un seul qui a tort la moitié du temps.
Rien n'est mémorisé sur le disque. La boîte part de ce qui a été tapé la fois précédente, pour la session. L'écrire dans le répertoire propre du projet a été envisagé et rejeté : ce répertoire contient ce que le projet a décidé, et le script que quelqu'un a lancé en traquant un bogue n'en fait pas partie.
Un fichier qui ne peut pas être parsé est refusé à la lecture, pas quand l'outil est choisi. Un {{ non fermé qui atteint le shell est une commande qui échoue avec des accolades dedans, ce qui ne nomme ni l'outil ni le fichier ; refuser au chargement nomme les deux. C'est la même règle que suit déjà une valeur d'output inconnue.
Le dialogue est refusé quand il ne tient pas. Un outil qui demande plus de valeurs que le terminal n'a de lignes donnerait une boîte dont le bouton OK est sous le bas de l'écran — à laquelle on ne peut répondre que par Échap, qui annule. Dire « ceci demande douze valeurs et neuf tiennent » n'est pire que rien que si vous préférez le découvrir en essayant.
Liens avec le reste
- Chaque clé du fichier et chaque règle : Référence des outils JavaScript
- S'en servir : Lancer des commandes Node depuis l'éditeur
- Les fenêtres qu'utilise
output = "terminal", et pourquoi ce sont de vrais terminaux : Fenêtres de terminal - L'autre menu construit à partir d'un fichier : Snippets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
|