// 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, ]), ); }, }, }; }