Les décorateurs du standard TC39 dans Babel
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 →
Babel 7.1.0 prend enfin en charge la nouvelle proposition de décorateurs : vous pouvez l'essayer avec le plugin @babel/plugin-proposal-decorators 🎉.
Un peu d'histoire
Les décorateurs ont été proposés initialement par Yehuda Katz il y a plus de trois ans. TypeScript a ajouté leur support dans la version 1.5 (2015) parallèlement à de nombreuses fonctionnalités ES6. Des frameworks majeurs comme Angular et MobX ont commencé à les utiliser pour améliorer l'expérience développeur : cela a popularisé les décorateurs et donné à la communauté un faux sentiment de stabilité.
Babel a initialement implémenté les décorateurs dans la version 5, mais les a retirés dans Babel 6 car la proposition évoluait encore. Logan Smyth a créé un plugin non officiel (babel-plugin-transform-decorators-legacy) qui reproduisait le comportement de Babel 5 ; il a ensuite été intégré au dépôt officiel de Babel lors de la première alpha de Babel 7. Ce plugin utilisait toujours l'ancienne sémantique des décorateurs, car la nouvelle proposition n'était pas encore définie.
Depuis lors, Daniel Ehrenberg et Brian Terlson sont devenus co-auteurs de la proposition avec Yehuda Katz, et elle a été presque entièrement réécrite. Tout n'est pas encore décidé, et aucune implémentation conforme n'existe à ce jour.
Babel 7.0.0 a introduit un nouveau flag dans le plugin @babel/plugin-proposal-decorators : l'option legacy, dont la seule valeur valide était true. Ce changement cassant était nécessaire pour assurer une transition fluide de la version Stage 1 de la proposition vers la version actuelle.
Dans Babel 7.1.0, nous introduisons le support de cette nouvelle proposition, activé par défaut avec le plugin @babel/plugin-proposal-decorators. Sans l'option legacy: true dans Babel 7.0.0, il aurait été impossible d'utiliser la sémantique correcte par défaut (équivalente à legacy: false).
La nouvelle proposition prend également en charge les décorateurs sur les champs et méthodes privés. Nous n'avons pas encore implémenté cette fonctionnalité dans Babel (pour chaque classe, vous pouvez utiliser soit les décorateurs soit les éléments privés), mais elle arrivera très prochainement.
Qu'est-ce qui a changé dans la nouvelle proposition ?
Bien que la nouvelle proposition semble très similaire à l'ancienne, plusieurs différences importantes les rendent incompatibles.
Syntaxe
L'ancienne proposition autorisait toute expression valide côté gauche (littéraux, expressions de fonctions et de classes, expressions new et appels de fonctions, accès aux propriétés simples et calculés) comme corps d'un décorateur. Par exemple, ce code était valide :
class MyClass {
@getDecorators().methods[name]
foo() {}
@decorator
[bar]() {}
}
Cette syntaxe posait problème : la notation [...] servait à la fois d'accès aux propriétés dans le corps du décorateur et à définir des noms calculés. Pour éviter cette ambiguïté, la nouvelle proposition n'autorise que l'accès aux propriétés par point (foo.bar), éventuellement avec des arguments à la fin (foo.bar()). Si vous avez besoin d'expressions plus complexes, vous pouvez les encapsuler dans des parenthèses :
class MyClass {
@decorator
@dec(arg1, arg2)
@namespace.decorator
@(complex ? dec1 : dec2)
method() {}
}
Décorateurs d'objets
L'ancienne version de la proposition permettait, en plus des décorateurs de classe et d'éléments de classe, des décorateurs sur les membres d'objet :
const myObj = {
@dec1 foo: 3,
@dec2 bar() {},
};
En raison d'incompatibilités avec la sémantique actuelle des littéraux objets, ils ont été retirés de la proposition. Si vous les utilisez dans votre code, restez à l'écoute car ils pourraient être réintroduits dans une proposition ultérieure (tc39/proposal-decorators#119).
Arguments des fonctions décoratrices
Le troisième changement important introduit par la nouvelle proposition concerne les arguments passés aux fonctions décoratrices.
Dans la première version de la proposition, les décorateurs d'éléments de classe recevaient une classe cible (ou un objet), une clé et un descripteur de propriété - similaire à ce que vous passeriez à Object.defineProperty. Les décorateurs de classe prenaient comme seul argument un constructeur cible.
La nouvelle proposition de décorateurs est bien plus puissante : les décorateurs d'éléments prennent un objet qui, au-delà de modifier le descripteur de propriété, permet de changer la clé, le placement (static, prototype ou own) et le type (field ou method) de l'élément. Ils peuvent aussi créer des propriétés supplémentaires et définir une fonction (un finisher) qui s'exécute sur la classe décorée.
Les décorateurs de classe prennent un objet contenant les descripteurs de chaque élément de classe, permettant de les modifier avant la création de la classe.
Mise à niveau
Compte tenu de ces incompatibilités, il est impossible d'utiliser les décorateurs existants avec la nouvelle proposition : cela rendrait la migration très lente, car les bibliothèques existantes (MobX, Angular, etc.) ne peuvent pas être mises à niveau sans introduire de changements cassants.
Pour contourner ce problème, nous avons publié un package utilitaire qui encapsule les décorateurs dans votre code. Après l'avoir exécuté,
vous pouvez modifier en toute sécurité votre configuration Babel pour utiliser la nouvelle proposition 🎉.
Vous pouvez mettre à jour vos fichiers avec une seule ligne de commande :
npx wrap-legacy-decorators src/file-with-decorators.js --decorators-before-export --write
Si votre code ne s'exécute que dans Node, ou si vous le bundlez avec Webpack ou Rollup, vous pouvez éviter d'injecter la fonction wrapper dans chaque fichier en utilisant une dépendance externe :
npm install --save decorators-compat
npx wrap-legacy-decorators src/file-with-decorators.js --decorators-before-export --external-helpers --write
Pour plus d'informations, consultez la documentation du package.
Questions ouvertes
Tout n'a pas encore été décidé : les décorateurs sont une fonctionnalité très vaste et leur définition optimale est complexe.
Où placer les décorateurs sur les classes exportées ?
La proposition des décorateurs a hésité sur cette question : les décorateurs doivent-ils venir avant ou après le mot-clé export ?
export @decorator class MyClass {}
// or
@decorator
export class MyClass {}
La question sous-jacente est de savoir si le mot-clé export fait partie de la déclaration de classe ou s'il est un "wrapper". Dans le premier cas, il devrait venir après les décorateurs, puisque ceux-ci se placent au début de la déclaration ; dans le second, il devrait venir avant, car les décorateurs font partie de la déclaration de classe.
Comment permettre aux décorateurs d'interagir de manière sécurisée avec les éléments privés ?
Les décorateurs soulèvent un important problème de sécurité : s'il est possible de décorer des éléments privés, alors les noms privés (qui peuvent être considérés comme les "clés" des éléments privés) pourraient fuiter. Plusieurs niveaux de sécurité sont à considérer :
-
Les décorateurs ne doivent pas divulguer accidentellement des noms privés. Du code malveillant ne devrait pas pouvoir "voler" des noms privés à d'autres décorateurs, de quelque manière que ce soit.
-
Seuls les décorateurs appliqués directement aux éléments privés pourraient être considérés comme fiables : les décorateurs de classe devraient-ils être incapables de lire et écrire les éléments privés ?
-
La confidentialité stricte (l'un des objectifs de la proposition des champs de classe) signifie que les éléments privés devraient être accessibles uniquement depuis la classe : un décorateur doit-il avoir accès aux noms privés ? Doit-il être possible de décorer uniquement les éléments publics ?
Ces questions nécessitent des discussions supplémentaires avant d'être résolues, et c'est là qu'intervient Babel.
Le rôle de Babel
Dans la continuité de l'article What's Happening With the Pipeline (|>) Proposal?, avec la sortie de Babel 7 nous utilisons notre position dans l'écosystème JS pour aider davantage les auteurs des propositions, en permettant aux développeurs de tester et donner leur avis sur les différentes variations des propositions.
Pour cette raison, parallèlement à la mise à jour de @babel/plugin-proposal-decorators, nous avons introduit une nouvelle option : decoratorsBeforeExport, qui permet aux utilisateurs d'essayer à la fois export @decorator class C {} et @decorator export default class.
Nous introduirons également une option pour personnaliser les contraintes de confidentialité des éléments privés décorés. Ces options seront obligatoires jusqu'à ce que TC39 prenne une décision à leur sujet, afin que nous puissions définir le comportement par défaut selon ce que spécifiera la proposition finale.
Si vous utilisez directement notre parseur (@babel/parser, anciennement babylon), vous pouvez déjà utiliser l'option decoratorsBeforeExport dans la version 7.0.0 :
const ast = babylon.parse(code, {
plugins: [
["decorators", { decoratorsBeforeExport: true }]
]
})
Utilisation
Pour l'utilisation dans Babel lui-même :
- npm
- Yarn
- pnpm
- Bun
npm install @babel/plugin-proposal-decorators --save-dev
yarn add @babel/plugin-proposal-decorators --dev
pnpm add @babel/plugin-proposal-decorators --save-dev
bun add @babel/plugin-proposal-decorators --dev
{
"plugins": ["@babel/plugin-proposal-decorators", { "decoratorsBeforeExport": true }]
}
Consultez la documentation de @babel/plugin-proposal-decorators pour plus d'options.
Votre rôle
En tant que développeur JavaScript, vous pouvez contribuer à façonner l'avenir du langage. Vous pouvez tester les différentes sémantiques envisagées pour les décorateurs et faire des retours aux auteurs des propositions. Nous avons besoin de savoir comment vous les utilisez dans des projets réels ! Vous pouvez également découvrir pourquoi certaines décisions de conception ont été prises en lisant les discussions dans les issues et les comptes-rendus de réunions du dépôt de la proposition.
Si vous souhaitez essayer les décorateurs dès maintenant, vous pouvez expérimenter les différentes options de presets dans notre REPL !