Skip to content

Bug: private class fields transformed incorrectly in ESM #2230

Closed
@dreamorosi

Description

Expected Behaviour

When using the Idempotency utility with a function formatted as ESM the utility and the function should work as intended.

Current Behaviour

As reported by @AllyMurray in this comment, using the Idempotency utility with ESM bundling causes the following runtime error: __classPrivateFieldSet is not defined at import time.

Looking at the stack trace, the error can be traced back to a private field in the IdempotencyConfig class. This is not the only occurrence of private fields (v8.dev), however it's the first occurrence in the import stack.

Code snippet

import { IdempotencyConfig } from '@aws-lambda-powertools/idempotency';

new IdempotencyConfig({});

Steps to Reproduce

  1. Install @aws-lambda-powertools/idempotency
  2. Bundle your function using ESM format
  3. Run the function
  4. Observe the error in the logs

Possible Solution

After a few tests it seems that the root cause for the issue is the noEmitHelpers: true setting that we have enabled for the ESM builds here and its interaction with the ES2021 (Node.js 16) target that we currently use.

When enabled, this setting makes it so that certain language features are polyfilled during transform depending on the targeted version. In the case of private class fields, depending on the target, the following code:

class MyClass {
  readonly #enabled: boolean;

  public constructor() {
    this.#enabled = true;
  }

  public getEnabled(): boolean {
    return this.#enabled;
  }
}

const foo = new MyClass();

becomes this:

var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
    if (kind === "m") throw new TypeError("Private method is not writable");
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
    return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
    return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _MyClass_enabled;
class MyClass {
    constructor() {
        _MyClass_enabled.set(this, void 0);
        __classPrivateFieldSet(this, _MyClass_enabled, true, "f");
    }
    getEnabled() {
        return __classPrivateFieldGet(this, _MyClass_enabled, "f");
    }
}
_MyClass_enabled = new WeakMap();
const foo = new MyClass();

instead of remaining as it is.

When implementing v2 I disabled the setting for ESM builds thinking that we wouldn't need them since according to the v8.dev docs the feature is supported in Node.js 14+ and we have a target of ES2021 which roughly aligns to Node.js 16.

Apparently I was mistaken, and the combination of noEmitHelpers: true and target: 'ES2021' caused the esm bundle to be emitted like this, aka __classPrivateFieldSet is not included even though the code has been downleveled and expects it:

var _MyClass_enabled;
class MyClass {
    constructor() {
        _MyClass_enabled.set(this, void 0);
        __classPrivateFieldSet(this, _MyClass_enabled, true, "f");
    }
    getEnabled() {
        return __classPrivateFieldGet(this, _MyClass_enabled, "f");
    }
}
_MyClass_enabled = new WeakMap();
const foo = new MyClass();

The short term solution is to disable the noEmitHelpers setting and make a release, this will fix the issue at the cost of a few extra lines (~20) being present in the bundle.

In the medium term, and specifically once we drop support for Node.js 16 (#2223) in a few months, we'll change the target to ES2022 and when that happens we will be able to disable noEmitHelpers again since TypeScript emits the private fields as they are (aka without needing an helper fn).

Additionally, and this is just as critical, we'll have to prioritize adding new tests that check ESM builds and catch these types of issues.

Powertools for AWS Lambda (TypeScript) version

latest

AWS Lambda function runtime

20.x

Packaging format used

npm

Execution logs

No response

Metadata

Assignees

Labels

bugSomething isn't workingcompletedThis item is complete and has been merged/shippedidempotencyThis item relates to the Idempotency Utility

Type

No type

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions