jayphelps / core-decorators

Library of stage-0 JavaScript decorators (aka ES2016/ES7 decorators but not accurate) inspired by languages that come with built-ins like @​override, @​deprecate, @​autobind, @​mixin and more. Popular with React/Angular, but is framework agnostic.
MIT License
4.52k stars 263 forks source link

Typescript build failing tests. (@lazy-initialize not working correctly with tsc) #130

Open dtweedle opened 7 years ago

dtweedle commented 7 years ago

@BurtHarris

I've been going through some of the typescript features that you've been adding into the project and noticed in issue #123 that after adding in the compiler step that 20 of the tests were failing.

I've narrowed it down to a problem with lazy-initialize being used in the Meta constructor in the private/utils file.

For some reason Meta is not being called during the loading of certain decorators, although funnily enough it still passes it's own tests. I'll keep looking into this when I have time but if you can think why this might be happening I'm all ears.

Removing the decorator from the Meta class allows all 73 tests to pass.

BurtHarris commented 7 years ago

Reproduced same, I'm going to try to understand what's going on, but your workaround seems reasonable. With the obsolete features removed, the Meta class only has one property, removing its lazy initiialization seems reasonable, but I'm wondering if the tests for @lazyInitialize are covering the expected behavior.

dtweedle commented 7 years ago

Been a while but after a bit of digging around it appears that there is something weird going on with the way typescript is desugaring the decorator, I haven't pin pointed it down exactly but during debugging I've noticed that the property descriptor value is being passed in as undefined for those meta properties when transpiling using typescript.

There is also the wider issue here that the mocha testing environment is executing all of the decorators after transpiling using babel, the testing suite is defined to transpile on the line: "npm run build && mocha --compilers js:babel-core/register --require babel-polyfill \"test/**/*.spec.js\" within the package. I think the reason we aren't seeing the lazy-initialise issue during it's unit tests is because it is being transpiled in the file by babel, which we know works. However in the case of the Meta constructor failing it is actually first transpiled by typescript before being imported. I think we will need a new testing script to run the testing suite after it is transpiled by TS, however...

@jayphelps you mention in issue #120 that you don't want to maintain essentially two projects, one for TS and the other for babel when it comes to the decorators. It seems at this point anyway that it will be pretty difficult to do avoid if you plan on supporting both TS and the ECMA communities since their transpile engines have different desugaring strategies for decorators.

@BurtHarris thoughts?

BurtHarris commented 7 years ago

Not sure I understand, but I'll try to wrap my head around it. I think using babel-core/register for testing may hide important details. That's not to say we shouldn't test Babel-transpiled code calling into the library, but it probably makes sense to transpile & save the tests in using both tools.

I think the clear goal is that one set of .js files for the library should suffice. It would be nice if the tests are the same source-wise as well, but for a couple of reasons that may not be practical in the short term. For example: TypeScript won't transpile files with the .js extension right now. It can be made to type-check .js files, but it's more picky about what can be in a .js file than Babel is. Babel (as configured for this project) seems to silently ignore typing information in .js files, but TypeScript finds it objectionable.

BurtHarris commented 7 years ago

@dtweedle you mentioned the TS and the ECMA communities. Do you really mean TS and the Babel communities? As far as I know, ECMA hasn't settled on decorators, so there are no native implementations, and I think eventually both transpilers will have to change some.

--

dtweedle commented 7 years ago

@BurtHarris Sorry my comment made it sound like I was advocating maintaining two libraries, this isn't the case, it's only the compilation step that needs amending.

Typescript and babel both transpile the core-decorators functions similarly from what I can see, it's the actual processing of the decorator themselves (that is the @decorator part) that are handled differently and right now we only use babel as a test runner compiler during testing.

If you want to replace the babel compiler entirely with the typescript one instead you're going to have to run the test suite with tsc.

And yes sorry I meant Babel and TS, not ES.

BurtHarris commented 7 years ago

OK good, I understand better now. Given your observation that the actual code emitted for @decorator differs, I'm saying we can't afford to run the tests only using tsc, but that we need both a tsc and Babel version of the tests.

Getting the tests to run under tsc has been harder than I thought, but I'll give it another push today, and specifically convert them so that the babel tests actually generate code so I can see and characterize the differences in codegen.

BurtHarris commented 7 years ago

@dtweedle, @jayphelps: TypeScript currently has a documented restriction on property decorators:

NOTE  A Property Descriptor is not provided as an argument to a property decorator due to how property decorators are initialized in TypeScript. This is because there is currently no mechanism to describe an instance property when defining members of a prototype, and no way to observe or modify the initializer for a property. As such, a property decorator can only be used to observe that a property of a specific name has been declared for a class.

I've now observed this side-by-side in calls to the nonenumerable decorator from both Babel and TypeScript, the typescript version gets undefined instead of the property descriptor. This input code in nonenumerable.spec.js:

  class Foo {
    @nonenumerable
    bar = 'test';
  }

Gets translated by TypeScript to

var Foo = (function () {
        function Foo() {
            this.bar = 'test';
        }
        __decorate([
            core_decorators_1.nonenumerable
        ], Foo.prototype, "bar", void 0);
        return Foo;
    }());

The void 0 taking the place of the missing descriptor parameter... Babel on the other hand generates:

  var Foo = (_class = function Foo() {
    _classCallCheck(this, Foo);

    _initDefineProp(this, 'bar', _descriptor, this);
  }, (_descriptor = _applyDecoratedDescriptor(_class.prototype, 'bar', [_coreDecorators.nonenumerable], {
    enumerable: true,
    initializer: function initializer() {
      return 'test';
    }
  })), _class);

So the Babel transpiler creates a synthetic descriptor with enumerable and initializer properties (the later being non-standard.)

It seems clear that until this gets changed in TypeScript, we can't get lazy-initialize to work, and a number of the other tests, when called from TypeScript, are going to fail.

BurtHarris commented 7 years ago

Typescript weirdness (part two)

Inside the TypeScript emitted __decorate method, if the descriptor is not provided, it looks one up using Object.getOwnPropertyDescriptor(target, key). For method decorators, this is returning a descriptor with the right value, but the configurable, enumerable, and writable properties are all set to true, at least in some of this projects test cases. The resulting behavior breaks more tests (e.g. one in decorate.spec.js) because the test is checking the enumerable property and finding it unexpectedly set. For method decorators, the TypeScript emitted code does lookup the method's descriptor (using Object.getOwnPropertyDescriptor

jayphelps commented 7 years ago

@BurtHarris forgive my ignorance: is this still an issue after #133?

BurtHarris commented 7 years ago

It depends...,

yes lazy initialize is still broken, but

no the tests don't fail because I removed its use from the utils.ts file.

BurtHarris commented 7 years ago

I understand now why lazy initialize is broken, the inclusion of a "initialize" member in the descriptor passed to a property decorator wasn't part of the stage 0 proposal, and thus wasn't implemented by TypeScript.

jayphelps commented 7 years ago

@BurtHarris I'm OK with deprecating and removing lazyInitialize tbh. I'm doubtful any sizable number of people use it and now that we're dropping the debounce, etc I don't think we need it internally.

BurtHarris commented 7 years ago

Gosh, I was really hoping it would work.

jayphelps commented 7 years ago

@BurtHarris are you a big user of lazyInitialize?

dtweedle commented 7 years ago

@BurtHarris are you saying that initialize property is causing the typescript decorators to fail?

BurtHarris commented 7 years ago

@jayphelps No, I'm not a big user of lazyInitialize, it just sounds like a cool use of decorators.

@dtweedle I'm saying that without the initialize property in a descriptor (which I don't think was part of the stage 0 proposal) it is impossible to make lazyInitialize work. However with PR #133 (my typescriptify branch) all the build and test breaks are gone, you may want to have a look at that branch and try it out.

Jay may know if it's part of the stage-3 draft includes the initialize property, I'll have to read that document all over again, I've learned a lot about decorators since the last time I looked it over.

jayphelps commented 7 years ago

@BurtHarris it's still up in the air, but I find it unlikely both decorators and class fields will ship without interop between them. TC39 is still attempting to solidify these behaviors, the latest is here: https://github.com/littledan/proposal-unified-class-features/blob/master/DETAILS.md#member-descriptors where indeed it will be given an initializer it can wrap; though how decorators work are pretty different ever since stage-1 (everyone implements stage-0 as of now) so it's super unclear whether TypeScript will actually bite the bullet and ship that major breaking change eventually.