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.
We should create a new plugin (decorators-static
?), because the
syntax is different:
@foo.bar.baz
, while the static one only @identifier
const @decorator = @(arg) => @deco1(param) @deco2
import { @decorator } from "module"
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() {}
}
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.
The authors of the static decorators proposal consider them a compile time feature rather than a runtime feature:
@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
.
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?
// 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!"
pretty-decorator/index.decorators.json
):
{
"pretty": {
"binding": "__babel__decorator__pretty"
"decorators": [ "register", "wrap" ]
}
}
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.
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.