diff --git a/ChangeLog.md b/ChangeLog.md index cf60430b844b2..f6804726a9f9d 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -20,6 +20,14 @@ See docs/process.md for more on how version tagging works. 3.0.2 ----- +- Emscripten in starting to use ES6 features in its core libraries (at last!). + For most users targeting the default set of browsers this is a code size win. + For projects targeting older browsers (e.g. `-sMIN_CHROME_VERSION=10`), + emscripten will now run closure compiler in `WHITESPACE_ONLY` mode in order to + traspile any ES6 down to ES5. When this automatic transpilation is performed + we generate a warning which can disabled (using `-Wno-transpile`) or by + explicitly opting in-to or out-of closure using `--closure=1` or + `--closure=0`. (#15763). 3.0.1 - 12/17/2021 ------------------ diff --git a/emcc.py b/emcc.py index 4ffe4b105d632..d402507822f3a 100755 --- a/emcc.py +++ b/emcc.py @@ -1816,6 +1816,24 @@ def default_setting(name, new_default): setup_environment_settings() + if options.use_closure_compiler != 0: + # Emscripten requires certain ES6 constructs by default in library code + # - https://caniuse.com/let : EDGE:12 FF:44 CHROME:49 SAFARI:11 + # - https://caniuse.com/const : EDGE:12 FF:36 CHROME:49 SAFARI:11 + # - https://caniuse.com/arrow-functions: : EDGE:12 FF:22 CHROME:45 SAFARI:10 + # - https://caniuse.com/mdn-javascript_builtins_object_assign: + # EDGE:12 FF:34 CHROME:45 SAFARI:9 + # Taking the highest requirements gives is our minimum: + # Max Version: EDGE:12 FF:44 CHROME:49 SAFARI:11 + settings.TRANSPILE_TO_ES5 = (settings.MIN_EDGE_VERSION < 12 or + settings.MIN_FIREFOX_VERSION < 44 or + settings.MIN_CHROME_VERSION < 49 or + settings.MIN_SAFARI_VERSION < 110000 or + settings.MIN_IE_VERSION != 0x7FFFFFFF) + + if options.use_closure_compiler is None and settings.TRANSPILE_TO_ES5: + diagnostics.warning('transpile', 'enabling transpilation via closure due to browser version settings. This warning can be suppressed by passing `--closure=1` or `--closure=0` to opt into our explicitly.') + # Silently drop any individual backwards compatibility emulation flags that are known never to occur on browsers that support WebAssembly. if not settings.WASM2JS: settings.POLYFILL_OLD_MATH_FUNCTIONS = 0 @@ -3272,15 +3290,14 @@ def phase_binaryen(target, options, wasm_target): def preprocess_wasm2js_script(): return read_and_preprocess(utils.path_from_root('src/wasm2js.js'), expand_macros=True) - def run_closure_compiler(): - global final_js - final_js = building.closure_compiler(final_js, pretty=not minify_whitespace(), - extra_closure_args=options.closure_args) + if final_js and (options.use_closure_compiler or settings.TRANSPILE_TO_ES5): + if options.use_closure_compiler: + final_js = building.closure_compiler(final_js, pretty=not minify_whitespace(), + extra_closure_args=options.closure_args) + else: + final_js = building.closure_transpile(final_js, pretty=not minify_whitespace()) save_intermediate_with_wasm('closure', wasm_target) - if final_js and options.use_closure_compiler: - run_closure_compiler() - symbols_file = None if options.emit_symbol_map: symbols_file = shared.replace_or_append_suffix(target, '.symbols') diff --git a/src/settings_internal.js b/src/settings_internal.js index 178fd66f546de..15544a1643244 100644 --- a/src/settings_internal.js +++ b/src/settings_internal.js @@ -219,3 +219,8 @@ var HAS_MAIN = 0; // Set to true if we are linking as C++ and including C++ stdlibs var LINK_AS_CXX = 0; + +// Set when some minimum browser version triggers doesn't support the +// minimum set of ES6 featurs. This triggers transpilation to ES5 +// using closure compiler. +var TRANSPILE_TO_ES5 = 0; diff --git a/src/shell.js b/src/shell.js index 28924b7517d86..9b0a7a4882af7 100644 --- a/src/shell.js +++ b/src/shell.js @@ -46,7 +46,7 @@ var Module = typeof {{{ EXPORT_NAME }}} !== 'undefined' ? {{{ EXPORT_NAME }}} : // See https://caniuse.com/mdn-javascript_builtins_object_assign #if MIN_CHROME_VERSION < 45 || MIN_EDGE_VERSION < 12 || MIN_FIREFOX_VERSION < 34 || MIN_IE_VERSION != TARGET_NOT_SUPPORTED || MIN_SAFARI_VERSION < 90000 function objAssign(target, source) { - for (key in source) { + for (var key in source) { if (source.hasOwnProperty(key)) { target[key] = source[key]; } diff --git a/tests/test_browser.py b/tests/test_browser.py index e87a796559c0a..99369d16c9f2a 100644 --- a/tests/test_browser.py +++ b/tests/test_browser.py @@ -2601,7 +2601,7 @@ def test_doublestart_bug(self): '': ([],), 'closure': (['-O2', '-g1', '--closure=1', '-s', 'HTML5_SUPPORT_DEFERRING_USER_SENSITIVE_REQUESTS=0'],), 'pthread': (['-s', 'USE_PTHREADS', '-s', 'PROXY_TO_PTHREAD'],), - 'legacy': (['-s', 'MIN_FIREFOX_VERSION=0', '-s', 'MIN_SAFARI_VERSION=0', '-s', 'MIN_IE_VERSION=0', '-s', 'MIN_EDGE_VERSION=0', '-s', 'MIN_CHROME_VERSION=0'],) + 'legacy': (['-s', 'MIN_FIREFOX_VERSION=0', '-s', 'MIN_SAFARI_VERSION=0', '-s', 'MIN_IE_VERSION=0', '-s', 'MIN_EDGE_VERSION=0', '-s', 'MIN_CHROME_VERSION=0', '-Wno-transpile'],) }) @requires_threads def test_html5_core(self, opts): @@ -2666,7 +2666,7 @@ def test_webgl_unmasked_vendor_webgl(self): @requires_graphics_hardware def test_webgl2(self): for opts in [ - ['-s', 'MIN_CHROME_VERSION=0'], + ['-s', 'MIN_CHROME_VERSION=0', '-Wno-transpile'], ['-O2', '-g1', '--closure=1', '-s', 'WORKAROUND_OLD_WEBGL_UNIFORM_UPLOAD_IGNORED_OFFSET_BUG'], ['-s', 'FULL_ES2=1'], ]: diff --git a/tests/test_other.py b/tests/test_other.py index f5a16d98b3507..249622d423ed2 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -11328,3 +11328,69 @@ def test_hello_function(self): # reference them in our docs. Should we move this file to somewhere else such # as `examples/`?) self.run_process([EMCC, test_file('hello_function.cpp'), '-o', 'function.html', '-sEXPORTED_FUNCTIONS=_int_sqrt', '-sEXPORTED_RUNTIME_METHODS=ccall,cwrap']) + + @parameterized({ + '': ([],), + 'O2': (['-O2'],), + }) + def test_es5_transpile(self, args): + self.emcc_args += args + + # Create a library file that uses the following ES6 features + # - let/const + # - arrow funcs + # - for..of + # - object.assign + create_file('es6_library.js', '''\ + mergeInto(LibraryManager.library, { + foo: function() { + // Object.assign + let + let obj = Object.assign({}, {prop:1}); + err('prop: ' + obj.prop); + + // arror funcs + const + const bar = () => 2; + err('bar: ' + bar()); + } + }); + ''') + create_file('test.c', 'extern void foo(); int main() { foo(); }') + self.emcc_args += ['--js-library', 'es6_library.js'] + self.uses_es6 = True + + def check_for_es6(filename, expect): + js = read_file(filename) + if expect: + self.assertContained(['() => 2', '()=>2'], js) + self.assertContained('const ', js) + self.assertContained('let ', js) + else: + self.assertNotContained('() => 2', js) + self.assertNotContained('()=>2', js) + self.assertNotContained('const ', js) + self.assertNotContained('let ', js) + + # Check that under normal circumstances none of these features get + # removed / transpiled. + print('base case') + self.do_runf('test.c', 'prop: 1\nbar: 2\n') + check_for_es6('test.js', True) + + # If we select and older browser than closure will kick in by default + # to traspile. + print('with old browser') + self.emcc_args.remove('-Werror') + self.set_setting('MIN_CHROME_VERSION', '10') + self.do_runf('test.c', 'prop: 1\nbar: 2\n', output_basename='test2') + check_for_es6('test2.js', False) + + # If we add `--closure=0` that traspiler (closure) is not run at all + print('with old browser + --closure=0') + self.do_runf('test.c', 'prop: 1\nbar: 2\n', emcc_args=['--closure=0'], output_basename='test3') + check_for_es6('test3.js', True) + + # If we use `--closure=1` closure will run in full optimization mode + # and also transpile to ES5 + print('with old browser + --closure=1') + self.do_runf('test.c', 'prop: 1\nbar: 2\n', emcc_args=['--closure=1'], output_basename='test4') + check_for_es6('test4.js', False) diff --git a/tools/building.py b/tools/building.py index bd1e6b919ff78..3d75a12f8dab8 100644 --- a/tools/building.py +++ b/tools/building.py @@ -732,6 +732,15 @@ def add_to_path(dirname): return closure_cmd, env +@ToolchainProfiler.profile_block('closure_transpile') +def closure_transpile(filename, pretty): + user_args = [] + closure_cmd, env = get_closure_compiler_and_env(user_args) + closure_cmd += ['--language_out', 'ES5'] + closure_cmd += ['--compilation_level', 'WHITESPACE_ONLY'] + return run_closure_cmd(closure_cmd, filename, env, pretty) + + @ToolchainProfiler.profile_block('closure_compiler') def closure_compiler(filename, pretty, advanced=True, extra_closure_args=None): user_args = [] @@ -794,7 +803,10 @@ def closure_compiler(filename, pretty, advanced=True, extra_closure_args=None): # Tell closure not to do any transpiling or inject any polyfills. # At some point we may want to look into using this as way to convert to ES5 but # babel is perhaps a better tool for that. - args += ['--language_out', 'NO_TRANSPILE'] + if settings.TRANSPILE_TO_ES5: + args += ['--language_out', 'ES5'] + else: + args += ['--language_out', 'NO_TRANSPILE'] # Tell closure never to inject the 'use strict' directive. args += ['--emit_use_strict=false'] @@ -834,7 +846,7 @@ def move_to_safe_7bit_ascii_filename(filename): if pretty: cmd += ['--formatting', 'PRETTY_PRINT'] - logger.debug(f'closure compiler: {shared.shlex_join(cmd)}') + shared.print_compiler_stage(cmd) # Closure compiler does not work if any of the input files contain characters outside the # 7-bit ASCII range. Therefore make sure the command line we pass does not contain any such diff --git a/tools/shared.py b/tools/shared.py index 043f0770abb33..f08c1ab8e792e 100644 --- a/tools/shared.py +++ b/tools/shared.py @@ -71,6 +71,7 @@ diagnostics.add_warning('map-unrecognized-libraries') diagnostics.add_warning('unused-command-line-argument', shared=True) diagnostics.add_warning('pthreads-mem-growth') +diagnostics.add_warning('transpile') # TODO(sbc): Investigate switching to shlex.quote