-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
esbuild minify fails some recent uglify-js tests #1305
Comments
Thanks for reporting this. I appreciate the heads up. Looking into these now... |
Hmm... I think the skipped test cases have been incorrectly folded into The figures with the following patch look more reasonable:
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..61745a6 100644
--- a/scripts/uglify-tests.js
+++ b/scripts/uglify-tests.js
@@ -9,2 +9,3 @@ const testDir = path.join(repoDir, 'scripts', '.uglify-tests');
const uglifyDir = path.join(repoDir, 'demo', 'uglify');
+const SKIP = new Error("SKIP");
let U;
@@ -36,6 +37,8 @@ async function main() {
let failedTotal = 0;
+ let skippedTotal = 0;
const runTest = file => test_file(esbuild, path.join(compressDir, file))
- .then(({ passed, failed }) => {
+ .then(({ passed, failed, skipped }) => {
passedTotal += passed;
failedTotal += failed;
+ skippedTotal += skipped;
});
@@ -46,3 +49,3 @@ async function main() {
- console.log(`${failedTotal} failed out of ${passedTotal + failedTotal}`);
+ console.log(`${failedTotal} failed out of ${passedTotal + failedTotal}, with ${skippedTotal} skipped`);
if (failedTotal) {
@@ -55,2 +58,3 @@ async function test_file(esbuild, file) {
let failed = 0;
+ let skipped = 0;
const tests = parse_test(file);
@@ -59,8 +63,11 @@ async function test_file(esbuild, file) {
.catch(e => {
- failed++;
- console.error(`<U+274C> ${file}: ${name}: ${(e && e.message || e).trim()}\n`);
- pass = false;
+ if (e === SKIP) {
+ skipped++;
+ } else {
+ failed++;
+ console.error(`<U+274C> ${file}: ${name}: ${(e && e.message || e).trim()}\n`);
+ }
});
await Promise.all(Object.keys(tests).map(runTest));
- return { passed, failed };
+ return { passed, failed, skipped };
}
@@ -102,2 +109,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)) throw SKIP;
+
+ // 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 output matches the
+ // unminified test output for this version of NodeJS - whether valid
+ // or an error."
+ if (test.expect_stdout === true) throw SKIP;
+
// Ignore tests that no longer pass in modern versions of node. These tests
@@ -110,3 +129,3 @@ async function test_case(esbuild, test) {
console.error("*** skipping test %j with node_version %j", test.name, test.node_version);
- return;
+ throw SKIP;
}
@@ -117,17 +136,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 }) => { |
If this line in the latest patch is commented out:
there will be false failures in some tests, but more tests will be run:
Additional code may be added to selectively skip certain tests. |
Some of the tests look to me like they are actually encoding a bug in V8 (specifically mishoo/UglifyJS#4269). I have filed the bug here: https://bugs.chromium.org/p/v8/issues/detail?id=11810. I could be wrong of course but I believe properties should be defined in source order. If the behavior of the input code is buggy in a certain implementation, then I don't think esbuild necessarily has to avoid optimizations that might accidentally generate the correct output. Either that or esbuild should automatically apply a workaround for these bugs if the target environment includes a V8-based VM. Basically I think esbuild should either have a garbage-in-garbage-out philosophy or should always generate the correct output, but it shouldn't bake in implementation bugs into its optimization passes. |
uglify-js employs a garbage-in/garbage-out philosophy for the most part. When possible it will try to agree with NodeJS to prevent unwanted noise in its automated test case generating fuzzer. It has encountered and/or exposed a few V8 bugs. |
Some other cases test TDZ handling. I'm not currently preserving TDZ checks because doing so inhibits a lot of optimizations, both at compile-time and run-time. I suspect TDZ checks are rarely relied upon in real-world code but that disabling optimizations which assume local variable references have no side effects would be a very unpopular change (it would likely disable most tree-shaking done by the bundler). Preserving top-level TDZ checks also has extremely negative performance consequences. So I'm inclined to skip over failures due to TDZ checks, although I could potentially be convinced otherwise. |
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/. |
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
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 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:
|
Some of the features in ECMAScript are just bonkers or seldom used. And TDZ is the bane of any fuzzer implementor trying to generate valid random programs that don't throw an error. But if a decision is made to have esbuild deviate from the ES specification or not support some of its features by design then I think it ought to be documented somewhere. At the very least it might start a discussion to deprecate some less than useful ES features. |
let/const/class are not fringe features of ES6 - they are central to the language and ought to be implemented efficiently as Chrome/V8 and Mozilla have. In the case of Safari/JavaScriptCore the result was not incorrect - just slow for large bundles. The way I see it, when performance issues in a particular JS engine are papered over by minifiers and bundlers it provides less of an incentive for the vendor or project to push out fixes in a timely manner. Look at IE8 as an example of an entire developer ecosystem extending the life of an obsolete browser a decade longer than it ought to have lasted. |
Another issue is that I'm relying on lack of TDZ to implement lazily-initialized modules for correct ordering of dynamically-loaded modules (both // entry.mjs
console.log('before')
await import('./a.mjs')
console.log('after') // a.mjs
import { b } from './b.mjs'
export let a = b // b.mjs
import { a } from './a.mjs'
export let b = a Evaluating that code should throw an error, and does when you run
However, it doesn't when bundled with esbuild (via var __defProp = Object.defineProperty;
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[Object.keys(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
// b.mjs
var b;
var init_b = __esm({
"b.mjs"() {
init_a();
b = a;
}
});
// a.mjs
var a_exports = {};
__export(a_exports, {
a: () => a
});
var a;
var init_a = __esm({
"a.mjs"() {
init_b();
a = b;
}
});
// entry.mjs
console.log("before");
await Promise.resolve().then(() => (init_a(), a_exports));
console.log("after"); Even exchanging I assume enforcing TDZ checks like this would be an unpopular choice (I certainly wouldn't want this behavior for my code in production) but I can't think of another way to do it. Or, well, you could always refuse to bundle the code and generate individual files instead, but that sort of defeats the point of a bundler. |
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. |
fwiw, if the multi-module TDZ example above is run through rollup it does preserve NodeJS's TDZ semantics:
rollup can also generate multiple output chunks using:
But I have no idea how to get it to produce However, this non-top-level-await entry point will work:
|
Finally cracked the code to get rollup to work with NodeJS TLA...
|
@evanw By the way, is Rollup's approach to TDZ semantics preservation as seen above something you would consider to support in esbuild? It doesn't appear to impose any significant code size nor run-time speed cost. |
The
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 Also it doesn't look like Rollup treats TDZ checks as a side effect. For example bundling |
My bad. I mistakenly assumed that the remaining test failures were there by design. No one truly cares about using
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 Edit: terser does not reliably respect TDZ side effects:
uglify-js does however:
|
I'm going to close this issue because it's a moving target with uglify-js adding a dozen or so new test cases per week. Feel free to advance the uglify-js commit hash and retest esbuild from time to time at your convenience. On a related note, Rollup has just started to retain some of the more common TDZ violations in code, although not as comprehensively as uglify-js. |
Using node v16.1.0 with the patch below, esbuild e76b49f produces:
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.
The text was updated successfully, but these errors were encountered: