[7.0] Replacing current decorators with decorators-legacy (#5290)
This commit is contained in:
@@ -1,132 +1,341 @@
|
||||
// Fork of https://github.com/loganfsmyth/babel-plugin-transform-decorators-legacy
|
||||
|
||||
import template from "babel-template";
|
||||
import explodeClass from "babel-helper-explode-class";
|
||||
|
||||
const buildClassDecorator = template(`
|
||||
CLASS_REF = DECORATOR(CLASS_REF) || CLASS_REF;
|
||||
DECORATOR(CLASS_REF = INNER) || CLASS_REF;
|
||||
`);
|
||||
|
||||
export default function ({ types: t }) {
|
||||
function cleanDecorators(decorators) {
|
||||
return decorators.reverse().map((dec) => dec.expression);
|
||||
}
|
||||
const buildClassPrototype = template(`
|
||||
CLASS_REF.prototype;
|
||||
`);
|
||||
|
||||
function transformClass(path, ref, state) {
|
||||
const nodes = [];
|
||||
const buildGetDescriptor = template(`
|
||||
Object.getOwnPropertyDescriptor(TARGET, PROPERTY);
|
||||
`);
|
||||
|
||||
state;
|
||||
|
||||
let classDecorators = path.node.decorators;
|
||||
if (classDecorators) {
|
||||
path.node.decorators = null;
|
||||
classDecorators = cleanDecorators(classDecorators);
|
||||
|
||||
for (const decorator of classDecorators) {
|
||||
nodes.push(buildClassDecorator({
|
||||
CLASS_REF: ref,
|
||||
DECORATOR: decorator
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
const map = Object.create(null);
|
||||
|
||||
for (const method of path.get("body.body")) {
|
||||
const decorators = method.node.decorators;
|
||||
if (!decorators) continue;
|
||||
|
||||
const alias = t.toKeyAlias(method.node);
|
||||
map[alias] = map[alias] || [];
|
||||
map[alias].push(method.node);
|
||||
|
||||
method.remove();
|
||||
}
|
||||
|
||||
for (const alias in map) {
|
||||
const items = map[alias];
|
||||
|
||||
items;
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function hasDecorators(path) {
|
||||
if (path.isClass()) {
|
||||
if (path.node.decorators) return true;
|
||||
|
||||
for (const method of (path.node.body.body: Array<Object>)) {
|
||||
if (method.decorators) {
|
||||
return true;
|
||||
const buildGetObjectInitializer = template(`
|
||||
(TEMP = Object.getOwnPropertyDescriptor(TARGET, PROPERTY), (TEMP = TEMP ? TEMP.value : undefined), {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
initializer: function(){
|
||||
return TEMP;
|
||||
}
|
||||
}
|
||||
} else if (path.isObjectExpression()) {
|
||||
for (const prop of (path.node.properties: Array<Object>)) {
|
||||
if (prop.decorators) {
|
||||
return true;
|
||||
})
|
||||
`);
|
||||
|
||||
const buildInitializerWarningHelper = template(`
|
||||
function NAME(descriptor, context){
|
||||
throw new Error(
|
||||
'Decorating class property failed. Please ensure that ' +
|
||||
'transform-class-properties is enabled.'
|
||||
);
|
||||
}
|
||||
`);
|
||||
|
||||
const buildInitializerDefineProperty = template(`
|
||||
function NAME(target, property, descriptor, context){
|
||||
if (!descriptor) return;
|
||||
|
||||
Object.defineProperty(target, property, {
|
||||
enumerable: descriptor.enumerable,
|
||||
configurable: descriptor.configurable,
|
||||
writable: descriptor.writable,
|
||||
value: descriptor.initializer ? descriptor.initializer.call(context) : void 0,
|
||||
});
|
||||
}
|
||||
`);
|
||||
|
||||
const buildApplyDecoratedDescriptor = template(`
|
||||
function NAME(target, property, decorators, descriptor, context){
|
||||
var desc = {};
|
||||
Object['ke' + 'ys'](descriptor).forEach(function(key){
|
||||
desc[key] = descriptor[key];
|
||||
});
|
||||
desc.enumerable = !!desc.enumerable;
|
||||
desc.configurable = !!desc.configurable;
|
||||
if ('value' in desc || desc.initializer){
|
||||
desc.writable = true;
|
||||
}
|
||||
}
|
||||
|
||||
desc = decorators.slice().reverse().reduce(function(desc, decorator){
|
||||
return decorator(target, property, desc) || desc;
|
||||
}, desc);
|
||||
|
||||
if (context && desc.initializer !== void 0){
|
||||
desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
|
||||
desc.initializer = undefined;
|
||||
}
|
||||
|
||||
if (desc.initializer === void 0){
|
||||
// This is a hack to avoid this being processed by 'transform-runtime'.
|
||||
// See issue #9.
|
||||
Object['define' + 'Property'](target, property, desc);
|
||||
desc = null;
|
||||
}
|
||||
|
||||
return desc;
|
||||
}
|
||||
`);
|
||||
|
||||
export default function({ types: t }) {
|
||||
/**
|
||||
* Add a helper to take an initial descriptor, apply some decorators to it, and optionally
|
||||
* define the property.
|
||||
*/
|
||||
function ensureApplyDecoratedDescriptorHelper(path, state) {
|
||||
if (!state.applyDecoratedDescriptor) {
|
||||
state.applyDecoratedDescriptor = path.scope.generateUidIdentifier("applyDecoratedDescriptor");
|
||||
const helper = buildApplyDecoratedDescriptor({
|
||||
NAME: state.applyDecoratedDescriptor,
|
||||
});
|
||||
path.scope.getProgramParent().path.unshiftContainer("body", helper);
|
||||
}
|
||||
|
||||
return false;
|
||||
return state.applyDecoratedDescriptor;
|
||||
}
|
||||
|
||||
function doError(path) {
|
||||
throw path.buildCodeFrameError(
|
||||
`Decorators are not officially supported yet in 6.x pending a proposal update.
|
||||
However, if you need to use them you can install the legacy decorators transform with:
|
||||
/**
|
||||
* Add a helper to call as a replacement for class property definition.
|
||||
*/
|
||||
function ensureInitializerDefineProp(path, state) {
|
||||
if (!state.initializerDefineProp) {
|
||||
state.initializerDefineProp = path.scope.generateUidIdentifier("initDefineProp");
|
||||
const helper = buildInitializerDefineProperty({
|
||||
NAME: state.initializerDefineProp,
|
||||
});
|
||||
path.scope.getProgramParent().path.unshiftContainer("body", helper);
|
||||
}
|
||||
|
||||
npm install babel-plugin-transform-decorators-legacy --save-dev
|
||||
return state.initializerDefineProp;
|
||||
}
|
||||
|
||||
and add the following line to your .babelrc file:
|
||||
/**
|
||||
* Add a helper that will throw a useful error if the transform fails to detect the class
|
||||
* property assignment, so users know something failed.
|
||||
*/
|
||||
function ensureInitializerWarning(path, state) {
|
||||
if (!state.initializerWarningHelper) {
|
||||
state.initializerWarningHelper = path.scope.generateUidIdentifier("initializerWarningHelper");
|
||||
const helper = buildInitializerWarningHelper({
|
||||
NAME: state.initializerWarningHelper,
|
||||
});
|
||||
path.scope.getProgramParent().path.unshiftContainer("body", helper);
|
||||
}
|
||||
|
||||
{
|
||||
"plugins": ["transform-decorators-legacy"]
|
||||
}
|
||||
return state.initializerWarningHelper;
|
||||
}
|
||||
|
||||
The repo url is: https://github.com/loganfsmyth/babel-plugin-transform-decorators-legacy.
|
||||
`);
|
||||
/**
|
||||
* 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.expression));
|
||||
if (identDecorators.length === 0) return;
|
||||
|
||||
return t.sequenceExpression(identDecorators.map((decorator) => {
|
||||
const expression = decorator.expression;
|
||||
const id = decorator.expression = path.scope.generateDeclaredUidIdentifier("dec");
|
||||
return t.assignmentExpression("=", id, expression);
|
||||
}).concat([path.node]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a class expression with class-level decorators, create a new expression
|
||||
* with the proper decorated behavior.
|
||||
*/
|
||||
function applyClassDecorators(classPath) {
|
||||
const decorators = classPath.node.decorators || [];
|
||||
classPath.node.decorators = null;
|
||||
|
||||
if (decorators.length === 0) return;
|
||||
|
||||
const name = classPath.scope.generateDeclaredUidIdentifier("class");
|
||||
|
||||
return decorators
|
||||
.map((dec) => dec.expression)
|
||||
.reverse()
|
||||
.reduce(function(acc, decorator) {
|
||||
return buildClassDecorator({
|
||||
CLASS_REF: name,
|
||||
DECORATOR: decorator,
|
||||
INNER: acc,
|
||||
}).expression;
|
||||
}, classPath.node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a class expression with method-level decorators, create a new expression
|
||||
* with the proper decorated behavior.
|
||||
*/
|
||||
function applyMethodDecorators(path, state) {
|
||||
const hasMethodDecorators = path.node.body.body.some(function(node) {
|
||||
return (node.decorators || []).length > 0;
|
||||
});
|
||||
|
||||
if (!hasMethodDecorators) return;
|
||||
|
||||
return applyTargetDecorators(path, state, path.node.body.body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an object expression with property decorators, create a new expression
|
||||
* with the proper decorated behavior.
|
||||
*/
|
||||
function applyObjectDecorators(path, state) {
|
||||
const hasMethodDecorators = path.node.properties.some(function(node) {
|
||||
return (node.decorators || []).length > 0;
|
||||
});
|
||||
|
||||
if (!hasMethodDecorators) 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");
|
||||
|
||||
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(
|
||||
ensureInitializerWarning(path, state), [descriptor, t.thisExpression()]
|
||||
);
|
||||
|
||||
acc = acc.concat([
|
||||
t.assignmentExpression(
|
||||
"=", descriptor, t.callExpression(ensureApplyDecoratedDescriptorHelper(path, state), [
|
||||
target,
|
||||
property,
|
||||
t.arrayExpression(decorators.map((dec) => dec.expression)),
|
||||
t.objectExpression([
|
||||
t.objectProperty(t.identifier("enumerable"), t.booleanLiteral(true)),
|
||||
t.objectProperty(t.identifier("initializer"), initializer),
|
||||
]),
|
||||
])
|
||||
),
|
||||
]);
|
||||
} else {
|
||||
acc = acc.concat(
|
||||
t.callExpression(ensureApplyDecoratedDescriptorHelper(path, state), [
|
||||
target,
|
||||
property,
|
||||
t.arrayExpression(decorators.map((dec) => dec.expression)),
|
||||
(
|
||||
t.isObjectProperty(node) ||
|
||||
t.isClassProperty(node, { static: true })) ?
|
||||
buildGetObjectInitializer({
|
||||
TEMP: path.scope.generateDeclaredUidIdentifier("init"),
|
||||
TARGET: target,
|
||||
PROPERTY: property,
|
||||
}).expression : buildGetDescriptor({
|
||||
TARGET: target,
|
||||
PROPERTY: property,
|
||||
}
|
||||
).expression,
|
||||
target,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return t.sequenceExpression([
|
||||
t.assignmentExpression("=", name, path.node),
|
||||
t.sequenceExpression(exprs),
|
||||
name,
|
||||
]);
|
||||
}
|
||||
|
||||
return {
|
||||
inherits: require("babel-plugin-syntax-decorators"),
|
||||
|
||||
visitor: {
|
||||
ClassExpression(path) {
|
||||
if (!hasDecorators(path)) return;
|
||||
doError(path);
|
||||
ExportDefaultDeclaration(path) {
|
||||
if (!path.get("declaration").isClassDeclaration()) return;
|
||||
|
||||
explodeClass(path);
|
||||
const { node } = path;
|
||||
const ref = node.declaration.id || path.scope.generateUidIdentifier("default");
|
||||
node.declaration.id = ref;
|
||||
|
||||
const ref = path.scope.generateDeclaredUidIdentifier("ref");
|
||||
let nodes = [];
|
||||
|
||||
nodes.push(t.assignmentExpression("=", ref, path.node));
|
||||
|
||||
nodes = nodes.concat(transformClass(path, ref, this));
|
||||
|
||||
nodes.push(ref);
|
||||
|
||||
path.replaceWith(t.sequenceExpression(nodes));
|
||||
// Split the class declaration and the export into two separate statements.
|
||||
path.replaceWith(node.declaration);
|
||||
path.insertAfter(t.exportNamedDeclaration(null, [t.exportSpecifier(ref, t.identifier("default"))]));
|
||||
},
|
||||
|
||||
ClassDeclaration(path) {
|
||||
if (!hasDecorators(path)) return;
|
||||
doError(path);
|
||||
explodeClass(path);
|
||||
const { node } = path;
|
||||
|
||||
const ref = path.node.id;
|
||||
let nodes = [];
|
||||
const ref = node.id || path.scope.generateUidIdentifier("class");
|
||||
|
||||
nodes = nodes.concat(transformClass(path, ref, this).map((expr) => t.expressionStatement(expr)));
|
||||
nodes.push(t.expressionStatement(ref));
|
||||
path.replaceWith(t.variableDeclaration("let", [
|
||||
t.variableDeclarator(ref, t.toExpression(node))
|
||||
]));
|
||||
},
|
||||
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)
|
||||
);
|
||||
|
||||
path.insertAfter(nodes);
|
||||
if (decoratedClass) path.replaceWith(decoratedClass);
|
||||
},
|
||||
ObjectExpression(path, state) {
|
||||
const decoratedObject = applyEnsureOrdering(path) || applyObjectDecorators(path, state);
|
||||
|
||||
if (decoratedObject) path.replaceWith(decoratedObject);
|
||||
},
|
||||
|
||||
ObjectExpression(path) {
|
||||
if (!hasDecorators(path)) return;
|
||||
doError(path);
|
||||
}
|
||||
AssignmentExpression(path, state) {
|
||||
if (!state.initializerWarningHelper) return;
|
||||
|
||||
if (!path.get("left").isMemberExpression()) return;
|
||||
if (!path.get("left.property").isIdentifier()) return;
|
||||
if (!path.get("right").isCallExpression()) return;
|
||||
if (!path.get("right.callee").isIdentifier({ name: state.initializerWarningHelper.name })) return;
|
||||
|
||||
path.replaceWith(t.callExpression(ensureInitializerDefineProp(path, state), [
|
||||
path.get("left.object").node,
|
||||
t.stringLiteral(path.get("left.property").node.name),
|
||||
path.get("right.arguments")[0].node,
|
||||
path.get("right.arguments")[1].node,
|
||||
]));
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user