Skip to content

Commit

Permalink
Merge pull request babel#7734 from nicolo-ribaudo/decorators-legacy-o…
Browse files Browse the repository at this point in the history
…ption

Decorators legacy option
  • Loading branch information
hzoo committed Apr 19, 2018
2 parents 4224412 + 0cd868a commit 339dfdd
Show file tree
Hide file tree
Showing 25 changed files with 447 additions and 320 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"compact": false,
"presets": ["es2015","stage-2"]
"presets": [
"es2015",
["stage-2", { "decoratorsLegacy": true }]
]
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"plugins": ["external-helpers", ["proposal-class-properties", {"loose": true}]],
"presets": ["stage-0", "es2015"]
"presets": [
["stage-0", { "decoratorsLegacy": true }],
"es2015"
]
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"plugins": ["external-helpers", "proposal-class-properties"],
"presets": ["stage-0", "es2015"]
"presets": [
["stage-0", { "decoratorsLegacy": true }],
"es2015"
]
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"presets": ["es2015", "stage-2"]
"presets": [
"es2015",
["stage-2", { "decoratorsLegacy": true }]
]
}
47 changes: 32 additions & 15 deletions packages/babel-plugin-proposal-decorators/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,33 @@ Add the following line to your .babelrc file:
}
```

#### NOTE: Order of Plugins Matters!
### Via CLI

```sh
babel --plugins @babel/plugin-proposal-decorators script.js
```

### Via Node API

```javascript
require("@babel/core").transform("code", {
plugins: ["@babel/plugin-proposal-decorators"]
});
```

## Options

### `legacy`

`boolean`, defaults to `false`.

Use the legacy (stage 1) decorators syntax and behavior.

#### NOTE: Compatibility with `@babel/plugin-proposal-class-properties`

If you are including your plugins manually and using `@babel/plugin-proposal-class-properties`, make sure that `@babel/plugin-proposal-decorators` comes *before* `@babel/plugin-proposal-class-properties`.

Currently, `@babel/plugin-proposal-class-properties` must be used in `loose` mode to support the `@babel/plugin-proposal-decorators`. To use `@babel/plugin-proposal-class-properties` in spec mode with decorators, wait for the next major version of decorators (Stage 2).
When using the `legacy: true` mode, `@babel/plugin-proposal-class-properties` must be used in `loose` mode to support the `@babel/plugin-proposal-decorators`.

Wrong:

Expand All @@ -85,23 +107,18 @@ Right:
{
"plugins": [
"@babel/plugin-proposal-decorators",
["@babel/plugin-proposal-class-properties", { "loose" : true }]
"@babel/plugin-proposal-class-properties"
]
}
```

### Via CLI

```sh
babel --plugins @babel/plugin-proposal-decorators script.js
```

### Via Node API

```javascript
require("@babel/core").transform("code", {
plugins: ["@babel/plugin-proposal-decorators"]
});
```json
{
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose" : true }]
]
}
```

## References
Expand Down
275 changes: 12 additions & 263 deletions packages/babel-plugin-proposal-decorators/src/index.js
Original file line number Diff line number Diff line change
@@ -1,278 +1,27 @@
// Fork of https://github.com/loganfsmyth/babel-plugin-proposal-decorators-legacy

import { declare } from "@babel/helper-plugin-utils";
import syntaxDecorators from "@babel/plugin-syntax-decorators";
import { template, types as t } from "@babel/core";

const buildClassDecorator = template(`
DECORATOR(CLASS_REF = INNER) || CLASS_REF;
`);

const buildClassPrototype = template(`
CLASS_REF.prototype;
`);
import visitor from "./transformer";
import legacyVisitor from "./transformer-legacy";

const buildGetDescriptor = template(`
Object.getOwnPropertyDescriptor(TARGET, PROPERTY);
`);

const buildGetObjectInitializer = template(`
(TEMP = Object.getOwnPropertyDescriptor(TARGET, PROPERTY), (TEMP = TEMP ? TEMP.value : undefined), {
enumerable: true,
configurable: true,
writable: true,
initializer: function(){
return TEMP;
}
})
`);

export default declare(api => {
export default declare((api, options) => {
api.assertVersion(7);

const WARNING_CALLS = new WeakSet();

/**
* If the decorator expressions are non-identifiers, hoist them to before the class so we can be sure
* that they are evaluated in order.
*/
function applyEnsureOrdering(path) {
// TODO: This should probably also hoist computed properties.
const decorators = (path.isClass()
? [path].concat(path.get("body.body"))
: path.get("properties")
).reduce((acc, prop) => acc.concat(prop.node.decorators || []), []);

const identDecorators = decorators.filter(
decorator => !t.isIdentifier(decorator.callee),
);
if (identDecorators.length === 0) return;

return t.sequenceExpression(
identDecorators
.map(decorator => {
const callee = decorator.callee;
const id = (decorator.callee = path.scope.generateDeclaredUidIdentifier(
"dec",
));
return t.assignmentExpression("=", id, callee);
})
.concat([path.node]),
);
}

/**
* Given a class expression with class-level decorators, create a new expression
* with the proper decorated behavior.
*/
function applyClassDecorators(classPath) {
if (!hasClassDecorators(classPath.node)) return;

const decorators = classPath.node.decorators || [];
classPath.node.decorators = null;

const name = classPath.scope.generateDeclaredUidIdentifier("class");

return decorators
.map(dec => dec.callee)
.reverse()
.reduce(function(acc, decorator) {
return buildClassDecorator({
CLASS_REF: t.cloneNode(name),
DECORATOR: t.cloneNode(decorator),
INNER: acc,
}).expression;
}, classPath.node);
}

function hasClassDecorators(classNode) {
return !!(classNode.decorators && classNode.decorators.length);
}

/**
* Given a class expression with method-level decorators, create a new expression
* with the proper decorated behavior.
*/
function applyMethodDecorators(path, state) {
if (!hasMethodDecorators(path.node.body.body)) return;

return applyTargetDecorators(path, state, path.node.body.body);
}

function hasMethodDecorators(body) {
return body.some(node => node.decorators && node.decorators.length);
const { legacy = false } = options;
if (typeof legacy !== "boolean") {
throw new Error("'legacy' must be a boolean.");
}

/**
* Given an object expression with property decorators, create a new expression
* with the proper decorated behavior.
*/
function applyObjectDecorators(path, state) {
if (!hasMethodDecorators(path.node.properties)) return;

return applyTargetDecorators(path, state, path.node.properties);
}

/**
* A helper to pull out property decorators into a sequence expression.
*/
function applyTargetDecorators(path, state, decoratedProps) {
const name = path.scope.generateDeclaredUidIdentifier(
path.isClass() ? "class" : "obj",
if (legacy !== true) {
throw new Error(
"The new decorators proposal is not supported yet." +
' You muse pass the `"legacy": true` option to' +
" @babel/plugin-proposal-decorators",
);

const exprs = decoratedProps.reduce(function(acc, node) {
const decorators = node.decorators || [];
node.decorators = null;

if (decorators.length === 0) return acc;

if (node.computed) {
throw path.buildCodeFrameError(
"Computed method/property decorators are not yet supported.",
);
}

const property = t.isLiteral(node.key)
? node.key
: t.stringLiteral(node.key.name);

const target =
path.isClass() && !node.static
? buildClassPrototype({
CLASS_REF: name,
}).expression
: name;

if (t.isClassProperty(node, { static: false })) {
const descriptor = path.scope.generateDeclaredUidIdentifier(
"descriptor",
);

const initializer = node.value
? t.functionExpression(
null,
[],
t.blockStatement([t.returnStatement(node.value)]),
)
: t.nullLiteral();

node.value = t.callExpression(
state.addHelper("initializerWarningHelper"),
[descriptor, t.thisExpression()],
);

WARNING_CALLS.add(node.value);

acc = acc.concat([
t.assignmentExpression(
"=",
descriptor,
t.callExpression(state.addHelper("applyDecoratedDescriptor"), [
t.cloneNode(target),
t.cloneNode(property),
t.arrayExpression(decorators.map(dec => t.cloneNode(dec.callee))),
t.objectExpression([
t.objectProperty(
t.identifier("enumerable"),
t.booleanLiteral(true),
),
t.objectProperty(t.identifier("initializer"), initializer),
]),
]),
),
]);
} else {
acc = acc.concat(
t.callExpression(state.addHelper("applyDecoratedDescriptor"), [
t.cloneNode(target),
t.cloneNode(property),
t.arrayExpression(decorators.map(dec => t.cloneNode(dec.callee))),
t.isObjectProperty(node) ||
t.isClassProperty(node, { static: true })
? buildGetObjectInitializer({
TEMP: path.scope.generateDeclaredUidIdentifier("init"),
TARGET: t.cloneNode(target),
PROPERTY: t.cloneNode(property),
}).expression
: buildGetDescriptor({
TARGET: t.cloneNode(target),
PROPERTY: t.cloneNode(property),
}).expression,
t.cloneNode(target),
]),
);
}

return acc;
}, []);

return t.sequenceExpression([
t.assignmentExpression("=", t.cloneNode(name), path.node),
t.sequenceExpression(exprs),
t.cloneNode(name),
]);
}

return {
inherits: syntaxDecorators,

visitor: {
ClassDeclaration(path) {
const { node } = path;

if (!hasClassDecorators(node) && !hasMethodDecorators(node.body.body)) {
return;
}

const ref = node.id
? t.cloneNode(node.id)
: path.scope.generateUidIdentifier("class");
const letDeclaration = t.variableDeclaration("let", [
t.variableDeclarator(ref, t.toExpression(node)),
]);

if (path.parentPath.isExportDefaultDeclaration()) {
// Split the class declaration and the export into two separate statements.
path.parentPath.replaceWithMultiple([
letDeclaration,
t.exportNamedDeclaration(null, [
t.exportSpecifier(t.cloneNode(ref), t.identifier("default")),
]),
]);
} else {
path.replaceWith(letDeclaration);
}
},
ClassExpression(path, state) {
// Create a replacement for the class node if there is one. We do one pass to replace classes with
// class decorators, and a second pass to process method decorators.
const decoratedClass =
applyEnsureOrdering(path) ||
applyClassDecorators(path, state) ||
applyMethodDecorators(path, state);

if (decoratedClass) path.replaceWith(decoratedClass);
},
ObjectExpression(path, state) {
const decoratedObject =
applyEnsureOrdering(path) || applyObjectDecorators(path, state);

if (decoratedObject) path.replaceWith(decoratedObject);
},

AssignmentExpression(path, state) {
if (!WARNING_CALLS.has(path.node.right)) return;

path.replaceWith(
t.callExpression(state.addHelper("initializerDefineProperty"), [
t.cloneNode(path.get("left.object").node),
t.stringLiteral(path.get("left.property").node.name),
t.cloneNode(path.get("right.arguments")[0].node),
t.cloneNode(path.get("right.arguments")[1].node),
]),
);
},
},
visitor: legacy ? legacyVisitor : visitor,
};
});

0 comments on commit 339dfdd

Please sign in to comment.