Skip to content

Commit d648fff

Browse files
author
Bartek Wrona
committed
Add Closure compiler support for ESM dynamic imports
Closure compiler runs before the MODULARIZE async wrapper is applied, so it sees `await import('node:xyz')` outside an async function and fails to parse it. Work around this by: 1. Before Closure: Replace `await import('node:xyz')` with placeholder variables like `__EMSCRIPTEN_PRIVATE_AWAIT_IMPORT_xyz__` 2. Generate externs for the placeholders so Closure doesn't error on undeclared variables 3. After Closure: Restore placeholders to `(await import('node:xyz'))` with parentheses to handle cases where Closure inlines the variable into expressions like `placeholder.method()` This follows the same pattern as the existing `__EMSCRIPTEN_PRIVATE_MODULE_EXPORT_NAME_SUBSTITUTION__` mechanism.
1 parent 1efd494 commit d648fff

File tree

1 file changed

+37
-1
lines changed

1 file changed

+37
-1
lines changed

tools/link.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2322,7 +2322,43 @@ def phase_binaryen(target, options, wasm_target):
23222322

23232323
if options.use_closure_compiler:
23242324
with ToolchainProfiler.profile_block('closure_compile'):
2325-
final_js = building.closure_compiler(final_js, extra_closure_args=settings.CLOSURE_ARGS)
2325+
# In EXPORT_ES6 mode, we use `await import('node:xyz')` for dynamic imports.
2326+
# Closure compiler runs before the MODULARIZE async wrapper is applied, so it
2327+
# sees `await` outside an async function and fails to parse it.
2328+
# Work around this by replacing dynamic imports with placeholders before Closure,
2329+
# then restoring them after.
2330+
closure_args = list(settings.CLOSURE_ARGS) if settings.CLOSURE_ARGS else []
2331+
if settings.EXPORT_ES6:
2332+
src = utils.read_file(final_js)
2333+
# Find all node modules being dynamically imported (with optional bundler hints)
2334+
node_modules = set(re.findall(r"await\s+import\(\s*(?:/\*.*?\*/\s*)*['\"]node:(\w+)['\"]\s*\)", src))
2335+
if node_modules:
2336+
# Replace: await import(/* ... */ 'node:fs') -> __EMSCRIPTEN_PRIVATE_AWAIT_IMPORT_fs__
2337+
src = re.sub(r"await\s+import\(\s*(?:/\*.*?\*/\s*)*['\"]node:(\w+)['\"]\s*\)",
2338+
r'__EMSCRIPTEN_PRIVATE_AWAIT_IMPORT_\1__', src)
2339+
utils.write_file(final_js, src)
2340+
save_intermediate('closure_pre')
2341+
# Generate externs for the placeholder variables so Closure doesn't complain
2342+
externs_content = '\n'.join(
2343+
f'/** @type {{?}} */ var __EMSCRIPTEN_PRIVATE_AWAIT_IMPORT_{mod}__;'
2344+
for mod in node_modules
2345+
)
2346+
externs_file = shared.get_temp_files().get('.js', prefix='node_import_externs_')
2347+
externs_file.write(externs_content.encode())
2348+
externs_file.close()
2349+
closure_args += ['--externs', externs_file.name]
2350+
2351+
final_js = building.closure_compiler(final_js, extra_closure_args=closure_args)
2352+
2353+
if settings.EXPORT_ES6:
2354+
src = utils.read_file(final_js)
2355+
# Restore: __EMSCRIPTEN_PRIVATE_AWAIT_IMPORT_fs__ -> (await import(/* hints */ 'node:fs'))
2356+
# Parentheses are needed because Closure may inline the placeholder into
2357+
# expressions like `placeholder.method()` which needs `(await import(...)).method()`
2358+
src = re.sub(r'__EMSCRIPTEN_PRIVATE_AWAIT_IMPORT_(\w+)__',
2359+
r"(await import(/* webpackIgnore: true */ /* @vite-ignore */ 'node:\1'))", src)
2360+
utils.write_file(final_js, src)
2361+
23262362
save_intermediate('closure')
23272363

23282364
if settings.TRANSPILE:

0 commit comments

Comments
 (0)