evanw / esbuild

An extremely fast bundler for the web
https://esbuild.github.io/
MIT License
38.19k stars 1.15k forks source link

Esbuild breaks bundle when `nodent-runtime` is included #1802

Closed BendingBender closed 2 years ago

BendingBender commented 2 years ago

Hi, we have an Angular project and we use the angular/cli@12.2.13 along with @angular-devkit/build-angular@12.2.13 to build a universal app. This internally uses esbuild@0.13.8 to minify the generated bundles.

When we include a dependency that uses the nodent-runtime@3.2.1 package, the build succeeds but the generated bundle then throws when executed with the error: ReferenceError: __name is not defined.

It seems to be related to how nodent-runtime works, which seems to stringify and evaluate a function via Function constructor whose contents were previously annotated with Esbuild's __name helper and this helper seems not to be in scope any more when this evaluation takes place.

Here's the full error output:

❯ node dist/frontend/server/main.js 
undefined:3
return function $asyncbind2(self2, catcher) { "use strict"; Function.prototype.$asyncbind || Object.defineProperty(Function.prototype, "$asyncbind", { value: $asyncbind2, enumerable: !1, configurable: !0, writable: !0 }), $asyncbind2.trampoline || ($asyncbind2.trampoline = __name(function(t, x, s, e, u) { return __name(function b(q) { for (; q; ) { if (q.then) return q = q.then(b, e), u ? void 0 : q; try { if (q.pop) { if (q.length) return q.pop() ? x.call(t) : q; q = s; } else q = q.call(t); } catch (r) { return e(r); } } }, "b"); }, "trampoline")), $asyncbind2.LazyThenable || ($asyncbind2.LazyThenable = function() { function isThenable(obj) { return obj && obj instanceof Object && typeof obj.then == "function"; } __name(isThenable, "isThenable"); function resolution(p, r, how) { try { var x = how ? how(r) : r; if (p === x) return p.reject(new TypeError("Promise resolution loop")); isThenable(x) ? x.then(function(y) { resolution(p, y); }, function(e) { p.reject(e); }) : p.resolve(x); } catch (ex) { p.reject(ex); } } __name(resolution, "resolution"); function _unchained(v) { } __name(_unchained, "_unchained"); function thenChain(res, rej) { this.resolve = res, this.reject = rej; } __name(thenChain, "thenChain"); function Chained() { } __name(Chained, "Chained"), Chained.prototype = { resolve: _unchained, reject: _unchained, then: thenChain }; function then(res, rej) { var chain = new Chained(); try { this._resolver(function(value) { return isThenable(value) ? value.then(res, rej) : resolution(chain, value, res); }, function(ex) { resolution(chain, ex, rej); }); } catch (ex) { resolution(chain, ex, rej); } return chain; } __name(then, "then"); function Thenable(resolver) { this._resolver = resolver, this.then = then; } return __name(Thenable, "Thenable"), Thenable.resolve = function(v) { return Thenable.isThenable(v) ? v : { then: function(resolve) { return resolve(v); } }; }, Thenable.isThenable = isThenable, Thenable; }(), $asyncbind2.EagerThenable = $asyncbind2.Thenable = ($asyncbind2.EagerThenableFactory = function(tick) { tick = tick || typeof process == "object" && process.nextTick || typeof setImmediate == "function" && setImmediate || function(f) { setTimeout(f, 0); }; var soon = function() { var fq = [], fqStart = 0, bufferSize = 1024; function callQueue() { for (; fq.length - fqStart; ) { try { fq[fqStart](); } catch (ex) { } fq[fqStart++] = void 0, fqStart === bufferSize && (fq.splice(0, bufferSize), fqStart = 0); } } return __name(callQueue, "callQueue"), function(fn2) { fq.push(fn2), fq.length - fqStart == 1 && tick(callQueue); }; }(); function Zousan(func) { if (func) { var me = this; func(function(arg) { me.resolve(arg); }, function(arg) { me.reject(arg); }); } } __name(Zousan, "Zousan"), Zousan.prototype = { resolve: function(value) { if (this.state === void 0) { if (value === this) return this.reject(new TypeError("Attempt to resolve promise with self")); var me = this; if (value && (typeof value == "function" || typeof value == "object")) try { var first = 0, then = value.then; if (typeof then == "function") { then.call(value, function(ra) { first++ || me.resolve(ra); }, function(rr) { first++ || me.reject(rr); }); return; } } catch (e) { first || this.reject(e); return; } this.state = STATE_FULFILLED, this.v = value, me.c && soon(function() { for (var n = 0, l = me.c.length; n < l; n++) STATE_FULFILLED(me.c[n], value); }); } }, reject: function(reason) { if (this.state === void 0) { this.state = STATE_REJECTED, this.v = reason; var clients = this.c; clients && soon(function() { for (var n = 0, l = clients.length; n < l; n++) STATE_REJECTED(clients[n], reason); }); } }, then: function(onF, onR) { var p = new Zousan(), client = { y: onF, n: onR, p }; if (this.state === void 0) this.c ? this.c.push(client) : this.c = [client]; else { var s = this.state, a = this.v; soon(function() { s(client, a); }); } return p; } }; function STATE_FULFILLED(c, arg) { if (typeof c.y == "function") try { var yret = c.y.call(void 0, arg); c.p.resolve(yret); } catch (err) { c.p.reject(err); } else c.p.resolve(arg); } __name(STATE_FULFILLED, "STATE_FULFILLED"); function STATE_REJECTED(c, reason) { if (typeof c.n == "function") try { var yret = c.n.call(void 0, reason); c.p.resolve(yret); } catch (err) { c.p.reject(err); } else c.p.reject(reason); } return __name(STATE_REJECTED, "STATE_REJECTED"), Zousan.resolve = function(val) { if (val && val instanceof Zousan) return val; var z = new Zousan(); return z.resolve(val), z; }, Zousan.reject = function(err) { if (err && err instanceof Zousan) return err; var z = new Zousan(); return z.reject(err), z; }, Zousan.version = "2.3.3-nodent", Zousan; })()); function boundThen() { return resolver.apply(self2, arguments); } __name(boundThen, "boundThen"); var resolver = this; switch (catcher) { case !0: return new $asyncbind2.Thenable(boundThen); case 0: return new $asyncbind2.LazyThenable(boundThen); case void 0: return boundThen.then = boundThen, boundThen; default: return function() { try { return resolver.apply(self2, arguments); } catch (ex) { return catcher(ex); } }; } }
                                                                                                                                                                                                                                                         ^

ReferenceError: __name is not defined
    at $asyncbind2 (eval at processIncludes ([omitted]/frontend/dist/frontend/server/main.js:94350:72), <anonymous>:3:250)
    at Object.59761 ([omitted]/frontend/dist/frontend/server/main.js:94438:43)
    at __webpack_require__ ([omitted]/frontend/dist/frontend/server/main.js:122007:42)
    at Object.7764 ([omitted]/frontend/dist/frontend/server/main.js:88229:28)
    at __webpack_require__ ([omitted]/frontend/dist/frontend/server/main.js:122007:42)
    at Object.16981 ([omitted]/frontend/dist/frontend/server/main.js:88170:29)
    at __webpack_require__ ([omitted]/frontend/dist/frontend/server/main.js:122007:42)
    at Object.83418 ([omitted]/frontend/dist/frontend/server/main.js:111747:451)
    at __webpack_require__ ([omitted]/frontend/dist/frontend/server/main.js:122007:42)
    at Object.74549 ([omitted]/frontend/dist/frontend/server/main.js:114419:423)

node --version: v16.11.1
npm --version: 8.1.4

hyrious commented 2 years ago

This package uses Function.toString() to modify the function $asyncbind: https://github.com/MatAtBread/nodent-runtime/blob/master/runtime.js#L21 Then use new Function() to return the modified function again.

The problem here is after this code being processed with esbuild's --keep-names option, esbuild puts some __name() to the original $asyncbind function body, and new Function() creates a pure environment that evaluates at the global scope where code can not find variables outer:

var __name = 1
new Function(`console.log(__name)`)() // ReferenceError: __name is not defined
// NOTE: try run this code in a closure, for example in a function(){}

The solution is either not relying on function string or not using --keep-names.

BendingBender commented 2 years ago

Tried this out, without keepNames option the build isn't broken any more. Thank you!

evanw commented 2 years ago

This package uses Function.toString() to modify the function $asyncbind: https://github.com/MatAtBread/nodent-runtime/blob/master/runtime.js#L21 Then use new Function() to return the modified function again.

Libraries that do this are explicitly not supported: https://esbuild.github.io/content-types/#function-tostring. Libraries that do this will likely not work with esbuild, and may break with esbuild in the future even if they happen to work at the current moment. This is because esbuild relies on the ability to introduce helper functions for various things and relies on the ability to rename things within the bounds of JavaScript's scoping rules.

So disabling --keep-names isn't really a solution even if it happens to work because may can break at any time in the future. It's fine if it works for you! But I just wanted to give you a heads up that doing this is brittle and may still not work. There is no straightforward way to fix this from esbuild's side. The way to get this to work robustly with esbuild would be for that library to change their build script to convert the function to a string ahead of time instead of calling toString() at run-time.

Closing this issue because this is out of scope. Preserving the value of toString() is explicitly unsupported.

BendingBender commented 2 years ago

Just for the record, disabling keepNames only solved the immediate runtime error. The bundle was still broken after disabling this option. We solved the problem by removing the offending lib completely.