Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
9 changes: 9 additions & 0 deletions test/js_optimizer/sourcePhaseImports-output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import source wasmModule from "./foo.wasm";

import source otherModule from "./bar.wasm";

function use() {
return [ wasmModule, otherModule ];
}

use();
11 changes: 11 additions & 0 deletions test/js_optimizer/sourcePhaseImports.js
Original file line number Diff line number Diff line change
@@ -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();
9 changes: 7 additions & 2 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'))

Expand Down Expand Up @@ -3012,6 +3016,7 @@ def test_extern_prepost(self):
'minifyGlobals': (['minifyGlobals'],),
'minifyLocals': (['minifyLocals'],),
'JSDCE': (['JSDCE', '--export-es6'],),
'sourcePhaseImports': (['JSDCE', '--export-es6'],),
'JSDCE-hasOwnProperty': (['JSDCE'],),
'JSDCE-defaultArg': (['JSDCE'],),
'JSDCE-fors': (['JSDCE'],),
Expand Down
144 changes: 124 additions & 20 deletions third_party/terser/terser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -2864,6 +2864,20 @@ function parse($TEXT, options) {
function import_statement() {
var start = prev();

// Source-phase imports proposal: `import source NAME from "..."` and
// `import defer * as NS from "..."`. The phase keyword is a contextual
// identifier; disambiguate from a legitimate default-import named
// `source`/`defer` by peeking past it: if the next token is `from` or
// `,`, the identifier IS the default-imported binding, not a phase.
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")) {
Expand Down Expand Up @@ -2898,14 +2912,38 @@ function parse($TEXT, options) {
end: mod_str,
}),
assert_clause,
phase,
end: S.token,
});
}

function import_meta(allow_calls) {
// Parses an `import.<property>` expression, after `expr_atom` has
// already confirmed the `import .` lookahead. Handles:
//
// import.meta — meta-property
// import.source(specifier [, options]) — source-phase imports proposal
// import.defer(specifier [, options]) — "
//
// The two source-phase forms (https://github.com/tc39/proposal-source-phase-imports)
// share the `import.NAME` shape with `import.meta` but are an entirely
// different proposal: they must be followed by a call and their result
// is a dynamic import, not a meta-property reference.
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");
}
return subscripts(new AST_DynamicImport({
start: start,
phase: phase,
end: prev()
}), allow_calls);
}
expect_token("name", "meta");
return subscripts(new AST_ImportMeta({
start: start,
Expand Down Expand Up @@ -5071,13 +5109,14 @@ var AST_NameMapping = DEFNODE("NameMapping", "foreign_name name", function AST_N

var AST_Import = DEFNODE(
"Import",
"imported_name imported_names module_name assert_clause",
"imported_name imported_names module_name assert_clause phase",
function AST_Import(props) {
if (props) {
this.imported_name = props.imported_name;
this.imported_names = props.imported_names;
this.module_name = props.module_name;
this.assert_clause = props.assert_clause;
this.phase = props.phase;
this.start = props.start;
this.end = props.end;
}
Expand All @@ -5090,7 +5129,8 @@ var AST_Import = DEFNODE(
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",
assert_clause: "[AST_Object?] The import assertion"
assert_clause: "[AST_Object?] The import assertion",
phase: "[string?] Phase keyword for source-phase imports ('source' or 'defer'), or null."
},
_walk: function(visitor) {
return visitor._visit(this, function() {
Expand Down Expand Up @@ -5127,6 +5167,26 @@ var AST_ImportMeta = DEFNODE("ImportMeta", null, function AST_ImportMeta(props)
$documentation: "A reference to import.meta",
});

var AST_DynamicImport = DEFNODE(
"DynamicImport",
"phase",
function AST_DynamicImport(props) {
if (props) {
this.phase = props.phase;
this.start = props.start;
this.end = props.end;
}

this.flags = 0;
},
{
$documentation: "The callee of a dynamic `import(...)` / `import.source(...)` / `import.defer(...)` expression. Always appears as the `expression` of an AST_Call.",
$propdoc: {
phase: "[string?] Phase keyword ('source' or 'defer'), or null for a plain dynamic import."
}
}
);

var AST_Export = DEFNODE(
"Export",
"exported_definition exported_value is_default exported_names module_name assert_clause",
Expand Down Expand Up @@ -6706,7 +6766,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;

/***********************************************************************
Expand Down Expand Up @@ -7449,7 +7509,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
});
},

Expand All @@ -7475,6 +7536,39 @@ 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));
}
// Source-phase imports proposal (https://github.com/tc39/proposal-source-phase-imports):
// `import.source(x)` and `import.defer(x)` arrive as ImportExpression
// nodes with a `phase` field (per acorn-import-phases). Plain
// dynamic imports continue to use the synthetic `import` SymbolRef
// callee for back-compat with downstream code that already groks
// that pattern.
var expression;
if (M.phase) {
expression = new AST_DynamicImport({
start: my_start_token(M),
end: my_end_token(M),
phase: M.phase
});
} else {
expression = from_moz({
type: "Identifier",
name: "import"
});
}
return new AST_Call({
start: my_start_token(M),
end: my_end_token(M),
expression: expression,
optional: false,
args
});
},

ExportAllDeclaration: function(M) {
var foreign_name = M.exported == null ?
new AST_SymbolExportForeign({ name: "*" }) :
Expand Down Expand Up @@ -7887,19 +7981,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 =
Expand Down Expand Up @@ -8112,6 +8193,16 @@ def_transform(AST_PrefixedTemplateString, function(self, tw) {
};
});
def_to_moz(AST_Call, function To_Moz_CallExpression(M) {
if (M.expression instanceof AST_DynamicImport) {
const [source, options] = M.args.map(to_moz);
const out = {
type: "ImportExpression",
source,
options: options || null
};
if (M.expression.phase) out.phase = M.expression.phase;
return out;
}
return {
type: "CallExpression",
callee: to_moz(M.expression),
Expand Down Expand Up @@ -8347,12 +8438,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() {
Expand Down Expand Up @@ -10398,6 +10491,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);
}
Expand Down Expand Up @@ -10438,6 +10535,13 @@ function OutputStream(options) {
DEFPRINT(AST_ImportMeta, function(self, output) {
output.print("import.meta");
});
DEFPRINT(AST_DynamicImport, function(self, output) {
if (self.phase) {
output.print("import." + self.phase);
} else {
output.print("import");
}
});

DEFPRINT(AST_NameMapping, function(self, output) {
var is_import = output.parent() instanceof AST_Import;
Expand Down
10 changes: 9 additions & 1 deletion tools/acorn-optimizer.mjs
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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}`);
Expand Down
Loading