Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decorator Support #330

Open
bradenhs opened this issue Nov 9, 2018 · 19 comments
Open

Decorator Support #330

bradenhs opened this issue Nov 9, 2018 · 19 comments

Comments

@bradenhs
Copy link

bradenhs commented Nov 9, 2018

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.

@alangpierce
Copy link
Owner

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

@bradenhs
Copy link
Author

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.

@alangpierce
Copy link
Owner

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

@bradenhs
Copy link
Author

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

@alangpierce
Copy link
Owner

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
Copy link
Contributor

yang commented Mar 6, 2019

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

@jeremy-coleman
Copy link

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

@szagi3891
Copy link

szagi3891 commented Mar 15, 2019

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

@alangpierce
Copy link
Owner

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.

@szagi3891
Copy link

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 ?

@jeremy-coleman
Copy link

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

@joaogn
Copy link

joaogn commented Jun 27, 2019

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
Copy link

zxti commented Nov 28, 2019

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

@josecfreittas
Copy link

Hi @alangpierce, any news about this?

@PIMBA
Copy link

PIMBA commented Aug 4, 2020

any news about this?

@PIMBA
Copy link

PIMBA commented Aug 8, 2020

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

@AlonMiz
Copy link

AlonMiz commented Jul 29, 2021

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

@DavidZemon
Copy link

DavidZemon commented Dec 8, 2021

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.

@eightHundreds
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests