sucrase icon indicating copy to clipboard operation
sucrase copied to clipboard

Decorator Support

Open bradenhs opened this issue 6 years ago • 19 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.

bradenhs avatar Nov 09 '18 21:11 bradenhs

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).

alangpierce avatar Nov 10 '18 19:11 alangpierce

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.

bradenhs avatar Nov 12 '18 16:11 bradenhs

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

alangpierce avatar Nov 12 '18 17:11 alangpierce

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).

bradenhs avatar Nov 13 '18 18:11 bradenhs

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;
      }

    }]
  };
});

alangpierce avatar Nov 19 '18 02:11 alangpierce

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

yang avatar Mar 06 '19 07:03 yang

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

jeremy-coleman avatar Mar 15 '19 03:03 jeremy-coleman

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

szagi3891 avatar Mar 15 '19 21:03 szagi3891

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:

https://github.com/mobxjs/mobx/issues/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.

alangpierce avatar Mar 16 '19 04:03 alangpierce

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 ?

szagi3891 avatar Mar 16 '19 05:03 szagi3891

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

jeremy-coleman avatar Mar 16 '19 20:03 jeremy-coleman

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.

joaogn avatar Jun 27 '19 23:06 joaogn

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

zxti avatar Nov 28 '19 21:11 zxti

Hi @alangpierce, any news about this?

josecfreittas avatar Jan 09 '20 01:01 josecfreittas

any news about this?

PIMBA avatar Aug 04 '20 06:08 PIMBA

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'
  }
}

PIMBA avatar Aug 08 '20 08:08 PIMBA

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 '('

AlonMiz avatar Jul 29 '21 10:07 AlonMiz

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.

DavidZemon avatar Dec 08 '21 21:12 DavidZemon

my solution

// jest.config.ts
export default {
  testEnvironment: 'node',
  transform: { 
    "\\.(js|jsx|ts|tsx)$": `<rootDir>/custom-transform.js`,
  }
}
// custom-transform.js
const sucraseProcess = require('@sucrase/jest-plugin');
const { createTransformer } = require('babel-jest');

const babelTransformer = createTransformer({});

module.exports.process = function (src, filename, options) {
  try {
    return sucraseProcess(src, filename, options);
  } catch (error) {
    return babelTransformer.process(src, filename, options);
  }
};

eightHundreds avatar Jan 02 '24 11:01 eightHundreds