Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Weird behavior when bundling to esnext #1360

Closed
Hawmex opened this issue Jun 9, 2021 · 8 comments · Fixed by #3167
Closed

Weird behavior when bundling to esnext #1360

Hawmex opened this issue Jun 9, 2021 · 8 comments · Fixed by #3167

Comments

@Hawmex
Copy link

Hawmex commented Jun 9, 2021

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
@evanw
Copy link
Owner

evanw commented Jun 11, 2021

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 esbuild@0.12.8, lit-html@1.4.1, nexwidget@2.0.4, and nexbounce@1.0.21 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.

@Hawmex
Copy link
Author

Hawmex commented Jun 17, 2021

@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).

@jsantell
Copy link

jsantell commented Jul 1, 2021

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
Copy link

jsantell commented Jul 1, 2021

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);
})();

@matthewp
Copy link

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.

@heypiotr
Copy link
Contributor

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.

@evanw evanw added the classes label Apr 4, 2023
@snuup
Copy link

snuup commented Jun 9, 2023

Any news?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants