evanw / evanw/esbuild

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

Open
#3,765 1 comment 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Go
Stars
40.1k
Forks
1.3k
PR merge metrics
No merged PRs in 30d

Description

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.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the linked esbuild reproduction and compare the two compilation stages described in the issue. Trace static class-field lowering and subsequent tree shaking, then verify that an unused class with lowered static fields is removed without changing the output for a used class.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, javascript
Domain
build-system, compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.