À propos de la consommation (et publication) de packages ES2015+
Cette page a été traduite par PageTurner AI (bêta). Non approuvée officiellement par le projet. Vous avez trouvé une erreur ? Signaler un problème →
Pour ceux d'entre nous qui doivent prendre en charge d'anciens navigateurs, nous utilisons un compilateur comme Babel sur notre code applicatif. Mais ce n'est pas la seule partie du code que nous envoyons aux navigateurs ; il y a aussi le code dans notre node_modules.
Pouvons-nous rendre la compilation de nos dépendances non seulement possible, mais normale ?
La capacité à compiler les dépendances est une fonctionnalité clé pour tout l'écosystème. En commençant par certains changements apportés dans Babel v7 pour permettre une compilation sélective des dépendances, nous espérons voir cette pratique se standardiser à l'avenir.
Hypothèses
-
Nous livrons à des navigateurs modernes qui prennent nativement en charge ES2015+ (sans avoir à supporter IE) ou qui peuvent recevoir plusieurs types de bundles (par exemple en utilisant
<script type="module">et<script nomodule>). -
Nos dépendances publient réellement du code ES2015+ au lieu du niveau de référence actuel ES5/ES3.
-
Le niveau de référence futur ne devrait pas être fixé à ES2015, mais constitue une cible mouvante.
Pourquoi
Pourquoi est-il souhaitable de compiler les dépendances (par opposition à compiler uniquement notre propre code) ?
-
Pour avoir la liberté d'arbitrer où le code peut s'exécuter (contrairement aux bibliothèques).
-
Pour envoyer moins de code aux utilisateurs, puisque JavaScript a un coût.
L'éphémère runtime JavaScript
L'argument expliquant pourquoi la compilation des dépendances serait utile est le même que celui qui a conduit Babel à introduire @babel/preset-env à terme. Nous avons constaté que les développeurs finiraient par vouloir dépasser la compilation exclusive vers ES5.
Babel s'appelait à l'origine 6to5, car il ne convertissait que d'ES2015 (appelé ES6 à l'époque) vers ES5. À cette époque, la prise en charge d'ES2015 par les navigateurs était quasi inexistante, donc l'idée d'un compilateur JavaScript était à la fois novatrice et utile : nous pouvions écrire du code moderne qui fonctionnait pour tous nos utilisateurs.
Mais qu'en est-il des runtimes des navigateurs eux-mêmes ? Comme les navigateurs evergreen finiront par rattraper le standard (comme ils l'ont fait avec ES2015), la création de preset-env aide Babel et la communauté à s'aligner à la fois sur les navigateurs et sur TC39. Si nous ne compilions qu'en ES5, personne n'exécuterait jamais de code natif dans les navigateurs.
La vraie différence réside dans la prise de conscience qu'il existera toujours une fenêtre glissante de support :
-
Code applicatif (nos environnements supportés)
-
Navigateurs (Chrome, Firefox, Edge, Safari)
-
Babel (la couche d'abstraction)
-
Propositions TC39/ECMAScript (et implémentations Babel)
Ainsi, le besoin n'est pas seulement de renommer 6to5 en Babel parce qu'il compile en 7to5, mais que Babel change l'hypothèse implicite selon laquelle il ne cible qu'ES5. Avec @babel/preset-env, nous pouvons écrire le JavaScript le plus récent et cibler n'importe quel navigateur/environnement !
L'utilisation de Babel et preset-env nous aide à suivre cette fenêtre glissante en constante évolution. Cependant, même si nous l'utilisons, cela ne concerne actuellement que notre code applicatif, et non les dépendances de notre code.
À qui appartiennent nos dépendances ?
Comme nous contrôlons notre propre code, nous pouvons tirer parti de preset-env : à la fois en écrivant en ES2015+ et en ciblant les navigateurs prenant en charge ES2015+.
Ce n'est pas forcément le cas pour nos dépendances ; pour obtenir les mêmes avantages qu'avec la compilation de notre code, nous devrons peut-être apporter des modifications.
Est-ce aussi simple que d'exécuter Babel sur node_modules ?
Complexités actuelles de la compilation des dépendances
Complexité du compilateur
Bien que cela ne doive pas nous dissuader de rendre cela possible, nous devons être conscients que la compilation des dépendances accroît la surface d'exposition aux problèmes et la complexité, en particulier pour Babel lui-même.
-
Les compilateurs ne sont pas différents des autres programmes et peuvent contenir des bogues.
-
Toutes les dépendances n'ont pas besoin d'être compilées, et compiler plus de fichiers signifie effectivement une construction plus lente.
-
preset-envlui-même pourrait contenir des bogues car nous utilisonscompat-tablepour nos données, par opposition à Test262 (la suite de tests officielle). -
Les navigateurs eux-mêmes peuvent avoir des problèmes pour exécuter du code ES2015+ natif par rapport à ES5.
-
La question de déterminer ce qui est « pris en charge » demeure : voir babel/babel-preset-env#54 pour un exemple de cas limite. Passe-t-il le test simplement parce qu'il est analysable ou a un support partiel ?
Problèmes spécifiques dans Babel v6
Exécuter un script en tant que module peut provoquer une SyntaxError, de nouvelles erreurs d'exécution ou un comportement inattendu en raison des différences sémantiques entre les scripts classiques et les modules.
Babel v6 considérait chaque fichier comme un module et donc en « mode strict ».
On pourrait argumenter que c'est en fait une bonne chose, car tous ceux qui utilisent Babel optent par défaut pour le mode strict 🙂.
Exécuter Babel avec une configuration conventionnelle sur l'ensemble de nos node_modules peut causer des problèmes avec du code qui est un script, comme un plugin jQuery.
Un exemple de problème est la façon dont this est converti en undefined.
// Input
(function($) {
// …
}(this.jQuery));
// Output
"use strict";
(function ($) {
// …
})(undefined.jQuery);
Cela a été modifié dans la v7 pour qu'il n'injecte plus automatiquement la directive "use strict" sauf si le fichier source est un module.
Compiler les dépendances ne faisait pas non plus partie du périmètre initial de Babel : nous avons en effet reçu des rapports de problèmes indiquant que les gens le faisaient accidentellement, ce qui ralentissait la construction. Il existe de nombreux paramètres par défaut et une documentation dans l'outillage qui désactivent intentionnellement la compilation de node_modules.
Utilisation d'une syntaxe non standard
Il existe de nombreux problèmes liés à la livraison d'une syntaxe de proposition non compilée (cet article a été inspiré par l'inquiétude de Dan à ce sujet).
Processus de mise en œuvre (staging)
Le processus de mise en œuvre de TC39 n'avance pas toujours : une proposition peut évoluer à n'importe quel stade du processus : revenir en arrière, comme de l'Étape 3 à l'Étape 2, comme ce fut le cas pour les séparateurs numériques (1_000), être abandonnée complètement (Object.observe(), et d'autres que nous avons peut-être oubliées 😁), ou simplement stagner comme la liaison de fonction (a::b) ou les décorateurs jusqu'à récemment.
- Résumé des Stades : Le Stade 0 n'a aucun critère et signifie que la proposition est juste une idée, le Stade 1 reconnaît que le problème mérite d'être résolu, le Stade 2 décrit une solution dans un texte de spécification, le Stade 3 signifie que la solution spécifique est pensée en détail, et le Stade 4 signifie qu'elle est prête à être incluse dans la spécification avec des tests, plusieurs implémentations navigateurs et une expérience terrain.
Utilisation des Propositions
— Rach Smith 🌈 (@rachsmithtweets) August 1, 2017
Nous recommandons déjà aux développeurs d'être prudents lorsqu'ils utilisent des propositions inférieures au Stade 3, et encore plus lorsqu'ils les publient.
Mais simplement dire aux gens de ne pas utiliser le Stade X va à l'encontre de l'objectif même de Babel. Une raison majeure pour laquelle les propositions s'améliorent et progressent est le retour d'expérience que le comité reçoit des usages réels (en production ou non) basés sur leur utilisation via Babel.
Il y a certainement un équilibre à trouver : nous ne voulons pas décourager l'utilisation de nouvelles syntaxes (ce qui est difficile 😂), mais nous ne voulons pas non plus que les gens pensent que « une fois dans Babel, la syntaxe est officielle ou immuable ». Idéalement, les gens devraient examiner le but d'une proposition et faire des compromis selon leur cas d'usage.
Suppression des Presets de Stade dans la v7
Même si l'une des pratiques les plus courantes est d'utiliser le preset Stage 0, nous prévoyons de supprimer les presets de stade dans la v7. Nous pensions initialement que ce serait pratique, que les gens créeraient leurs propres presets non officiels de toute façon, ou que cela aiderait à lutter contre la « fatigue JavaScript ». Cela semble créer plus de problèmes : les gens continuent de copier/coller des configurations sans comprendre ce qui compose un preset.
Après tout, voir "stage-0" ne signifie rien. J'espère qu'en rendant explicite la décision d'utiliser des plugins de proposition, les gens devront apprendre quelle syntaxe non standard ils adoptent. Plus intentionnellement, cela devrait mener à une meilleure compréhension non seulement de Babel mais aussi de JavaScript comme langage et de son développement, au-delà de son simple usage.
Publication de Syntaxe Non Standard
En tant qu'auteur de bibliothèque, publier une syntaxe non standard expose nos utilisateurs à d'éventuelles incohérences, refontes et ruptures de leurs projets. Parce qu'une proposition TC39 (même au Stade 3) peut encore changer, cela signifie que nous devrons inévitablement modifier le code de la bibliothèque. Une proposition « nouvelle » ne signifie pas que l'idée est fixée ou certaine, mais plutôt que nous voulons collectivement explorer l'espace des solutions.
Au moins si nous livrons la version compilée, elle continuera de fonctionner, et le mainteneur de la bibliothèque pourra modifier la sortie pour qu'elle compile du code fonctionnant comme avant. Livrer la version non compilée signifie que quiconque consomme un package doit avoir une étape de build pour l'utiliser et doit avoir la même configuration Babel que nous. C'est dans la même catégorie que l'utilisation de TS/JSX/Flow : nous n'attendrions pas des consommateurs qu'ils configurent le même environnement de compilation simplement parce que nous les avons utilisés.
Confusion entre Modules JavaScript et ES2015+
Lorsque nous écrivons import foo from "foo" ou require("foo") et que foo n'a pas d'index.js, cela résout le champ main dans le package.json du module.
Certains outils comme Rollup/webpack lisent également un autre champ appelé module (auparavant jsnext:main). Ils l'utilisent pour résoudre le fichier Module JavaScript.
- Un exemple avec
redux
// redux package.json
{
...
"main": "lib/redux.js", // ES5 + Common JS
"module": "es/redux.js", // ES5 + JS Modules
}
Cela a été introduit pour que les utilisateurs puissent consommer des Modules JavaScript (ESM).
Cependant, la seule intention de ce champ est l'ESM, rien d'autre. La documentation Rollup précise que le champ module indique clairement qu'il n'est pas destiné à la syntaxe JavaScript future.
Malgré cet avertissement, les auteurs de packages confondent invariablement l'utilisation des modules ES avec le niveau de langage JavaScript dans lequel ils l'ont écrit.
Par conséquent, nous pourrions avoir besoin d'une autre méthode pour indiquer le niveau du langage.
Solutions non évolutives ?
Une suggestion courante est que les bibliothèques publient ES2015 sous un autre champ comme es2015, par exemple "es2015": "es2015/package.mjs".
// @angular/core package.json
{
"main": "./bundles/core.umd.js",
"module": "./fesm5/core.js",
"es2015": "./fesm2015/core.js",
"esm5": "./esm5/core.js",
"esm2015": "./esm2015/core.js",
"fesm5": "./fesm5/core.js",
"fesm2015": "./fesm2015/core.js",
}
Cela fonctionne pour ES2015, mais cela soulève la question de savoir quoi faire pour ES2016 ? Devons-nous créer un nouveau dossier chaque année et un nouveau champ dans package.json ? Cela semble insoutenable et continuera à produire des node_modules plus volumineux.
C'était un problème avec Babel lui-même : nous avions prévu de continuer à publier des préréglages annuels (
preset-es2015,preset-es2016...) jusqu'à ce que nous réalisions quepreset-envrendrait cela inutile.
La publication basée sur des environnements/syntaxes spécifiques semblerait tout aussi intenable car le nombre de combinaisons ne cesse d'augmenter ("ie-11-arrow-functions").
Qu'en est-il de distribuer uniquement le code source lui-même ? Cela pourrait poser des problèmes similaires si nous utilisions une syntaxe non standard comme mentionné précédemment.
Un champ esnext pourrait ne pas être entièrement utile non plus. La version "la plus récente" de JavaScript varie selon l'époque où nous avons écrit le code.
Les dépendances pourraient ne pas publier ES2015+
Cet effort ne deviendra standard que s'il devient simple à appliquer en tant qu'auteur de bibliothèque. Il sera difficile de défendre l'importance de ce changement si les bibliothèques nouvelles et populaires ne peuvent pas utiliser la syntaxe la plus récente.
En raison de la complexité et de la configuration des outils, il peut être difficile pour les projets de publier en ES2015+/ESM. C'est probablement le principal défi à relever, et ajouter plus de documentation ne suffit tout simplement pas.
Pour Babel, nous pourrions devoir ajouter des demandes de fonctionnalités à @babel/cli pour faciliter cela, et peut-être faire en sorte que le paquet babel le fasse par défaut ? Ou nous devrions mieux nous intégrer avec des outils comme microbundle de @developit.
Et comment gérons-nous les polyfills (ce sera l'objet d'un prochain article) ? À quoi ressemblerait le fait pour un auteur de bibliothèque (ou l'utilisateur) de ne pas avoir à penser aux polyfills ?
Cela étant dit, comment Babel aide-t-il dans tout cela ?
Comment Babel v7 aide
Comme nous l'avons évoqué, compiler les dépendances dans Babel v6 peut être assez pénible. Babel v7 résoudra certains de ces points douloureux.
Un problème concerne la recherche de configuration. Babel s'exécute actuellement par fichier, donc lors de la compilation, il tente de trouver la configuration la plus proche (.babelrc). Il continue à remonter l'arborescence des répertoires s'il ne la trouve pas dans le dossier actuel.
project
└── .babelrc // closest config for a.js
└── a.js
└── node_modules
└── package
└── .babelrc // closest config for b.js
└── b.js
Nous avons apporté quelques modifications :
-
L'une consiste à arrêter la recherche à la limite du paquet (s'arrêter lorsqu'on trouve un
package.json). Cela garantit que Babel ne tentera pas de charger un fichier de configuration hors de l'application, le cas le plus surprenant étant lorsqu'il en trouve un dans le répertoire personnel. -
Si nous utilisons un monorepo, nous pourrions vouloir avoir un
.babelrcpar paquet qui étend une configuration centrale. -
Babel lui-même est un monorepo, donc nous utilisons plutôt le nouveau
babel.config.jsqui nous permet de résoudre tous les fichiers via cette configuration (plus de recherche).
Compilation sélective avec "overrides"
Nous avons ajouté une option "overrides" qui nous permet de créer une nouvelle configuration pour n'importe quel ensemble de chemins de fichiers.
Cela permet à chaque objet de configuration de spécifier un champ
test/include/exclude, comme vous pourriez le faire avec Webpack. Chaque élément accepte un item ou un tableau d'items pouvant être unestring, unRegExpou unefunction.
Cela nous permet d'avoir une configuration unique pour toute notre application : nous pourrions vouloir compiler notre code JavaScript serveur différemment du code client (ainsi que compiler certains paquets dans node_modules).
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
targets: { node: 'current' },
}],
],
overrides: [{
test: ["./client-code", "./node_modules/package-a"],
presets: [
['@babel/preset-env', {
targets: { "chrome": "60" } },
}],
],
}],
}
Recommandations à discuter
Nous devons abandonner notre vision figée de la publication JavaScript pour adopter une approche évolutive avec les standards récents.
Nous devrions continuer à publier ES5/CJS sous main pour la compatibilité ascendante avec les outils actuels, tout en publiant une version compilée vers la syntaxe la plus récente (sans propositions expérimentales) sous une nouvelle clé standardisée comme main-es. (Je ne pense pas que module devrait être cette clé car elle était destinée uniquement aux JS Modules).
Peut-être devrions-nous décider d'une autre clé dans
package.json, comme"es"? Cela me rappelle le sondage que j'avais réalisé pour babel-preset-latest.
La compilation des dépendances n'est pas réservée à un projet ou une entreprise : elle nécessite une impulsion de toute la communauté. Bien que cet effort soit naturel, il pourrait nécessiter une standardisation : nous pourrions définir des critères pour que les bibliothèques puissent participer à la publication ES2015+ et vérifier cela via CI/outils/npm lui-même.
La documentation doit être mise à jour pour mentionner les avantages de compiler node_modules, expliquer comment le faire pour les auteurs de bibliothèques, et comment l'utiliser dans les bundlers/compilateurs.
Avec Babel 7, les utilisateurs peuvent exploiter preset-env plus sûrement et activer le traitement des node_modules grâce à de nouvelles options de configuration comme overrides.
Passons à l'action !
Compiler JavaScript ne devrait pas se limiter à la distinction ES2015/ES5, que ce soit pour notre application ou nos dépendances ! J'espère que ceci relancera les conversations pour intégrer pleinement les dépendances publiées en ES2015+.
Cet article explore comment Babel devrait soutenir cet effort, mais nous aurons besoin de l'aide de tous pour changer l'écosystème : plus de sensibilisation, plus de paquets opt-in publiés, et de meilleurs outils.
Merci aux nombreuses personnes ayant relu cet article, notamment @chrisdarroch, @existentialism, @mathias, @betaorbust, @_developit, @jdalton, @bonsaistudio.