Static decorators implementation in Babel

This decorators are meant to be transpiled at compile time, and shouldn't need a runtime helper. This is a problem for Babel because it only works on a per-file basis. For this reason, the implementation will be divided in different phases.

Phase 0: Implement parser support

We should create a new plugin (decorators-static?), because the
syntax is different:

  1. The stage 2 proposal allows @foo.bar.baz, while the static one only @identifier
  2. Decorators are created using a new syntax: const @decorator = @(arg) => @deco1(param) @deco2
  3. Decorator imports are new syntax: import { @decorator } from "module"

Phase 1: Implement compile support only for inline builtin decorators

This first implementation will only support the following syntax:

class Foo { @wrap(wrapper) method() {} }

The const @decorator syntax won't be supported by the transformer yet, but custom decorators could be defined like this:

const log = id => method => function () { console.log(`Method #${id} called.`); return method.apply(this, arguments); }; class C { @wrap(log(1)) method() {} }

Phase 2: Add support for decorator declarations in the same file

This allows rewriting the previous log decorator as follows:

const @log = @(id) => @wrap(method => function () { console.log(`Method #${id} called.`); return method.apply(this, arguments); }); class C { @log(1) method() {} }

This will be effectively handled by inlining it to the phase-1-style decorator.

Phase 3: Add support to define new compile-time decorators

The authors of the static decorators proposal consider them a compile time feature rather than a runtime feature:

  • ECMAScript engines would handle them right after resolving the modules graph, before executing any code. They would probably be applied during the first bytecode generation.
  • Compilers should remove decorators at compile time, without shipping a runtime version of them to the browser.

@littledan proposed to add a new official decorator, @babel7(transform), to declare new compile time only decorators: this can be useful to experiment with new decorators not possible with the @wrap/@register/@initialize builtin decorators and which could then be moved to the spec.
I don't think that we should introduce that babel-specific decorator, but we should add an "hook" to the transform plugins so that other plugins can define their own decorators, maybe similarly to babel-plugin-macros.

Phase 4: Add support for importing decorators from different files

Since Babel doesn't have the "multiple files" concept, we need to be clever here. I don't think that Babel alone can support them.

I plan to define a new file format which "describes" the decorators exported by a JavaScript file. It is a concept similar to index.d.ts or index.js.flow: we would need a index.decorators.json file.
When Babel sees import { @decorator } from "foo", it will try to load "foo"'s .decorators.json file to know how to apply that decorator at compile time. Since we probably don't want Babel to be platform-specific (e.g. using node's resolution algorithm), we should defer the resolution of this .decorators.json file to a function passed as a plugin option.

How does this decorators definition file look?

Input files:
// pretty-decorator/index.js const prettyMethods = new WeakMap(); export const @pretty = @(v) => @register((target, key) => { prettyMethods.add(target[key], v) }) @wrap(method => function () { console.log(`You are ${v * 100}% pretty!`); return method.apply(this, arguments); }); export const getPrettiness = method => prettyMethods.get(method);
import { @pretty, getPrettiness } from "pretty-decorator"; class Cl { @pretty(0.75) static method() {} } const v = getPrettiness(Cl.method); console.log(v); // logs 0.75 Cl.method(); // logs "You are 75% pretty!"
Decorators definition file (pretty-decorator/index.decorators.json):
{ "pretty": { "binding": "__babel__decorator__pretty" "decorators": [ "register", "wrap" ] } }
Output files:
const prettyMethods = new WeakMap(); export const __babel__decorator__pretty = (v) => [ ["register", [ (target, key) => { prettyMethods.add(target[key], v) } ] ], ["wrap", [ method => function () { console.log(`You are ${v * 100}% pretty!`); return method.apply(this, arguments); } ] ], ]); export const getPrettiness = method => prettyMethods.get(method);
import { __babel__decorator__pretty, getPrettiness } from "pretty-decorator"; const __version = __babel__decorator__pretty(0.75); assert(__version[0][0] === "register"); assert(__version[1][0] === "wrap"); class Cl { // These are then transpiled like they would be in the phase-1 implementation @register(...__version[0][1]) @wrap(...__version[1][1]) static method() {} } const v = getPrettiness(Cl.method); console.log(v); // logs 0.75 Cl.method(); // logs "You are 75% pretty!"

We should work with the TypeScript team to use the same .decorators.json file format and the runtime arrays representation, so that libraries compiled with Babel can be used by TypeScript and vice-versa.
This can be specified in the proposal repository.

Possible follow-up enhancements

This proposal makes it required for any file or library which exports decorators to have a *.decorators.json file, otherwise its decorators can't be imported.
In the future it would be nice to implement the concept of "modules graph" in Babel: currently Babel doesn't know that files or multiple modules exists, it's just transform(input: string) => output: string. If we implement it, *.decorators.json files would only be needed at the top-level of a transpiled library, and Babel could figure it out by itself how to import decorators from different files in the same package.
It would also highly improove Babel's TypeScript support, since it would make it possible to know if an imported binding is a type or a variable.
This idea can be discussed and further analyzed later, since the implementation plan written in this document already provides good support for the new decorators proposal.

Select a repo