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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parcel not respecting tsconfig useDefineForClassFields: false with decorators #8036

Closed
e111077 opened this issue May 1, 2022 · 9 comments 路 Fixed by #8107
Closed

Parcel not respecting tsconfig useDefineForClassFields: false with decorators #8036

e111077 opened this issue May 1, 2022 · 9 comments 路 Fixed by #8107

Comments

@e111077
Copy link

e111077 commented May 1, 2022

馃悰 bug report

Heya please let me know if this should instead by filed under swc.

In projects that rely on decorators with accessors, in tsconfig you have to set useDefineForClassFields: false because decorators that generate accessors do not get called if the properties are initialized using Object.defineProperty.

When defining a decorator around a property and you have useDefineForClassFields:false, Parcel will still use Object.define under the hood because it uses @swc/helpers/_initializerDefineProperty.js.

馃帥 Configuration (.babelrc, package.json, cli command)

command: parcel build

no babelrc, only tsconfig:

{
  "compilerOptions": {
    "target": "es2022",
    "module": "es2022",
    "lib": ["es2021", "DOM", "DOM.Iterable"],
    "useDefineForClassFields": false,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "inlineSources": true,
    "outDir": "./",
    "rootDir": "./",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitAny": true,
    "noImplicitThis": true,
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "forceConsistentCasingInFileNames": true,
    "noImplicitOverride": true,
  },
  "include": ["**/*.ts"],
  "exclude": []
}

馃 Expected Behavior

Given a simple @logger decorator that creates a set and get in which it will console.log in the getter:

export class LoggerClass {
  @logger()
  myProp = 5;
}

function logger() {
  return function (proto: any, originalKey: string) {
    const privateKey = `__${originalKey}`];
    Object.defineProperty(proto, originalKey, {
      get() {
        console.log(`${originalKey}: ${this[privateKey]}`);
        return this[privateKey];
      },
      set(this, value) {
        this[privateKey] = value;
      }
    })
  };
}

const instance = new LoggerClass();
instance.myProp;  // console.log(5)
instance.myProp = 6;
instance.myProp; // console.log(6)

this should be transpiled to:

let LoggerClass = (_class = class LoggerClass {
    constructor(){
        this.myProp = 5; // Does not use Object.defineProperty and gets called after the Object.defineProperty in function logger() so it does not override the Object.defineProperty in function logger()
    }
}, _dec = logger(), _descriptor = _helpers.applyDecoratedDescriptor(_class.prototype, "myProp", [
    _dec
], {
    configurable: true,
    enumerable: true,
    writable: true,
    initializer: function() {
        return 5;
    }
}), _class);
function logger() {
    return function(proto, originalKey) {
        const privateKey = `__${originalKey}`;
        Object.defineProperty(proto, originalKey, { // this gets called before this.myProp = 5 in the constructor()
            get () {
                console.log(`${originalKey}: ${this[privateKey]}`);
                return this[privateKey];
            },
            set (value) {
                this[privateKey] = value;
            }
        });
    };
}
const instance = new LoggerClass();
instance.myProp;
instance.myProp = 6;
instance.myProp;

馃槸 Current Behavior

It is instead transpiled to:

let LoggerClass = (_class = class LoggerClass {
    constructor(){
        _helpers.initializerDefineProperty(this, "myProp", _descriptor, this); // gets called after function logger() and used Object.define under the hood
    }
}, _dec = logger(), _descriptor = _helpers.applyDecoratedDescriptor(_class.prototype, "myProp", [
    _dec
], {
    configurable: true,
    enumerable: true,
    writable: true,
    initializer: function() {
        return 5;
    }
}), _class);
function logger() {
    return function(proto, originalKey) {
        const privateKey = `__${originalKey}`;
        Object.defineProperty(proto, originalKey, { // Gets called first
            get () {
                console.log(`${originalKey}: ${this[privateKey]}`);
                return this[privateKey];
            },
            set (value) {
                this[privateKey] = value;
            }
        });
    };
}
const instance = new LoggerClass();
instance.myProp; // nothing logged
instance.myProp = 6;
instance.myProp; // nothing logged

Because the constructor gets called after the decorator's logger() function, the Object.defineProperty in the constructor is overriding the Object.defineProperty in the logger() decorator, thus making it near impossible to wrap a class property in a decorator with a class accessor.

馃拋 Possible Solution

Opt-out of useDefineForClassFields when using decorators if the flag is set to false.

Workarounds:

use the TS compiler instead in .parcelrc

馃敠 Context

Trying to use several libraries that wrap class members in accessors with decorators.

馃捇 Code Sample

export class LoggerClass {
  @logger()
  myProp = 5;
}

function logger() {
  return function (proto: any, originalKey: string) {
    const privateKey = `__${originalKey}`];
    Object.defineProperty(proto, originalKey, {
      get() {
        console.log(`${originalKey}: ${this[privateKey]}`);
        return this[privateKey];
      },
      set(this, value) {
        this[privateKey] = value;
      }
    })
  };
}

const instance = new LoggerClass();
instance.myProp;  // console.log(5)
instance.myProp = 6;
instance.myProp; // console.log(6)

馃實 Your Environment

Software Version(s)
Parcel 2.5.0
Node 17.4.0
npm/Yarn 8.4.0
Operating System Mac OS 12.3.1
@e111077
Copy link
Author

e111077 commented May 3, 2022

Looks like SWC just merged a possible fix. When can we expect this version to be integrated into parcel?

swc-project/swc#4496

@mischnic mischnic added the swc label May 3, 2022
@mischnic
Copy link
Member

mischnic commented May 3, 2022

#8029 is currently blocking an swc upgrade, because swc did some breaking changes.

@Blackwidow-sudo
Copy link

Ah i just wasted a whole day on trying to figure out why my reactive properties (added with the @property() decorator) in my lit-element's dont work...

Is there any workaround?

@e111077
Copy link
Author

e111077 commented May 16, 2022

In Lit, declare your reactive properties as you would in JS, static properties = {propName: {type: Number}}

@Blackwidow-sudo
Copy link

Blackwidow-sudo commented May 16, 2022

Well then i would also have to write the constructor instead of just using @customElement("my-elem")?
So i guess we will have to wait for the parcel update to make use of the new lit syntax?
(sry for my newbie questions :D im a newbie with bundlers)

@e111077
Copy link
Author

e111077 commented May 16, 2022

Correct, you would have to write the constructor to initialize your property, but the customElement decorator has nothing to do with the constructor and should still work despite this but because it's a class decorator not an accessor decorator.

Edit: Actually I think parcel without decorators and the useDefineForClassFields: false flag should work such that you don't have to write the constructor

@Blackwidow-sudo
Copy link

Blackwidow-sudo commented May 16, 2022

Actually I think parcel without decorators and the useDefineForClassFields: false flag should work such that you don't have to write the constructor

Im a bit confused rn^^
Could you elaborate? Cause my goal was to write the lit-elements with decorators. Thats why i created a tsconfig and set the "experimentalDecorators": true & "useDefineForClassFields": false (as suggested by Lit Docs).
So for example this example doesn't work when i build it with parcel (its not reactive).
When i change:

@property({ type: Number })
count = 0

// to

static properties = { count: { type: Number } }

i have to add a class field count: number and the constructor with super() to initialize count.
So after all these changes it works (i tried).

But your suggestion is to not use decorators and to remove the useDefineForClassFields: false-flag, so then i can omit writing the constructor? Or am i misunderstanding you?

@e111077
Copy link
Author

e111077 commented May 17, 2022

I actually had time to sit down and test my hypothesis. You will not be able to use class fields at all here. Sorry for the confusion! You have to include the constructor, because it seems like SWC will just ignore that flag. You have to do the following:

@customElement('my-element')
export class MyElement extends LitElement {
  static properties = { count: { type: Number } }

  count: Number;
  
  constructor() {
    super();
    this.count = 0;
  }  
}

Here is a live repro.

Though if you have further questions about Lit, I feel like this is probably an inappropriate place to have this conversation. I recommend you join the Lit Slack for further Lit questions.

@mischnic
Copy link
Member

Looks like SWC just merged a possible fix. When can we expect this version to be integrated into parcel?
swc-project/swc#4496

swc is now upgraded in the nightly version (yarn add parcel@nightly).

The real problems appears to be though that Parcel only forwards experimentalDecorators

decorators = compilerOptions?.experimentalDecorators;

and not useDefineForClassFields which is just hardcoded
use_define_for_class_fields: true

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

Successfully merging a pull request may close this issue.

3 participants