diff --git a/package-lock.json b/package-lock.json index e6455c330813a..cf38c3943457c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,6 +6,7 @@ "": { "dependencies": { "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.4", "google-closure-compiler": "20260429.0.0", "html-minifier-terser": "7.2.0" }, @@ -1478,10 +1479,10 @@ } }, "node_modules/acorn-import-phases": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.3.tgz", - "integrity": "sha512-jtKLnfoOzm28PazuQ4dVBcE9Jeo6ha1GAJvq3N0LlNOszmTfx+wSycBehn+FN0RnyeR77IBxN/qVYMw0Rlj0Xw==", - "dev": true, + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "license": "MIT", "engines": { "node": ">=10.13.0" }, diff --git a/package.json b/package.json index c509f348df798..8818318ae3ca9 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.4", "google-closure-compiler": "20260429.0.0", "html-minifier-terser": "7.2.0" }, diff --git a/test/js_optimizer/JSDCE-sourcePhaseImports-output.js b/test/js_optimizer/JSDCE-sourcePhaseImports-output.js new file mode 100644 index 0000000000000..6a86f08230f28 --- /dev/null +++ b/test/js_optimizer/JSDCE-sourcePhaseImports-output.js @@ -0,0 +1,9 @@ +import source wasmModule from "./foo.wasm"; + +import source otherModule from "./bar.wasm"; + +function use() { + return [ wasmModule, otherModule ]; +} + +use(); diff --git a/test/js_optimizer/JSDCE-sourcePhaseImports.js b/test/js_optimizer/JSDCE-sourcePhaseImports.js new file mode 100644 index 0000000000000..f7d69267d6526 --- /dev/null +++ b/test/js_optimizer/JSDCE-sourcePhaseImports.js @@ -0,0 +1,11 @@ +// Source-phase imports (https://github.com/tc39/proposal-source-phase-imports). +// The acorn optimizer must parse these via the acorn-import-phases plugin and +// preserve the `source` phase keyword through the terser from_mozilla_ast -> +// print round-trip used at -O2+. +import source wasmModule from './foo.wasm'; +import source otherModule from './bar.wasm'; + +function use() { + return [wasmModule, otherModule]; +} +use(); diff --git a/test/test_other.py b/test/test_other.py index f77aa3e902129..7e91a326c8bf0 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -421,11 +421,15 @@ def test_esm(self, args): self.assertContained('Hello, world!', self.run_js('hello_world.mjs')) @requires_node_25 - def test_esm_source_phase_imports(self): + @parameterized({ + '': ([],), + 'O3': (['-O3'],), + }) + def test_esm_source_phase_imports(self, args): self.node_args += ['--experimental-wasm-modules', '--no-warnings'] self.run_process([EMCC, '-o', 'hello_world.mjs', '-sSOURCE_PHASE_IMPORTS', '--extern-post-js', test_file('modularize_post_js.js'), - test_file('hello_world.c')]) + test_file('hello_world.c')] + args) self.assertContained('import source wasmModule from', read_file('hello_world.mjs')) self.assertContained('Hello, world!', self.run_js('hello_world.mjs')) @@ -3012,6 +3016,7 @@ def test_extern_prepost(self): 'minifyGlobals': (['minifyGlobals'],), 'minifyLocals': (['minifyLocals'],), 'JSDCE': (['JSDCE', '--export-es6'],), + 'JSDCE-sourcePhaseImports': (['JSDCE', '--export-es6'],), 'JSDCE-hasOwnProperty': (['JSDCE'],), 'JSDCE-defaultArg': (['JSDCE'],), 'JSDCE-fors': (['JSDCE'],), diff --git a/third_party/terser/README.md b/third_party/terser/README.md index bae2adf9c6c60..7945ad521ba0d 100644 --- a/third_party/terser/README.md +++ b/third_party/terser/README.md @@ -7,7 +7,7 @@ version published in npm. This `terser.js` bundle in this directory was built from our fork of terser which lives at: https://github.com/emscripten-core/terser/ -The current patches are stored in the `emscripten_patches_v4.8.0` branch. +The current patches are stored in the `emscripten_patches_v5.18.2` branch. To make changes to this code please submit patches to https://github.com/emscripten-core/terser/ and then re-create this bundle diff --git a/third_party/terser/terser.js b/third_party/terser/terser.js index d64f71ea6b4c7..8c85a11a1f7af 100644 --- a/third_party/terser/terser.js +++ b/third_party/terser/terser.js @@ -1,8 +1,8 @@ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : -(global = global || self, factory(global.Terser = {})); -}(this, (function (exports) { 'use strict'; +(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Terser = {})); +})(this, (function (exports) { 'use strict'; /*********************************************************************** @@ -1161,7 +1161,7 @@ function parse($TEXT, options) { // Example: /* I count */ ( /* I don't */ foo() ) // Useful because comments_before property of call with parens outside // contains both comments inside and outside these parens. Used to find the - // right #__PURE__ comments for an expression + const outer_comments_before_counts = new WeakMap(); options = defaults(options, { @@ -2404,7 +2404,7 @@ function parse($TEXT, options) { return new_(allow_calls); } if (is("name", "import") && is_token(peek(), "punc", ".")) { - return import_meta(allow_calls); + return parse_import_expr(allow_calls); } var start = S.token; var peeked; @@ -2864,6 +2864,17 @@ function parse($TEXT, options) { function import_statement() { var start = prev(); + // import source x from "..." + // import defer * as x from "..." + var phase = null; + if (is("name", "source") || is("name", "defer")) { + var peeked = peek(); + if (!is_token(peeked, "name", "from") && !is_token(peeked, "punc", ",")) { + phase = S.token.value; + next(); + } + } + var imported_name; var imported_names; if (is("name")) { @@ -2898,14 +2909,33 @@ function parse($TEXT, options) { end: mod_str, }), assert_clause, + phase, end: S.token, }); } - function import_meta(allow_calls) { + // import.meta + // import.source("module") + // import.defer("module") + function parse_import_expr(allow_calls) { var start = S.token; expect_token("name", "import"); expect_token("punc", "."); + if (is("name", "source") || is("name", "defer")) { + var phase = S.token.value; + next(); + if (!is("punc", "(")) { + croak("'import." + phase + "' can only be used in a dynamic import"); + } + next(); + var args = expr_list(")"); + return subscripts(new AST_DynamicImport({ + start: start, + phase: phase, + args: args, + end: prev() + }), allow_calls); + } expect_token("name", "meta"); return subscripts(new AST_ImportMeta({ start: start, @@ -5071,9 +5101,10 @@ var AST_NameMapping = DEFNODE("NameMapping", "foreign_name name", function AST_N var AST_Import = DEFNODE( "Import", - "imported_name imported_names module_name assert_clause", + "phase imported_name imported_names module_name assert_clause", function AST_Import(props) { if (props) { + this.phase = props.phase; this.imported_name = props.imported_name; this.imported_names = props.imported_names; this.module_name = props.module_name; @@ -5087,6 +5118,7 @@ var AST_Import = DEFNODE( { $documentation: "An `import` statement", $propdoc: { + phase: "[string?] Phase keyword: 'source', 'defer', or null.", imported_name: "[AST_SymbolImport] The name of the variable holding the module's default export.", imported_names: "[AST_NameMapping*] The names of non-default imported variables", module_name: "[AST_String] String literal describing where this module came from", @@ -5127,6 +5159,40 @@ var AST_ImportMeta = DEFNODE("ImportMeta", null, function AST_ImportMeta(props) $documentation: "A reference to import.meta", }); +var AST_DynamicImport = DEFNODE( + "DynamicImport", + "phase args", + function AST_DynamicImport(props) { + if (props) { + this.phase = props.phase; + this.args = props.args; + this.start = props.start; + this.end = props.end; + } + + this.flags = 0; + }, + { + $documentation: "A phased dynamic import expression: `import.source(specifier [, options])` or `import.defer(specifier [, options])`. Plain `import(x)` continues to be parsed as an AST_Call with a synthetic `import` SymbolRef callee.", + $propdoc: { + phase: "[string] Phase keyword ('source' or 'defer').", + args: "[AST_Node*] specifier followed by optional options argument" + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + var args = this.args; + for (var i = 0, len = args.length; i < len; i++) { + args[i]._walk(visitor); + } + }); + }, + _children_backwards(push) { + let i = this.args.length; + while (i--) push(this.args[i]); + }, + } +); + var AST_Export = DEFNODE( "Export", "exported_definition exported_value is_default exported_names module_name assert_clause", @@ -6485,7 +6551,7 @@ var AST_Null = DEFNODE("Null", null, function AST_Null(props) { value: null }, AST_Atom); -var AST_NaN = DEFNODE("NaN", null, function AST_NaN(props) { +DEFNODE("NaN", null, function AST_NaN(props) { if (props) { this.start = props.start; this.end = props.end; @@ -6497,7 +6563,7 @@ var AST_NaN = DEFNODE("NaN", null, function AST_NaN(props) { value: 0/0 }, AST_Atom); -var AST_Undefined = DEFNODE("Undefined", null, function AST_Undefined(props) { +DEFNODE("Undefined", null, function AST_Undefined(props) { if (props) { this.start = props.start; this.end = props.end; @@ -6521,7 +6587,7 @@ var AST_Hole = DEFNODE("Hole", null, function AST_Hole(props) { value: (function() {}()) }, AST_Atom); -var AST_Infinity = DEFNODE("Infinity", null, function AST_Infinity(props) { +DEFNODE("Infinity", null, function AST_Infinity(props) { if (props) { this.start = props.start; this.end = props.end; @@ -6706,7 +6772,7 @@ const _NOINLINE = 0b00000100; const _KEY = 0b00001000; const _MANGLEPROP = 0b00010000; -// XXX Emscripten: export TreeWalker for walking through AST in acorn-optimizer.mjs. +// XXX Emscripten: export TreeWalker for walking through AST in acorn-optimizer.js. exports.TreeWalker = TreeWalker; /*********************************************************************** @@ -7449,7 +7515,8 @@ def_transform(AST_PrefixedTemplateString, function(self, tw) { imported_name: imported_name, imported_names : imported_names, module_name : from_moz(M.source), - assert_clause: assert_clause_from_moz(M.assertions) + assert_clause: assert_clause_from_moz(M.assertions), + phase: M.phase || null }); }, @@ -7475,6 +7542,31 @@ def_transform(AST_PrefixedTemplateString, function(self, tw) { }); }, + ImportExpression: function(M) { + const args = [from_moz(M.source)]; + if (M.options) { + args.push(from_moz(M.options)); + } + if (M.phase) { + return new AST_DynamicImport({ + start: my_start_token(M), + end: my_end_token(M), + phase: M.phase, + args: args + }); + } + return new AST_Call({ + start: my_start_token(M), + end: my_end_token(M), + expression: from_moz({ + type: "Identifier", + name: "import" + }), + optional: false, + args + }); + }, + ExportAllDeclaration: function(M) { var foreign_name = M.exported == null ? new AST_SymbolExportForeign({ name: "*" }) : @@ -7887,19 +7979,6 @@ def_transform(AST_PrefixedTemplateString, function(self, tw) { }); }, - ImportExpression: function(M) { - let import_token = my_start_token(M); - return new AST_Call({ - start : import_token, - end : my_end_token(M), - expression : new AST_SymbolRef({ - start : import_token, - end : import_token, - name : "import" - }), - args : [from_moz(M.source)] - }); - } }; MOZ_TO_ME.UpdateExpression = @@ -8120,6 +8199,16 @@ def_transform(AST_PrefixedTemplateString, function(self, tw) { }; }); + def_to_moz(AST_DynamicImport, function To_Moz_ImportExpression(M) { + const [source, options] = M.args.map(to_moz); + return { + type: "ImportExpression", + source, + options: options || null, + phase: M.phase + }; + }); + def_to_moz(AST_Toplevel, function To_Moz_Program(M) { return to_moz_scope("Program", M); }); @@ -8347,12 +8436,14 @@ def_transform(AST_PrefixedTemplateString, function(self, tw) { }); } } - return { + var moz = { type: "ImportDeclaration", specifiers: specifiers, source: to_moz(M.module_name), assertions: assert_clause_to_moz(M.assert_clause) }; + if (M.phase) moz.phase = M.phase; + return moz; }); def_to_moz(AST_ImportMeta, function To_Moz_MetaProperty() { @@ -9962,9 +10053,9 @@ function OutputStream(options) { // XXX Emscripten localmod: Add a node type for a parenthesized expression so that we can retain // Closure annotations that need a form "/**annotation*/(expression)" DEFPRINT(AST_ParenthesizedExpression, function(self, output) { - output.print('('); + output.print("("); self.body.print(output); - output.print(')'); + output.print(")"); }); // XXX End Emscripten localmod function print_braced_empty(self, output) { @@ -10398,6 +10489,10 @@ function OutputStream(options) { DEFPRINT(AST_Import, function(self, output) { output.print("import"); output.space(); + if (self.phase) { + output.print(self.phase); + output.space(); + } if (self.imported_name) { self.imported_name.print(output); } @@ -10438,6 +10533,15 @@ function OutputStream(options) { DEFPRINT(AST_ImportMeta, function(self, output) { output.print("import.meta"); }); + DEFPRINT(AST_DynamicImport, function(self, output) { + output.print("import." + self.phase); + output.with_parens(function() { + self.args.forEach(function(arg, i) { + if (i) output.comma(); + arg.print(output); + }); + }); + }); DEFPRINT(AST_NameMapping, function(self, output) { var is_import = output.parent() instanceof AST_Import; @@ -12134,4 +12238,4 @@ const base54 = (() => { exports.AST_Node = AST_Node; exports.AST_Token = AST_Token; -}))); +})); diff --git a/tools/acorn-optimizer.mjs b/tools/acorn-optimizer.mjs index 5fd4c6c098637..e1b5604981450 100755 --- a/tools/acorn-optimizer.mjs +++ b/tools/acorn-optimizer.mjs @@ -1,11 +1,19 @@ #!/usr/bin/env node import * as acorn from 'acorn'; +import importPhases from 'acorn-import-phases'; import * as terser from '../third_party/terser/terser.js'; import * as fs from 'node:fs'; import assert from 'node:assert'; import {parseArgs} from 'node:util'; +// Extend acorn to understand source-phase import syntax +// (`import source foo from './bar.wasm'`) emitted under -sSOURCE_PHASE_IMPORTS. +// The plugin annotates ImportDeclaration nodes with a `phase` field; the +// bundled terser carries this through from_mozilla_ast / to_mozilla_ast and +// emits it back on output. +const parser = acorn.Parser.extend(importPhases()); + // Utilities function read(x) { @@ -1750,7 +1758,7 @@ const registry = { let ast; try { - ast = acorn.parse(input, params); + ast = parser.parse(input, params); for (let pass of passes) { const resolvedPass = registry[pass]; assert(resolvedPass, `unknown optimizer pass: ${pass}`);