esbuild
esbuild copied to clipboard
Weird behavior when bundling to `esnext`
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
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
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).
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()
callsstatic #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.
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);
})();
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.
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.
I'm having the same result.
Updated @matthewp's url to the working domain
https://esbuild.egoist.dev/#W1siaW5kZXgudHMiLHsiY29udGVudCI6ImNsYXNzIEZvbyB7XG4gIHN0YXRpYyBvbmUgPSAnb25lJztcbiAgI3R3byA9ICd0d28nO1xufVxuIn1dLFsiZXNidWlsZC5jb25maWcuanNvbiIseyJjb250ZW50Ijoie1xuICBcImZvcm1hdFwiOiBcImVzbVwiLFxuICBcInRhcmdldFwiOiBcImVzbmV4dFwiLFxuICBcImNkblVybFwiOiBcImh0dHBzOi8vY2RuLnNreXBhY2suZGV2XCJcbn0ifV1d
Any news?