395 lines
12 KiB
JavaScript
395 lines
12 KiB
JavaScript
// Fork of https://github.com/loganfsmyth/babel-plugin-proposal-decorators-legacy
|
|
|
|
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;
|
|
`);
|
|
|
|
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;
|
|
}
|
|
})
|
|
`);
|
|
|
|
const buildInitializerWarningHelper = template(`
|
|
function NAME(descriptor, context){
|
|
throw new Error(
|
|
'Decorating class property failed. Please ensure that ' +
|
|
'proposal-class-properties is enabled and set to use loose mode. ' +
|
|
'To use proposal-class-properties in spec mode with decorators, wait for ' +
|
|
'the next major version of decorators in stage 2.'
|
|
);
|
|
}
|
|
`);
|
|
|
|
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() {
|
|
/**
|
|
* 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 state.applyDecoratedDescriptor;
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
return state.initializerDefineProp;
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
return state.initializerWarningHelper;
|
|
}
|
|
|
|
/**
|
|
* 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: syntaxDecorators,
|
|
|
|
visitor: {
|
|
ExportDefaultDeclaration(path) {
|
|
if (!path.get("declaration").isClassDeclaration()) return;
|
|
|
|
const { node } = path;
|
|
const ref =
|
|
node.declaration.id || path.scope.generateUidIdentifier("default");
|
|
node.declaration.id = ref;
|
|
|
|
// 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) {
|
|
const { node } = path;
|
|
|
|
const ref = node.id || path.scope.generateUidIdentifier("class");
|
|
|
|
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);
|
|
|
|
if (decoratedClass) path.replaceWith(decoratedClass);
|
|
},
|
|
ObjectExpression(path, state) {
|
|
const decoratedObject =
|
|
applyEnsureOrdering(path) || applyObjectDecorators(path, state);
|
|
|
|
if (decoratedObject) path.replaceWith(decoratedObject);
|
|
},
|
|
|
|
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,
|
|
]),
|
|
);
|
|
},
|
|
},
|
|
};
|
|
}
|