alangpierce/sucrase

Decorator Support

bradenhs opened this issue ยท 18 comments

Sucrase gives me an "Unexpected character '@'" when trying to transform a typescript file with decorators in it. The readme doesn't say anything about decorators but a quick search of the code revealed a few references to decorators. Are decorators supported in sucrase currently? If so, is there some undocumented transform I would need to enable?

Also great idea for this project! It's exactly what we need to help speed up our development feedback loop.

Hi @bradenhs , thanks for the kind words!

Decorators are not supported at the moment, unfortunately. The only JS features that are transformed right now are the four listed in the README (class fields, export namespace, numeric separators, optional catch binding). I guess I should make it more clear which notable things are unsupported.

If you want to get really technical, you might say that decorators are "supported" in the sense that Sucrase should accept code with decorators and emit the same code with decorators without crashing, just like lots of other not-transformed JS features like classes and arrow functions. (Feel free to file a bug if that isn't working.) But that's not very useful these days since decorators aren't implemented in any JS runtime. You could, for example, run code through Sucrase and then through Babel, but that also defeats the purpose of Sucrase ๐Ÿ˜„.

The reason you were seeing decorators mentioned in the code is that Sucrase uses a fork of the Babel parser, which does understand decorator syntax, and I've been keeping the implementation up-to-date. So the token stream does include the @ tokens without getting confused, but Sucrase doesn't make any attempt to actually modify the code.

Could you explain your use cases for decorators and how widespread they are in your code? Implementing a decorator transform isn't out of the question, but it would be difficult. It's sort of in a gray area from a project vision standpoint, but I did implement class fields, which were also difficult but important because they're so common in real-world TypeScript code. I'm also a little hesitant because decorators aren't officially in JS yet and have been in committee discussions for years, and in TS they're disabled by default (behind the --experimentalDecorators flag).

They're pretty widespread. We use mobx for all of our state management in our react app and the preferred way to make data observable in mobx is with decorators. So a large scale refactor on our end isn't really an option. That being said I totally understand your hesitance to include support for decorators and agree it's probably a good idea to hold off supporting them until they reach stage 3 (hopefully soon!). The references to decorators in the code just made me curious if they already were supported but not documented.

Got it, I haven't personally used mobx, but certainly seems popular enough to justify some extra effort. I'll try taking a closer look at some point, it might not be so bad. From this example, looks like it'll hopefully mostly consist of inserting a code snippet at the top of the file and running some code after class init for each class, which we already for for static methods.

Do you know any good open source projects (public on GitHub) that use mobx or otherwise use decorators and have tests that exercise them? Ideally I'd verify correctness by getting to a point where I can clone a project like that, patch the build system to use Sucrase, run tests, and see that tests pass. That's already set up for a number of projects, but none of them use decorators: https://github.com/alangpierce/sucrase/tree/master/example-runner/example-configs

I took a look at a couple projects that make use of decorators but unfortunately their tests don't use them extensively. Mobx-react does have some tests with decorators so you could take a look at it. It's kinda a sticky area to get into though. TypeScript uses an old implementation of decorators and the latest revision of the spec would require a different emit. Babel has a legacy decorators preset with the same implementation as typescript. But there's also a new Babel decorator preset for the latest revision of the spec. If you want to avoid a headache it may make sense to wait for the dust to settle before moving forward with this especially since the decorators proposal should be moving to stage 3 in the near future (at least from what I've read about it).

For reference, here's before-and-after of the latest Babel decorator implementation. It has a lot of helper methods, but those are pretty easy from Sucrase's perspective. I think the main challenge will be properly detecting them in class processing and moving the right components to a statement at the end of the class. And I guess every decorator position may have its own challenges in terms of code transform. It also will rearrange code, which is sad, though there may be a trick to avoid that like I already do for class fields.

class A {
  @foo @bar(a)
  x = 1;
  @baz
  y = 2;
}
function _decorate(decorators, factory, superClass) { var r = factory(function initialize(O) { _initializeInstanceElements(O, decorated.elements); }, superClass); var decorated = _decorateClass(_coalesceClassElements(r.d.map(_createElementDescriptor)), decorators); _initializeClassElements(r.F, decorated.elements); return _runClassFinishers(r.F, decorated.finishers); }

function _createElementDescriptor(def) { var key = _toPropertyKey(def.key); var descriptor; if (def.kind === "method") { descriptor = { value: def.value, writable: true, configurable: true, enumerable: false }; Object.defineProperty(def.value, "name", { value: typeof key === "symbol" ? "" : key, configurable: true }); } else if (def.kind === "get") { descriptor = { get: def.value, configurable: true, enumerable: false }; } else if (def.kind === "set") { descriptor = { set: def.value, configurable: true, enumerable: false }; } else if (def.kind === "field") { descriptor = { configurable: true, writable: true, enumerable: true }; } var element = { kind: def.kind === "field" ? "field" : "method", key: key, placement: def.static ? "static" : def.kind === "field" ? "own" : "prototype", descriptor: descriptor }; if (def.decorators) element.decorators = def.decorators; if (def.kind === "field") element.initializer = def.value; return element; }

function _coalesceGetterSetter(element, other) { if (element.descriptor.get !== undefined) { other.descriptor.get = element.descriptor.get; } else { other.descriptor.set = element.descriptor.set; } }

function _coalesceClassElements(elements) { var newElements = []; var isSameElement = function (other) { return other.kind === "method" && other.key === element.key && other.placement === element.placement; }; for (var i = 0; i < elements.length; i++) { var element = elements[i]; var other; if (element.kind === "method" && (other = newElements.find(isSameElement))) { if (_isDataDescriptor(element.descriptor) || _isDataDescriptor(other.descriptor)) { if (_hasDecorators(element) || _hasDecorators(other)) { throw new ReferenceError("Duplicated methods (" + element.key + ") can't be decorated."); } other.descriptor = element.descriptor; } else { if (_hasDecorators(element)) { if (_hasDecorators(other)) { throw new ReferenceError("Decorators can't be placed on different accessors with for " + "the same property (" + element.key + ")."); } other.decorators = element.decorators; } _coalesceGetterSetter(element, other); } } else { newElements.push(element); } } return newElements; }

function _hasDecorators(element) { return element.decorators && element.decorators.length; }

function _isDataDescriptor(desc) { return desc !== undefined && !(desc.value === undefined && desc.writable === undefined); }

function _initializeClassElements(F, elements) { var proto = F.prototype; ["method", "field"].forEach(function (kind) { elements.forEach(function (element) { var placement = element.placement; if (element.kind === kind && (placement === "static" || placement === "prototype")) { var receiver = placement === "static" ? F : proto; _defineClassElement(receiver, element); } }); }); }

function _initializeInstanceElements(O, elements) { ["method", "field"].forEach(function (kind) { elements.forEach(function (element) { if (element.kind === kind && element.placement === "own") { _defineClassElement(O, element); } }); }); }

function _defineClassElement(receiver, element) { var descriptor = element.descriptor; if (element.kind === "field") { var initializer = element.initializer; descriptor = { enumerable: descriptor.enumerable, writable: descriptor.writable, configurable: descriptor.configurable, value: initializer === void 0 ? void 0 : initializer.call(receiver) }; } Object.defineProperty(receiver, element.key, descriptor); }

function _decorateClass(elements, decorators) { var newElements = []; var finishers = []; var placements = { static: [], prototype: [], own: [] }; elements.forEach(function (element) { _addElementPlacement(element, placements); }); elements.forEach(function (element) { if (!_hasDecorators(element)) return newElements.push(element); var elementFinishersExtras = _decorateElement(element, placements); newElements.push(elementFinishersExtras.element); newElements.push.apply(newElements, elementFinishersExtras.extras); finishers.push.apply(finishers, elementFinishersExtras.finishers); }); if (!decorators) { return { elements: newElements, finishers: finishers }; } var result = _decorateConstructor(newElements, decorators); finishers.push.apply(finishers, result.finishers); result.finishers = finishers; return result; }

function _addElementPlacement(element, placements, silent) { var keys = placements[element.placement]; if (!silent && keys.indexOf(element.key) !== -1) { throw new TypeError("Duplicated element (" + element.key + ")"); } keys.push(element.key); }

function _decorateElement(element, placements) { var extras = []; var finishers = []; for (var decorators = element.decorators, i = decorators.length - 1; i >= 0; i--) { var keys = placements[element.placement]; keys.splice(keys.indexOf(element.key), 1); var elementObject = _fromElementDescriptor(element); var elementFinisherExtras = _toElementFinisherExtras((0, decorators[i])(elementObject) || elementObject); element = elementFinisherExtras.element; _addElementPlacement(element, placements); if (elementFinisherExtras.finisher) { finishers.push(elementFinisherExtras.finisher); } var newExtras = elementFinisherExtras.extras; if (newExtras) { for (var j = 0; j < newExtras.length; j++) { _addElementPlacement(newExtras[j], placements); } extras.push.apply(extras, newExtras); } } return { element: element, finishers: finishers, extras: extras }; }

function _decorateConstructor(elements, decorators) { var finishers = []; for (var i = decorators.length - 1; i >= 0; i--) { var obj = _fromClassDescriptor(elements); var elementsAndFinisher = _toClassDescriptor((0, decorators[i])(obj) || obj); if (elementsAndFinisher.finisher !== undefined) { finishers.push(elementsAndFinisher.finisher); } if (elementsAndFinisher.elements !== undefined) { elements = elementsAndFinisher.elements; for (var j = 0; j < elements.length - 1; j++) { for (var k = j + 1; k < elements.length; k++) { if (elements[j].key === elements[k].key && elements[j].placement === elements[k].placement) { throw new TypeError("Duplicated element (" + elements[j].key + ")"); } } } } } return { elements: elements, finishers: finishers }; }

function _fromElementDescriptor(element) { var obj = { kind: element.kind, key: element.key, placement: element.placement, descriptor: element.descriptor }; var desc = { value: "Descriptor", configurable: true }; Object.defineProperty(obj, Symbol.toStringTag, desc); if (element.kind === "field") obj.initializer = element.initializer; return obj; }

function _toElementDescriptors(elementObjects) { if (elementObjects === undefined) return; return _toArray(elementObjects).map(function (elementObject) { var element = _toElementDescriptor(elementObject); _disallowProperty(elementObject, "finisher", "An element descriptor"); _disallowProperty(elementObject, "extras", "An element descriptor"); return element; }); }

function _toElementDescriptor(elementObject) { var kind = String(elementObject.kind); if (kind !== "method" && kind !== "field") { throw new TypeError('An element descriptor\'s .kind property must be either "method" or' + ' "field", but a decorator created an element descriptor with' + ' .kind "' + kind + '"'); } var key = _toPropertyKey(elementObject.key); var placement = String(elementObject.placement); if (placement !== "static" && placement !== "prototype" && placement !== "own") { throw new TypeError('An element descriptor\'s .placement property must be one of "static",' + ' "prototype" or "own", but a decorator created an element descriptor' + ' with .placement "' + placement + '"'); } var descriptor = elementObject.descriptor; _disallowProperty(elementObject, "elements", "An element descriptor"); var element = { kind: kind, key: key, placement: placement, descriptor: Object.assign({}, descriptor) }; if (kind !== "field") { _disallowProperty(elementObject, "initializer", "A method descriptor"); } else { _disallowProperty(descriptor, "get", "The property descriptor of a field descriptor"); _disallowProperty(descriptor, "set", "The property descriptor of a field descriptor"); _disallowProperty(descriptor, "value", "The property descriptor of a field descriptor"); element.initializer = elementObject.initializer; } return element; }

function _toElementFinisherExtras(elementObject) { var element = _toElementDescriptor(elementObject); var finisher = _optionalCallableProperty(elementObject, "finisher"); var extras = _toElementDescriptors(elementObject.extras); return { element: element, finisher: finisher, extras: extras }; }

function _fromClassDescriptor(elements) { var obj = { kind: "class", elements: elements.map(_fromElementDescriptor) }; var desc = { value: "Descriptor", configurable: true }; Object.defineProperty(obj, Symbol.toStringTag, desc); return obj; }

function _toClassDescriptor(obj) { var kind = String(obj.kind); if (kind !== "class") { throw new TypeError('A class descriptor\'s .kind property must be "class", but a decorator' + ' created a class descriptor with .kind "' + kind + '"'); } _disallowProperty(obj, "key", "A class descriptor"); _disallowProperty(obj, "placement", "A class descriptor"); _disallowProperty(obj, "descriptor", "A class descriptor"); _disallowProperty(obj, "initializer", "A class descriptor"); _disallowProperty(obj, "extras", "A class descriptor"); var finisher = _optionalCallableProperty(obj, "finisher"); var elements = _toElementDescriptors(obj.elements); return { elements: elements, finisher: finisher }; }

function _disallowProperty(obj, name, objectType) { if (obj[name] !== undefined) { throw new TypeError(objectType + " can't have a ." + name + " property."); } }

function _optionalCallableProperty(obj, name) { var value = obj[name]; if (value !== undefined && typeof value !== "function") { throw new TypeError("Expected '" + name + "' to be a function"); } return value; }

function _runClassFinishers(constructor, finishers) { for (var i = 0; i < finishers.length; i++) { var newConstructor = (0, finishers[i])(constructor); if (newConstructor !== undefined) { if (typeof newConstructor !== "function") { throw new TypeError("Finishers must return a constructor."); } constructor = newConstructor; } } return constructor; }

function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }

function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }

function _toArray(arr) { return _arrayWithHoles(arr) || _iterableToArray(arr) || _nonIterableRest(); }

function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); }

function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); }

function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }

let A = _decorate(null, function (_initialize) {
  class A {
    constructor() {
      _initialize(this);
    }

  }

  return {
    F: A,
    d: [{
      kind: "field",
      decorators: [foo, bar(a)],
      key: "x",

      value() {
        return 1;
      }

    }, {
      kind: "field",
      decorators: [baz],
      key: "y",

      value() {
        return 2;
      }

    }]
  };
});
yang commented

Another use case is react-dnd, which like mobx emphasizes using decorators!

an easier solution might be to use templates for the most common decorators and leave it at that. like for mobx (which i also use) , relies on 3, 2 of which (action and computed) could just be templated - in a similar manor to how the babel transforms for import()s to promises do it

I also use mobx. I would like to use sucrase but I can't because of lack of support from decorators :(

@alangpierce

Do you know any good open source projects (public on GitHub) that use mobx or otherwise use decorators and have tests that exercise them?

Example projects:
https://github.com/mobxjs/awesome-mobx#example-projects

react-mobx-realworld-example-app:
https://github.com/gothinkster/react-mobx-realworld-example-app

Thanks! I'm a bit hesitant to implement old-style decorators, but it looks like mobx does have a plan to support new-style decorators once they're finally standardized:

mobxjs/mobx#1928
https://github.com/mobxjs/mobx/issues?utf8=โœ“&q=label%3Awaiting-for-standardized-decorators+

Unfortunately looks like it may be a while. Really, I'm hoping that decorators will get standardized and Chrome will implement them before too long, but it may be best to get something working in Sucrase (or find some alternate solution) in the meantime.

There will always be some strange syntax that you will need to support.
It seems to me that it is more important that it can be easily managed.

This module "@babel/preset-env" have wery good approach. Maybe you could apply a similar approach to decorators (or more features) ? Mayby, when decorators are standardized and the old version is forgotten, you will throw away the support for the old type of decorations ?

One thing to consider , is that you can basically separate decorators into 2 categories.

  1. hoc (ie: withSomeProps)
  2. compile time reflectors (inversify,angular)

Most decorators are just the hoc kind , so like @behavior(newprops)(original-object) is roughly equal to behavior(newprops => original) . So, for popular libraries , the simplest approach would be to just make some template wrapper, pretty much the equivalent of turning an require statement to an import statement

I have the same problem with sequelize-typescript

https://github.com/RobinBuschmann/sequelize-typescript

The sequelize-typescript is very useful for typing the models in a less verbose way.

zxti commented

typeorm is another project that's painful to use without decorators.

Hi @alangpierce, any news about this?

PIMBA commented

any news about this?

PIMBA commented

I have been write a fallback options for webpack-loader at #549 that can allow user fallback to another loader, like babel-loader to transpile javascript file.

{
  transforms: ['typescript', 'jsx'],
  fallback: {
    test: `(code) => !!code.split('\\n').map(x => x.trim()).find(x => x.startsWith('@')`
    loader: 'babel-loader'
  }
}

tried sucrase for jest and currently ditching it as it's not supporting Decorators
specifically the use of class-transformers

    Details:

    app-frontend/src/models/session/model.ts:25
      Type(() => UserSettings) 
           ^
    SyntaxError: Unexpected token '('

Trying to introduce Inversify into our instance of Spotify's Backstage and having an unfortunate time because Backstage seems to be pretty tightly coupled to Sucrase. I've made it work, but having support for decorators would be very nice.