owned this note
owned this note
Published
Linked with GitHub
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:
```javascript=
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:
```javascript=
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:
```javascript=
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?
<details>
<summary>Input files:</summary>
```javascript=
// 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);
```
```javascript=
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!"
```
</details>
<details>
<summary>Decorators definition file (<code>pretty-decorator/index.decorators.json</code>):</summary>
```json=
{
"pretty": {
"binding": "__babel__decorator__pretty"
"decorators": [ "register", "wrap" ]
}
}
```
</details>
<details>
<summary>Output files:</summary>
```javascript=
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);
```
```javascript=
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!"
```
</details>
<br/>
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.