swc-project/swc

Class transformer not working with swc

DanielRamosAcosta opened this issue ยท 16 comments

Describe the bug

When applying the plainToClass function to a class defined with class-validator, it returns the class with no attributes

Input code

Here is a repo with a test reproducing the error

const usersOptionsInput = plainToClass(GetUsersOptionsInput, {
  query: "Query",
  pagination: { skip: 0, limit: 20 },
});

expect(usersOptionsInput.query).toEqual("Query"); // error

Config

You can also find the config at the repo reproducing the error

{
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "tsx": false,
      "decorators": true
    },
    "transform": {
      "legacyDecorator": true,
      "decoratorMetadata": true
    },
    "target": "es2018"
  },
  "module": {
    "type": "commonjs",
    "noInterop": true
  }
}

Expected behavior
I would expect the instance of the class to have all the defined attributes.

Version
The version of @swc/core: 1.2.80

Additional context

Just one more time, I created this repo in order to reproduce the error

This error is related to #1362

I'm coming from #1362 and this doesn't work #1362 (comment)

Good repro btw @DanielRamosAcosta!

I've been checking the generated code, and there's a problematic part.

Minimal repro

  • Source
import { IsOptional, IsString } from 'class-validator';

export class GetUsersOptionsInput {
  @IsString()
  @IsOptional()
  public query?: string;
  // it works
  // public query?: string = undefined;
}
  • Generated
"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.GetUsersOptionsInput = void 0;
var _classValidator = require("class-validator");
function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) {
    var desc = {
    };
    Object.keys(descriptor).forEach(function(key) {
        desc[key] = descriptor[key];
    });
    desc.enumerable = !!desc.enumerable;
    desc.configurable = !!desc.configurable;
    if ("value" in desc || desc.initializer) {
        desc.writable = true;
    }
    desc = decorators.slice().reverse().reduce(function(desc, decorator) {
        return decorator ? decorator(target, property, desc) || desc : desc;
    }, desc);
    if (context && desc.initializer !== void 0) {
        desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
        desc.initializer = undefined;
    }
    // IF NO INITILIZAER, THE RETURNED DESCRIPTOR IS NULL
    if (desc.initializer === void 0) {
        Object.defineProperty(target, property, desc);
        desc = null;
    }
    return desc;
}
function _initializerDefineProperty(target, property, descriptor, context) {
    if (!descriptor) return;
    Object.defineProperty(target, property, {
        enumerable: descriptor.enumerable,
        configurable: descriptor.configurable,
        writable: descriptor.writable,
        value: descriptor.initializer ? descriptor.initializer.call(context) : void 0
    });
}
var _class, _descriptor, _dec, _dec1, _dec2;
let GetUsersOptionsInput = ((_class = class GetUsersOptionsInput1 {
    constructor(){
        _initializerDefineProperty(this, "query", _descriptor/*THIS DESCRIPTOR IS NULL*/, this);
    }
}) || _class, _dec = (0, _classValidator).IsString(), _dec1 = (0, _classValidator).IsOptional(), _dec2 = typeof Reflect !== "undefined" && typeof Reflect.metadata === "function" && Reflect.metadata("design:type", String), _descriptor = _applyDecoratedDescriptor(_class.prototype, "query", [
    _dec,
    _dec1,
    _dec2
], {
    configurable: true,
    enumerable: true,
    writable: true,
    initializer: void 0 // THE INITIALIZER IS UNDEFINED
}), _class);
exports.GetUsersOptionsInput = GetUsersOptionsInput;

This is the problematic part from _applyDecoratedDescriptor, this branch is executed by initializer: void 0

    if (desc.initializer === void 0) {
        Object.defineProperty(target, property, desc);
        desc = null;
    }

When the property decorated has not initializer the descriptor decorator returned is null. If we remove that if, the repro code works and tests pass.

I don't know exactly why that check is neccesary, but there is the problem.

I hope it helps @kdy1! Excellent project btw ๐Ÿ†™

Great work @tonivj5! i tried diving into the generated code but I didn't reach any conclusions, thanks for your insight!

Issue on class-transformer side typestack/class-transformer#796 I suppose invalid, and it should be fixed in swc.

@unlight if class-validator works perfect with tsc and fail with swc. I think it's a problem of swc handling decorators...

kdy1 commented

@tonivj5 Did you try importing reflect-metadata?

@kdy1 yep! It doesn't work.

This code does not work:

import "reflect-metadata";
import { IsOptional, IsString } from "class-validator";

export class GetUsersOptionsInput {
  @IsString()
  @IsOptional()
  public query?: string;
}

This code does work:

import "reflect-metadata";
import { IsOptional, IsString } from "class-validator";

export class GetUsersOptionsInput {
  @IsString()
  @IsOptional()
  public query?: string = undefined;
}

I did a deeper research here #2117 (comment)

@tonivj5 Thank you so much for that workaround!

@kdy1 Fixing this would make a world of difference for anyone part of the Nest.js community, it'd be hugely appreciated!

+1, it would be great to fix this. We use class-validator pretty heavily, and we would like to run our unit tests with ts-node + swc since it's dramatically faster than using the default transpiler with ts-node.

RIP21 commented

If you're struggling with this issue here is the way to fix it so it's almost 0 pain while we're waiting for fix.
nestjs/nest-cli#731 (comment)

this is blocking for us to migrate to swc, company wide

RIP21 commented

@acoroleu-tempus use the workaround I mentioned above.

kdy1 commented

I investigated this.

For the initializer property of the property descriptor for fields without an initializer, babel uses null while swc emits void 0. _applyDecoratedDescriptor returns null if initializer is undefined, so the property descriptor used for _initializerDefineProperty while instantiating a class is an object for babel while being null for swc.

https://github.com/babel/babel/blob/69ae3b5a250d60618702751aa2aa4f56ccd43988/packages/babel-plugin-proposal-decorators/src/transformer-legacy.ts#L146-L152

I tried changing the value to null, but it broke other tests. Namely, the tests I added for #2127 in #3105 was problematic.

I decided to postpone this.

There is also a similar issue when upgrading to Next js 12, they use SWC and support legacy decorators but the @expose() and @exclude() decorators also seem to be falling. they return undefined attributes.

Coming from: https://stackoverflow.com/questions/70956406/plaintoclass-from-class-transform-converts-incorrecly
Any progress or possible workaround on this issue? Tried to use esbuild and plainToClass method works correctly.

This closed issue has been automatically locked because it had no new activity for a month. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.