esbuild icon indicating copy to clipboard operation
esbuild copied to clipboard

Weird behavior when bundling to `esnext`

Open Hawmex opened this issue 3 years ago • 5 comments

This issue has two parts

Part 1

esbuild version: 0.12.6

I have 2 dependencies using private fields and accessors syntax (ES2022). One of them (nexwidget) has lit-html as its dependency that is written in ES2017. Consider this:

import { Nexbounce } from 'nexbounce';
import { render } from 'nexwidget';

let counter = 0;

const nexbounce = new Nexbounce();

nexbounce.enqueue(() => (counter += 3));
nexbounce.enqueue(() => (counter += 1));
nexbounce.enqueue(() => (counter += 2));

setTimeout(() => render(counter, document.body));

If I run esbuild with this config:

.\node_modules\.bin\esbuild src/index.js --bundle --target=esnext --splitting --outdir=build --format=esm

Why is there still a polyfill for private fields and accessors in the output file?

var __accessCheck = (obj, member, msg) => {
  if (!member.has(obj))
    throw TypeError("Cannot " + msg);
};
var __privateGet = (obj, member, getter) => {
  __accessCheck(obj, member, "read from private field");
  return getter ? getter.call(obj) : member.get(obj);
};
var __privateAdd = (obj, member, value) => {
  if (member.has(obj))
    throw TypeError("Cannot add the same private member more than once");
  member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
};
var __privateSet = (obj, member, value, setter) => {
  __accessCheck(obj, member, "write to private field");
  setter ? setter.call(obj, value) : member.set(obj, value);
  return value;
};
var __privateMethod = (obj, member, method) => {
  __accessCheck(obj, member, "access private method");
  return method;
};

//...

Part 2

Browser: Chrome 91

If there is a more complex code using the same dependencies (more modules, etc.) bundled with this config:

.\node_modules\.bin\esbuild src/index.js --bundle --target=esnext --splitting --outdir=build --format=esm

In Chrome 91 (I haven't tested other browsers) I get:

Uncaught TypeError: Cannot read from private field
    at __accessCheck (chunk-PSDPPOBH.js:3)
    at __privateGet (chunk-PSDPPOBH.js:6)
    at Function.__create (chunk-PSDPPOBH.js:1293)
    at Function.createAttributes (chunk-PSDPPOBH.js:1093)
    at chunk-PSDPPOBH.js:1409

While with this config (with the exact same input), there are no errors:

.\node_modules\.bin\esbuild src/index.js --bundle --target=es2020--splitting --outdir=build --format=esm

Hawmex avatar Jun 09 '21 12:06 Hawmex

Part 1

I haven't looked into exactly why this happens yet, but this is likely a known limitation of esbuild's class transform. See #1328 for a prior discussion about this.

Part 2

I'm unable to reproduce this using the sample code from part 1. I tried [email protected], [email protected], [email protected], and [email protected] and it appears to work fine for me. The resulting code produces the text 2 when run in Chrome 91. I can look into this more if you provide a way for me to reproduce the problem.

evanw avatar Jun 11 '21 01:06 evanw

@evanw

Part 2 Reproduction

import { Nexwidget } from 'nexwidget';

class TestElement extends Nexwidget {
  get template() {
    return this.testAttribute;
  }
}

TestElement.createAttributes({ testAttribute: String });
TestElement.register();

Looks like it has to do with private static methods. static createAttributes() calls static #ensureAttributes() and there is a problem in transforming (just in esnext, es2020 works fine).

Hawmex avatar Jun 17 '21 07:06 Hawmex

Running into this now, it appears that bundling always transforms private fields.

Test case:

// input.js
class Foo {
  static #hello(){
    console.log("!");
  }
}

esbuild input.js --target=esnext

Using esnext/es2020, the output is unchanged from the input.

esbuild input.js --target=esnext --bundle

Bundling, though, always adds transforms, and same applies for most instances of private methods, private static methods, private getters, etc.

(() => {
  var __privateAdd = (obj, member, value) => {
    if (member.has(obj))
      throw TypeError("Cannot add the same private member more than once");
    member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
  };

  // esbuildtest.js
  var _hello, hello_fn;
  var Foo = class {
  };
  _hello = new WeakSet();
  hello_fn = function() {
    console.log("!");
  };
  __privateAdd(Foo, _hello);
})();

@evanw Looks like it has to do with private static methods. static createAttributes() calls static #ensureAttributes() and there is a problem in transforming (just in esnext, es2020 works fine).

FWIW, on my 0.12.12 build, when --bundle is set, and --target= is es2020, esnext, unset, anything, the private values are always transformed.

jsantell avatar Jul 01 '21 02:07 jsantell

Same thing if using static properties

export class Foo {
  static b = 1;
}

When running esbuild esbuildtest.js --target=esnext the output is untransformed; adding --bundle however:

(() => {
  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);
    return value;
  };

  // esbuildtest.js
  var Foo = class {
  };
  __publicField(Foo, "b", 1);
})();

jsantell avatar Jul 01 '21 15:07 jsantell

Here's a simplified example: https://esbuild.egoist.sh/#W1siaW5kZXgudHMiLHsiY29udGVudCI6ImNsYXNzIEZvbyB7XG4gIHN0YXRpYyBvbmUgPSAnb25lJztcbiAgI3R3byA9ICd0d28nO1xufVxuIn1dLFsiZXNidWlsZC5jb25maWcuanNvbiIseyJjb250ZW50Ijoie1xuICBcImZvcm1hdFwiOiBcImVzbVwiLFxuICBcInRhcmdldFwiOiBcImVzbmV4dFwiLFxuICBcImNkblVybFwiOiBcImh0dHBzOi8vY2RuLnNreXBhY2suZGV2XCJcbn0ifV1d

class Foo {
  static one = 'one';
  #two = 'two';
}

If you have a static public field and a private field (either static or not), the private fields will always be downgraded. So you cannot mix static fields with private fields in a class.

matthewp avatar Jul 10 '22 13:07 matthewp

We're seeing this for static fields:

export class Foo {
    static foo = "foo"
}

Bundled with --format=esm --target=esnext, we end up with something like this:

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);
  return value;
};

// index.js
var Foo = class {
};
__publicField(Foo, "foo", "foo");
export {
  Foo
};

It would be fine, except it breaks tree-shaking 😭 i.e., if we now use this bundled library somewhere, Foo won't be shaken out if unused. We're working around this by doing:

export const Foo = /* @__PURE__ */ (() => {
    return class Foo {
        static foo = "foo"
    }
})()

but it get's a little clunky when TypeScript comes into play.

heypiotr avatar Nov 11 '22 13:11 heypiotr

I'm having the same result.

Updated @matthewp's url to the working domain

https://esbuild.egoist.dev/#W1siaW5kZXgudHMiLHsiY29udGVudCI6ImNsYXNzIEZvbyB7XG4gIHN0YXRpYyBvbmUgPSAnb25lJztcbiAgI3R3byA9ICd0d28nO1xufVxuIn1dLFsiZXNidWlsZC5jb25maWcuanNvbiIseyJjb250ZW50Ijoie1xuICBcImZvcm1hdFwiOiBcImVzbVwiLFxuICBcInRhcmdldFwiOiBcImVzbmV4dFwiLFxuICBcImNkblVybFwiOiBcImh0dHBzOi8vY2RuLnNreXBhY2suZGV2XCJcbn0ifV1d

luwes avatar Dec 21 '22 18:12 luwes

Any news?

snuup avatar Jun 09 '23 21:06 snuup