esbuild: esbuild minify fails some recent uglify-js tests
Using node v16.1.0 with the patch below, esbuild e76b49fa3c9d8ef4dc13b4d7a3eb04b524ff4626 produces:
$ make clean-all uglify
...
29 failed out of 3163
make: *** [uglify] Error 1
Note that all the original unminified expect_stdout test cases will run successfully on node v16.1.0.
Some failures are due to esbuild being more strict than recent versions of NodeJS, but around half are genuine esbuild minify errors.
diff --git a/Makefile b/Makefile
index 19dfe06..cf97c76 100644
--- a/Makefile
+++ b/Makefile
@@ -372,3 +372,3 @@ github/uglify:
cd github/uglify && git init && git remote add origin https://github.com/mishoo/uglifyjs.git
- cd github/uglify && git fetch --depth 1 origin 83a3cbf1514e81292b749655f2f712e82a5a2ba8 && git checkout FETCH_HEAD
+ cd github/uglify && git fetch --depth 1 origin d2a45ba441ed6975021b3524215c01a011dfb46a && git checkout FETCH_HEAD
diff --git a/scripts/uglify-tests.js b/scripts/uglify-tests.js
index 4214959..e4cf715 100644
--- a/scripts/uglify-tests.js
+++ b/scripts/uglify-tests.js
@@ -102,2 +102,14 @@ async function test_case(esbuild, test) {
+ // Ignore tests without expect_stdout, as these tests cannot be verified when run
+ if (!("expect_stdout" in test)) return;
+
+ // Ignore tests that will likely result in a syntax error in recent NodeJS versions.
+ // uglify-js generally accepts whatever inputs that NodeJS allowed/allows
+ // as long as it is unambiguous with grammar lookahead -
+ // even if it deviates from the latest ECMAScript specification.
+ // `true` in this context means "the minified test program output matches the
+ // unminified test program output for this version of NodeJS - whether valid
+ // or an error."
+ if (test.expect_stdout === true) return;
+
// Ignore tests that no longer pass in modern versions of node. These tests
@@ -117,17 +129,4 @@ async function test_case(esbuild, test) {
minify: true,
- target: 'es5',
});
} catch (e) {
- // These two tests fail because they contain setters without arguments,
- // which is a syntax error. These test failures do not indicate anything
- // wrong with esbuild so the failures are ignored. Here is one of the
- // tests:
- //
- // function f(){var a={get b(){},set b(){}};return{a:a}}
- //
- if (test.name === 'unsafe_object_accessor' || test.name === 'keep_name_of_setter') {
- console.error("*** skipping test with known syntax error:", test.name);
- return;
- }
-
const formatError = ({ text, location }) => {
About this issue
- Original URL
- State: closed
- Created 3 years ago
- Comments: 18 (18 by maintainers)
My bad. I mistakenly assumed that the remaining test failures were there by design. No one truly cares about using
await
as an identifier, for example.I’d consider any lack of TDZ checking in Rollup to be a bug. But just using its REPL now I see it’s pretty common. It’s progressing though - rollup has recently added support for side effects for setters and getters, among other checks now.
A workaround to support TDZ with Rollup would be to use
--no-treeshake
and employ a minifier like uglify-jsor terserthat respects TDZ side effects. There’s a lot of overlap between Rollup optimizations and minifier optimizations. The only significant drawback with this approach is that webpack-stylesideEffects: false
code pruning would also be disabled, if the bundle happens to significantly rely upon it. The minifier wouldn’t be able to drop the side-effect code for bundledsideEffects: false
modules - unless pure annotations were used.Edit: terser does not reliably respect TDZ side effects:
uglify-js does however:
The
--out-extension
feature is intended to be used for this.It wasn’t missed. That’s part of why this issue is still open. I have limited time this week so I was only able to fix some of the things so far.
This should be what happens in esbuild too with
--splitting
enabled. But code splitting can also be disabled, which may not be a feature that Rollup has.Also it doesn’t look like Rollup treats TDZ checks as a side effect. For example bundling
let a = a
with Rollup generates an empty file even though that code will throw an exception, which is a side effect.You don’t have to convince me - I’m no fan of TDZ and appreciate why it’s a hindrance to bundling. I’m just saying it should be documented, that’s all. Perhaps you already have.
Yes, one could argue that. However, it seems different to me because of the extreme scale of the problem (the VM locks up for many seconds and it affects virtually every modern code base due to the use of
class
andlet
/const
). Although it only affects top-level variables so you could potentially argue that the minifier shouldn’t consider references side-effect free if they are either non-top-level or maybe inside a try/catch or something. I could see potentially making a change like that.Yeah. I’m aware of that one but haven’t fixed it yet because no one has asked for it. I should probably fix it though. It comes up in test262 as well.
By the way the failures due to
await
are because of esbuild’s top-level await support. The parser does not make a distinction between the script and module goals so it always parses top-level await. This hasn’t ever been an issue with real-world code and is basically only a problem for conformance tests. But I think it’s an acceptable trade-off.I have fixed the following issues so far, although there are still more issues to be fixed. Here is the text from the release notes:
The
in
operator is now surrounded parentheses inside arrow function expression bodies insidefor
loop initializers:Without this, the
in
operator would cause the for loop to be considered a for-in loop instead.The statement
return undefined;
is no longer minified toreturn;
inside async generator functions:Using
return undefined;
inside an async generator function has the same effect asreturn await undefined;
which schedules a task in the event loop and runs code in a different order than justreturn;
, which doesn’t hide an implicitawait
expression.Property access expressions are no longer inlined in template tag position:
The expression
a.b`c`
is different than the expression(0, a.b)`c`
. The first calls the functiona.b
witha
as the value forthis
but the second calls the functiona.b
with the default value forthis
(the global object in non-strict mode orundefined
in strict mode).Verbatim
__proto__
properties inside object spread are no longer inlined when minifying:A verbatim (i.e. non-computed non-method) property called
__proto__
inside an object literal actually sets the prototype of the surrounding object literal. It does not add an “own property” called__proto__
to that object literal, so inlining it into the parent object literal would be incorrect. The presence of a__proto__
property now stops esbuild from applying the object spread inlining optimization when minifying.The value of
this
has now been fixed for lowered private class members that are used as template tags:The value of
this
here should be an instance of the class because the template tag is a property access expression. However, it was previously the default value (the global object in non-strict mode orundefined
in strict mode) instead due to the private member transformation, which is incorrect.It’s up to you. TDZ is a part of the ES spec. One could make the argument that avoiding triggering V8/Chrome bugs is not much different than avoiding the Safari JS engine TDZ slowness with correct ES code. uglify-js does manage to preserve TDZ semantics while providing good minification.
I’ve also noticed failures related to https://tc39.es/proposal-template-literal-revision/.