esbuild icon indicating copy to clipboard operation
esbuild copied to clipboard

Static class fields can’t be tree-shaken away on subsequent compilation

Open iamakulov opened this issue 1 year ago • 1 comments

Hey Evan,

Thank you for creating and maintaining esbuild!

We @ Framer stumbled upon an issue around static class fields. When you use them, and you’re compiling for ES2021 and below, esbuild produces code that’s not tree-shakeable on a subsequent compilation. This is relevant e.g. when you’re a library author and are distributing the library as a bundle.

Steps to reproduce

  • Create my-library.js with the following code:

    export class Navigation {
      state = defaultState()
    
      static defaultProps = {
        enabled: true,
      }
    
      static contextType = NavigationCallbackContext
    }
    
  • Bundle the library with { target: "es2019" } into my-library-compiled.js. Observe that the library is compiled down to:

    var __defProp = Object.defineProperty;
    var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
    var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
    export class Navigation {
      constructor() {
        __publicField(this, "state", defaultState());
      }
    }
    __publicField(Navigation, "defaultProps", {
      enabled: true
    });
    __publicField(Navigation, "contextType", NavigationCallbackContext);
    
  • Now, import the compiled library from another file (say, my-app.js):

    import {} from "./my-library-compiled.js"
    
  • Bundle that file (using esbuild, webpack, etc – doesn’t matter). Observe that the app bundle includes Navigation, even though it’s not used.

Actual result

Any classes that use static fields in the library can’t be tree-shaken away due to top-level __publicField setters.

Expected result

esbuild compiles classes with static fields down to something like this:

export class Navigation {
  state = defaultState()

  static defaultProps = {
    enabled: true,
  }

  static contextType = NavigationCallbackContext
}

export var Navigation = /* @__PURE__ */ (() => {
  class Navigation {
    constructor() {
      __publicField(this, "state", defaultState());
    }
  }
  
  __publicField(Navigation, "defaultProps", {
    enabled: true
  });
  
  __publicField(Navigation, "contextType", NavigationCallbackContext);
  
  return Navigation;
})()

which allows Navigation to be tree-shaken away.

iamakulov avatar May 14 '24 00:05 iamakulov

It'd be great to support this. I'm surprised to see little fanfare around this issue. Is there another issue that already pointed out this deficiency?

aleclarson avatar Oct 30 '25 18:10 aleclarson