Skip to content

Commit 234940f

Browse files
committed
Support ES6 module syntax in Terser and Closure
Let's see if this passes the CI. (this commit should probably be split into its own PR)
1 parent 79c2fef commit 234940f

File tree

15 files changed

+239
-107
lines changed

15 files changed

+239
-107
lines changed

emcc.py

Lines changed: 54 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3094,10 +3094,9 @@ def phase_final_emitting(options, state, target, wasm_target, memfile):
30943094
generated_text_files_with_native_eols = []
30953095

30963096
if settings.MODULARIZE:
3097-
modularize()
3098-
3099-
if settings.USE_CLOSURE_COMPILER:
3100-
module_export_name_substitution()
3097+
final_js = post_modularize(final_js)
3098+
elif settings.USE_CLOSURE_COMPILER:
3099+
final_js = module_export_name_substitution(final_js)
31013100

31023101
# Run a final optimization pass to clean up items that were not possible to
31033102
# optimize by Closure, or unoptimalities that were left behind by processing
@@ -3109,18 +3108,6 @@ def phase_final_emitting(options, state, target, wasm_target, memfile):
31093108
# mode)
31103109
final_js = building.closure_compiler(final_js, pretty=False, advanced=False, extra_closure_args=options.closure_args)
31113110

3112-
# Unmangle previously mangled `import.meta` and `await import` references in
3113-
# both main code and libraries.
3114-
# See also: `preprocess` in parseTools.js.
3115-
if settings.EXPORT_ES6 and settings.USE_ES6_IMPORT_META:
3116-
src = read_file(final_js)
3117-
final_js += '.esmeta.js'
3118-
write_file(final_js, src
3119-
.replace('EMSCRIPTEN$IMPORT$META', 'import.meta')
3120-
.replace('EMSCRIPTEN$AWAIT$IMPORT', 'await import'))
3121-
shared.get_temp_files().note(final_js)
3122-
save_intermediate('es6-module')
3123-
31243111
# Apply pre and postjs files
31253112
if options.extern_pre_js or options.extern_post_js:
31263113
logger.debug('applying extern pre/postjses')
@@ -3566,6 +3553,11 @@ def phase_binaryen(target, options, wasm_target):
35663553
# after generating the wasm, do some final operations
35673554

35683555
if final_js:
3556+
# Note: this needs to be run before the other acorn-optimizer.js passes,
3557+
# to safely use the 'await' keyword when doing a multi-environment build.
3558+
if settings.MODULARIZE:
3559+
final_js = pre_modularize(final_js)
3560+
35693561
if settings.SUPPORT_BIG_ENDIAN:
35703562
final_js = building.little_endian_heap(final_js)
35713563

@@ -3706,10 +3698,9 @@ def node_es6_imports():
37063698
'''
37073699

37083700

3709-
def modularize():
3710-
global final_js
3711-
logger.debug(f'Modularizing, assigning to var {settings.EXPORT_NAME}')
3712-
src = read_file(final_js)
3701+
def pre_modularize(js_file):
3702+
logger.debug(f'Pre-modularizing, assigning to var {settings.EXPORT_NAME}')
3703+
src = read_file(js_file)
37133704

37143705
# Multi-environment ES6 builds require an async function
37153706
async_emit = ''
@@ -3718,16 +3709,14 @@ def modularize():
37183709
shared.target_environment_may_be('web'):
37193710
async_emit = 'async '
37203711

3721-
return_value = settings.EXPORT_NAME
3712+
return_value = 'Module'
37223713
if settings.WASM_ASYNC_COMPILATION:
37233714
return_value += '.ready'
37243715
if not settings.EXPORT_READY_PROMISE:
37253716
return_value = '{}'
37263717

37273718
src = '''
37283719
%(maybe_async)sfunction(%(EXPORT_NAME)s) {
3729-
%(EXPORT_NAME)s = %(EXPORT_NAME)s || {};
3730-
37313720
%(src)s
37323721
37333722
return %(return_value)s
@@ -3755,21 +3744,53 @@ def modularize():
37553744
script_url = "typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined"
37563745
if shared.target_environment_may_be('node'):
37573746
script_url_node = "if (typeof __filename !== 'undefined') _scriptDir = _scriptDir || __filename;"
3758-
src = '''%(node_imports)s
3747+
src = '''
37593748
var %(EXPORT_NAME)s = (() => {
37603749
var _scriptDir = %(script_url)s;
37613750
%(script_url_node)s
37623751
return (%(src)s);
37633752
})();
37643753
''' % {
3765-
'node_imports': node_es6_imports(),
37663754
'EXPORT_NAME': settings.EXPORT_NAME,
37673755
'script_url': script_url,
37683756
'script_url_node': script_url_node,
37693757
'src': src
37703758
}
37713759

3772-
final_js += '.modular.js'
3760+
final_js = js_file + '.pre-modular.js'
3761+
with open(final_js, 'w', encoding='utf-8') as f:
3762+
f.write(src)
3763+
3764+
# Ensure Closure compiler is aware of that
3765+
# settings.EXPORT_NAME needs to be exported.
3766+
# https://stackoverflow.com/questions/46092308
3767+
if settings.USE_CLOSURE_COMPILER:
3768+
f.write('window["%(EXPORT_NAME)s"] = %(EXPORT_NAME)s;' % {
3769+
'EXPORT_NAME': settings.EXPORT_NAME
3770+
})
3771+
3772+
shared.get_temp_files().note(final_js)
3773+
save_intermediate('pre-modularized')
3774+
return final_js
3775+
3776+
3777+
def post_modularize(js_file):
3778+
src = read_file(js_file)
3779+
3780+
# Replace variants of `window.Module` with `var Module`
3781+
# and remove the line containing `export{};`
3782+
# See also: `pre_modularize`
3783+
if settings.USE_CLOSURE_COMPILER:
3784+
src = src.replace('window.%s' % settings.EXPORT_NAME,
3785+
'var %s' % settings.EXPORT_NAME)
3786+
src = src.replace('window["%s"]' % settings.EXPORT_NAME,
3787+
'var %s' % settings.EXPORT_NAME)
3788+
src = src.replace('export{};\n', '')
3789+
3790+
# Prepend static import declarations, if any
3791+
src = node_es6_imports() + src
3792+
3793+
final_js = js_file + '.post-modular.js'
37733794
with open(final_js, 'w', encoding='utf-8') as f:
37743795
f.write(src)
37753796

@@ -3788,14 +3809,14 @@ def modularize():
37883809
''' % {'EXPORT_NAME': settings.EXPORT_NAME})
37893810

37903811
shared.get_temp_files().note(final_js)
3791-
save_intermediate('modularized')
3812+
save_intermediate('post-modularized')
3813+
return final_js
37923814

37933815

3794-
def module_export_name_substitution():
3795-
global final_js
3816+
def module_export_name_substitution(js_file):
37963817
logger.debug(f'Private module export name substitution with {settings.EXPORT_NAME}')
3797-
src = read_file(final_js)
3798-
final_js += '.module_export_name_substitution.js'
3818+
src = read_file(js_file)
3819+
final_js = js_file + '.module_export_name_substitution.js'
37993820
if settings.MINIMAL_RUNTIME and not settings.ENVIRONMENT_MAY_BE_NODE and not settings.ENVIRONMENT_MAY_BE_SHELL:
38003821
# On the web, with MINIMAL_RUNTIME, the Module object is always provided
38013822
# via the shell html in order to provide the .asm.js/.wasm content.
@@ -3804,8 +3825,10 @@ def module_export_name_substitution():
38043825
replacement = "typeof %(EXPORT_NAME)s !== 'undefined' ? %(EXPORT_NAME)s : {}" % {"EXPORT_NAME": settings.EXPORT_NAME}
38053826
src = re.sub(r'{\s*[\'"]?__EMSCRIPTEN_PRIVATE_MODULE_EXPORT_NAME_SUBSTITUTION__[\'"]?:\s*1\s*}', replacement, src)
38063827
write_file(final_js, src)
3828+
38073829
shared.get_temp_files().note(final_js)
38083830
save_intermediate('module_export_name_substitution')
3831+
return final_js
38093832

38103833

38113834
def generate_traditional_runtime_html(target, options, js_target, target_basename,

emscripten.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,12 @@ def compile_settings():
203203
cwd=path_from_root('src'), env=env, encoding='utf-8')
204204
assert '//FORWARDED_DATA:' in out, 'Did not receive forwarded data in pre output - process failed?'
205205
glue, forwarded_data = out.split('//FORWARDED_DATA:')
206+
207+
# Unmangle previously mangled `import.meta` references in library_*.js.
208+
# See also: `LibraryManager.load` in modules.js.
209+
if settings.EXPORT_ES6 and settings.USE_ES6_IMPORT_META:
210+
glue = glue.replace('EMSCRIPTEN$IMPORT$META', 'import.meta')
211+
206212
return glue, forwarded_data
207213

208214

src/closure-externs/closure-externs.js

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,7 @@
1111
* The closure_compiler() method in tools/shared.py refers to this file when calling closure.
1212
*/
1313

14-
// Special placeholder for `import.meta` and `await import`.
15-
var EMSCRIPTEN$IMPORT$META;
16-
var EMSCRIPTEN$AWAIT$IMPORT;
17-
18-
// Don't minify createRequire
14+
// Don't minify createRequire (i.e. when doing -sEXPORT_ES6 + -sENVIRONMENT=node)
1915
var createRequire;
2016

2117
// Closure externs used by library_sockfs.js
@@ -213,14 +209,6 @@ var close;
213209
*/
214210
var ENVIRONMENT_IS_FETCH_WORKER;
215211

216-
// Due to the way MODULARIZE works, Closure is run on generated code that does not define _scriptDir,
217-
// but only after MODULARIZE has finished, _scriptDir is injected to the generated code.
218-
// Therefore it cannot be minified.
219-
/**
220-
* @suppress {duplicate, undefinedVars}
221-
*/
222-
var _scriptDir;
223-
224212
// Closure run on asm.js uses a hack to execute only on shell code, declare externs needed for it.
225213
/**
226214
* @suppress {undefinedVars}

src/modules.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,13 @@ global.LibraryManager = {
212212
}
213213
try {
214214
processed = processMacros(preprocess(src, filename));
215+
if (EXPORT_ES6 && USE_ES6_IMPORT_META) {
216+
// `eval` doesn't support module syntax; to allow it, we need to temporarily
217+
// replace `import.meta` usages with placeholders during the JS compile phase,
218+
// then in Python we reverse this replacement.
219+
// See also: `compile_settings` in emscripten.py.
220+
processed = processed.replace(/\bimport\.meta\b/g, 'EMSCRIPTEN$IMPORT$META')
221+
}
215222
eval(processed);
216223
} catch (e) {
217224
const details = [e, e.lineNumber ? `line number: ${e.lineNumber}` : ''];

src/parseTools.js

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,6 @@ function processMacros(text) {
3535
// Param filenameHint can be passed as a description to identify the file that is being processed, used
3636
// to locate errors for reporting and for html files to stop expansion between <style> and </style>.
3737
function preprocess(text, filenameHint) {
38-
if (EXPORT_ES6 && USE_ES6_IMPORT_META) {
39-
// `eval`, Terser and Closure don't support module syntax; to allow it,
40-
// we need to temporarily replace `import.meta` and `await import` usages
41-
// with placeholders during preprocess phase, and back after all the other ops.
42-
// See also: `phase_final_emitting` in emcc.py.
43-
text = text
44-
.replace(/\bimport\.meta\b/g, 'EMSCRIPTEN$IMPORT$META')
45-
.replace(/\bawait import\b/g, 'EMSCRIPTEN$AWAIT$IMPORT');
46-
}
47-
4838
const IGNORE = 0;
4939
const SHOW = 1;
5040
// This state is entered after we have shown one of the block of an if/elif/else sequence.

src/shell.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
// after the generated code, you will need to define var Module = {};
2222
// before the code. Then that object will be used in the code, and you
2323
// can continue to use Module afterwards as well.
24-
#if USE_CLOSURE_COMPILER
24+
#if USE_CLOSURE_COMPILER && !MODULARIZE
2525
// if (!Module)` is crucial for Closure Compiler here as it will otherwise replace every `Module` occurrence with a string
2626
var /** @type {{
2727
noImageDecoding: boolean,
@@ -36,8 +36,12 @@ var /** @type {{
3636
*/ Module;
3737
if (!Module) /** @suppress{checkTypes}*/Module = {"__EMSCRIPTEN_PRIVATE_MODULE_EXPORT_NAME_SUBSTITUTION__":1};
3838
#else
39-
var Module = typeof {{{ EXPORT_NAME }}} != 'undefined' ? {{{ EXPORT_NAME }}} : {};
40-
#endif // USE_CLOSURE_COMPILER
39+
// Under `MODULARIZE`, avoid re-declaring the Module variable it's identical to EXPORT_NAME
40+
#if !MODULARIZE || EXPORT_NAME != 'Module'
41+
var
42+
#endif
43+
Module = typeof {{{ EXPORT_NAME }}} != 'undefined' ? {{{ EXPORT_NAME }}} : {};
44+
#endif // USE_CLOSURE_COMPILER && !MODULARIZE
4145

4246
#if POLYFILL
4347
#if ((MAYBE_WASM2JS && WASM != 2) || MODULARIZE) && (MIN_CHROME_VERSION < 33 || MIN_EDGE_VERSION < 12 || MIN_FIREFOX_VERSION < 29 || MIN_IE_VERSION != TARGET_NOT_SUPPORTED || MIN_SAFARI_VERSION < 80000) // https://caniuse.com/#feat=promises
@@ -195,7 +199,6 @@ if (ENVIRONMENT_IS_NODE) {
195199
// TODO: Swap all `require()`'s with `import()`'s?
196200
#if EXPORT_ES6 && ENVIRONMENT_MAY_BE_WEB
197201
const { createRequire } = await import('module');
198-
/** @suppress{duplicate} */
199202
var require = createRequire(import.meta.url);
200203
#endif
201204
// These modules will usually be used on Node.js. Load them eagerly to avoid

src/shell_minimal.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,25 @@
44
* SPDX-License-Identifier: MIT
55
*/
66

7-
#if USE_CLOSURE_COMPILER
7+
#if USE_CLOSURE_COMPILER && !MODULARIZE
88
// if (!Module)` is crucial for Closure Compiler here as it will
99
// otherwise replace every `Module` occurrence with the object below
1010
var /** @type{Object} */ Module;
1111
if (!Module) /** @suppress{checkTypes}*/Module = {"__EMSCRIPTEN_PRIVATE_MODULE_EXPORT_NAME_SUBSTITUTION__":1};
12-
#elif ENVIRONMENT_MAY_BE_NODE || ENVIRONMENT_MAY_BE_SHELL
12+
#else
13+
// Under `MODULARIZE`, avoid re-declaring the Module variable it's identical to EXPORT_NAME
14+
#if !MODULARIZE || EXPORT_NAME != 'Module'
15+
var
16+
#endif
1317
// When running on the web we expect Module to be defined externally, in the
1418
// HTML. Otherwise we must define it here before its first use
15-
var Module = typeof {{{ EXPORT_NAME }}} != 'undefined' ? {{{ EXPORT_NAME }}} : {};
19+
Module =
20+
#if ENVIRONMENT_MAY_BE_NODE || ENVIRONMENT_MAY_BE_SHELL
21+
typeof {{{ EXPORT_NAME }}} != 'undefined' ? {{{ EXPORT_NAME }}} : {};
1622
#else
17-
var Module = {{{ EXPORT_NAME }}};
18-
#endif // USE_CLOSURE_COMPILER
23+
{{{ EXPORT_NAME }}};
24+
#endif // ENVIRONMENT_MAY_BE_NODE || ENVIRONMENT_MAY_BE_SHELL
25+
#endif // USE_CLOSURE_COMPILER && !MODULARIZE
1926

2027
#if MODULARIZE && EXPORT_READY_PROMISE
2128
// Set up the promise that indicates the Module is initialized

test/export_module.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,2 @@
1-
function doNothing() {
2-
return false;
3-
}
4-
5-
export {doNothing};
1+
const doNothing = () => false;
2+
export default doNothing;

test/test_other.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -241,14 +241,31 @@ def test_emcc_generate_config(self):
241241
@parameterized({
242242
'': ([],),
243243
'node': (['-sENVIRONMENT=node'],),
244+
'optimize': (['-O3'],), # i.e., OPT_LEVEL >= 2, minified with tools/acorn-optimizer.js
244245
})
245246
def test_emcc_output_mjs(self, args):
247+
optimized = '-O3' in args
246248
create_file('extern-post.js', 'await Module();')
247249
self.run_process([EMCC, '-o', 'hello_world.mjs',
248250
'--extern-post-js', 'extern-post.js',
249251
test_file('hello_world.c')] + args)
250252
src = read_file('hello_world.mjs')
251253
self.assertContained('export default Module;', src)
254+
self.assertContainedIf("new URL('hello_world.wasm', import.meta.url)", src, not optimized)
255+
self.assertContainedIf('new URL("hello_world.wasm",import.meta.url)', src, optimized)
256+
self.assertContained('hello, world!', self.run_js('hello_world.mjs'))
257+
258+
@parameterized({
259+
'': ([],),
260+
'node': (['-sENVIRONMENT=node'],),
261+
})
262+
def test_emcc_output_mjs_closure(self, args):
263+
create_file('extern-post.js', 'await Module();')
264+
self.run_process([EMCC, '-o', 'hello_world.mjs',
265+
'--extern-post-js', 'extern-post.js',
266+
test_file('hello_world.c'), '--closure=1'] + args)
267+
src = read_file('hello_world.mjs')
268+
self.assertContained('new URL("hello_world.wasm", import.meta.url)', src)
252269
self.assertContained('hello, world!', self.run_js('hello_world.mjs'))
253270

254271
@parameterized({
@@ -282,15 +299,6 @@ def test_emcc_output_worker_mjs_single_file(self):
282299
self.assertContained("new Worker(new URL('hello_world.worker.js', import.meta.url))", src)
283300
self.assertContained('hello, world!', self.run_js('hello_world.mjs'))
284301

285-
def test_emcc_output_mjs_closure(self):
286-
create_file('extern-post.js', 'await Module();')
287-
self.run_process([EMCC, '-o', 'hello_world.mjs',
288-
'--extern-post-js', 'extern-post.js',
289-
test_file('hello_world.c'), '--closure=1'])
290-
src = read_file('hello_world.mjs')
291-
self.assertContained('new URL("hello_world.wasm", import.meta.url)', src)
292-
self.assertContained('hello, world!', self.run_js('hello_world.mjs'))
293-
294302
def test_emcc_output_mjs_web_no_import_meta(self):
295303
# Ensure we don't emit import.meta.url at all for:
296304
# ENVIRONMENT=web + EXPORT_ES6 + USE_ES6_IMPORT_META=0
@@ -316,9 +324,20 @@ def test_export_es6_node_requires_import_meta(self):
316324
self.assertContained('EXPORT_ES6 and ENVIRONMENT=*node* requires USE_ES6_IMPORT_META to be set', err)
317325

318326
def test_export_es6_allows_export_in_post_js(self):
319-
self.run_process([EMCC, test_file('hello_world.c'), '-O3', '-sEXPORT_ES6', '--post-js', test_file('export_module.js')])
327+
self.run_process([EMCC, test_file('hello_world.c'), '-O3', '-sEXPORT_ES6',
328+
'--post-js', test_file('export_module.js')])
320329
src = read_file('a.out.js')
321-
self.assertContained('export{doNothing};', src)
330+
self.assertContained('export default doNothing;', src)
331+
332+
# FIXME: This only works for multi-environment builds.
333+
# Should we always emit an async function for EXPORT_ES6?
334+
def test_export_es6_allows_await_in_pre_js(self):
335+
create_file('pre.js', 'await console.log("Wasm rocks!");')
336+
self.run_process([EMCC, '-o', 'hello_world.mjs', '-O3',
337+
test_file('hello_world.c'),
338+
'--pre-js', 'pre.js'])
339+
src = read_file('hello_world.mjs')
340+
self.assertContained('await console.log("Wasm rocks!");', src)
322341

323342
def test_emcc_out_file(self):
324343
# Verify that "-ofile" works in addition to "-o" "file"

0 commit comments

Comments
 (0)