Skip to content

Commit a537c17

Browse files
committed
add support for source-phase imports (import source, import.source(...))
Implements the TC39 source-phase imports proposal (https://github.com/tc39/proposal-source-phase-imports) for both the static and dynamic forms: // static import source wasmModule from "./foo.wasm"; import defer * as ns from "./mod.js"; // dynamic const m = await import.source("./foo.wasm"); const m = await import.defer("./mod.js"); The `source` phase tells the host to materialize the module's source representation (e.g. a WebAssembly.Module for .wasm) instead of its exports namespace; `defer` requests a lazily-evaluated namespace. Dropping the phase keyword silently changes runtime semantics, which is what terser was doing on the mozilla-AST round-trip used by tools like Emscripten's acorn-optimizer. Static side: * Parser recognises the contextual `source` / `defer` phase keyword after `import`, with peek-based disambiguation so existing patterns like `import source from "x"` and `import defer, {y} from "x"` keep parsing as default-name imports. * AST_Import gains a `phase` field (null for plain imports). * mozilla-ast from_moz / to_moz carry the phase across, matching the shape produced by acorn-import-phases. * Output prints the phase keyword between `import` and the specifier. * equivalent-to includes phase in shallow_cmp. Dynamic side: * New AST_DynamicImport node used as the callee of an AST_Call representing a phased dynamic import; carries the phase. Plain dynamic `import(x)` continues to use the synthetic AST_SymbolRef("import") callee for back-compat. * Parser dispatches `import.source` / `import.defer` via a new import_phase_call helper; they must be followed by a call, else a parse error is raised, mirroring the proposal. * mozilla-ast: from_moz ImportExpression builds the AST_DynamicImport callee when M.phase is set; to_moz AST_Call emits a phased ImportExpression { phase } when the callee is AST_DynamicImport. * Output prints `import.source` / `import.defer` for the callee. * size and equivalent-to updated. Local bindings stay AST_SymbolImport / AST_SymbolRef, so scope, mangle and drop_unused behave exactly as for ordinary imports. This fork is on terser 4.8.0 which lacks `import.meta` parser support, so the import_phase_call helper handles only `source` / `defer` (the upstream terser PR includes `meta` dispatch too).
1 parent d79671a commit a537c17

7 files changed

Lines changed: 307 additions & 11 deletions

File tree

lib/ast.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -849,12 +849,13 @@ var AST_NameMapping = DEFNODE("NameMapping", "foreign_name name", {
849849
},
850850
});
851851

852-
var AST_Import = DEFNODE("Import", "imported_name imported_names module_name", {
852+
var AST_Import = DEFNODE("Import", "imported_name imported_names module_name phase", {
853853
$documentation: "An `import` statement",
854854
$propdoc: {
855855
imported_name: "[AST_SymbolImport] The name of the variable holding the module's default export.",
856856
imported_names: "[AST_NameMapping*] The names of non-default imported variables",
857857
module_name: "[AST_String] String literal describing where this module came from",
858+
phase: "[string?] Phase keyword for source-phase imports ('source' or 'defer'), or null."
858859
},
859860
_walk: function(visitor) {
860861
return visitor._visit(this, function() {
@@ -879,6 +880,13 @@ var AST_Import = DEFNODE("Import", "imported_name imported_names module_name", {
879880
},
880881
});
881882

883+
var AST_DynamicImport = DEFNODE("DynamicImport", "phase", {
884+
$documentation: "The callee of a dynamic `import(...)` / `import.source(...)` / `import.defer(...)` expression. Always appears as the `expression` of an AST_Call.",
885+
$propdoc: {
886+
phase: "[string?] Phase keyword ('source' or 'defer'), or null for a plain dynamic import."
887+
},
888+
});
889+
882890
var AST_Export = DEFNODE("Export", "exported_definition exported_value is_default exported_names module_name", {
883891
$documentation: "An `export` statement",
884892
$propdoc: {
@@ -1656,6 +1664,7 @@ export {
16561664
AST_Function,
16571665
AST_Hole,
16581666
AST_If,
1667+
AST_DynamicImport,
16591668
AST_Import,
16601669
AST_Infinity,
16611670
AST_IterationStatement,

lib/equivalent-to.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
AST_ForIn,
2626
AST_ForOf,
2727
AST_If,
28+
AST_DynamicImport,
2829
AST_Import,
2930
AST_Jump,
3031
AST_LabeledStatement,
@@ -206,7 +207,12 @@ AST_NameMapping.prototype.shallow_cmp = pass_through;
206207

207208
AST_Import.prototype.shallow_cmp = mkshallow({
208209
imported_name: "exist",
209-
imported_names: "exist"
210+
imported_names: "exist",
211+
phase: "eq",
212+
});
213+
214+
AST_DynamicImport.prototype.shallow_cmp = mkshallow({
215+
phase: "eq",
210216
});
211217

212218
AST_Export.prototype.shallow_cmp = mkshallow({

lib/mozilla-ast.js

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ import {
8888
AST_Function,
8989
AST_Hole,
9090
AST_If,
91+
AST_DynamicImport,
9192
AST_Import,
9293
AST_Label,
9394
AST_LabeledStatement,
@@ -485,9 +486,11 @@ import {
485486
end : my_end_token(M),
486487
imported_name: imported_name,
487488
imported_names : imported_names,
488-
module_name : from_moz(M.source)
489+
module_name : from_moz(M.source),
490+
phase: M.phase || null
489491
});
490492
},
493+
491494
ExportAllDeclaration: function(M) {
492495
return new AST_Export({
493496
start: my_start_token(M),
@@ -603,14 +606,23 @@ import {
603606
},
604607
ImportExpression: function(M) {
605608
let import_token = my_start_token(M);
609+
// Source-phase imports proposal: `import.source(x)` / `import.defer(x)`
610+
// arrive as ImportExpression nodes with a `phase` field (per
611+
// acorn-import-phases). Plain dynamic imports continue to use a
612+
// synthetic `import` SymbolRef callee for back-compat.
613+
var expression = M.phase ? new AST_DynamicImport({
614+
start: import_token,
615+
end: my_end_token(M),
616+
phase: M.phase,
617+
}) : new AST_SymbolRef({
618+
start: import_token,
619+
end: import_token,
620+
name: "import",
621+
});
606622
return new AST_Call({
607623
start : import_token,
608624
end : my_end_token(M),
609-
expression : new AST_SymbolRef({
610-
start : import_token,
611-
end : import_token,
612-
name : "import"
613-
}),
625+
expression : expression,
614626
args : [from_moz(M.source)]
615627
});
616628
}
@@ -669,6 +681,28 @@ import {
669681
map("NewExpression", AST_New, "callee>expression, arguments@args");
670682
map("CallExpression", AST_Call, "callee>expression, arguments@args");
671683

684+
// AST_Call normally maps to CallExpression (registered above), but a
685+
// phased dynamic import — `import.source(x)` / `import.defer(x)` — round-
686+
// trips through Mozilla AST as an ImportExpression with `phase`, matching
687+
// acorn-import-phases' shape.
688+
def_to_moz(AST_Call, function To_Moz_CallOrImportExpression(M) {
689+
if (M.expression instanceof AST_DynamicImport) {
690+
var args = M.args.map(to_moz);
691+
var out = {
692+
type: "ImportExpression",
693+
source: args[0],
694+
options: args[1] || null,
695+
};
696+
if (M.expression.phase) out.phase = M.expression.phase;
697+
return out;
698+
}
699+
return {
700+
type: "CallExpression",
701+
callee: to_moz(M.expression),
702+
arguments: M.args.map(to_moz),
703+
};
704+
});
705+
672706
def_to_moz(AST_Toplevel, function To_Moz_Program(M) {
673707
return to_moz_scope("Program", M);
674708
});
@@ -867,11 +901,13 @@ import {
867901
});
868902
});
869903
}
870-
return {
904+
var moz = {
871905
type: "ImportDeclaration",
872906
specifiers: specifiers,
873907
source: to_moz(M.module_name)
874908
};
909+
if (M.phase) moz.phase = M.phase;
910+
return moz;
875911
});
876912

877913
def_to_moz(AST_Sequence, function To_Moz_SequenceExpression(M) {

lib/output.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ import {
9393
AST_Function,
9494
AST_Hole,
9595
AST_If,
96+
AST_DynamicImport,
9697
AST_Import,
9798
AST_Jump,
9899
AST_LabeledStatement,
@@ -1596,6 +1597,10 @@ function OutputStream(options) {
15961597
DEFPRINT(AST_Import, function(self, output) {
15971598
output.print("import");
15981599
output.space();
1600+
if (self.phase) {
1601+
output.print(self.phase);
1602+
output.space();
1603+
}
15991604
if (self.imported_name) {
16001605
self.imported_name.print(output);
16011606
}
@@ -1627,7 +1632,13 @@ function OutputStream(options) {
16271632
self.module_name.print(output);
16281633
output.semicolon();
16291634
});
1630-
1635+
DEFPRINT(AST_DynamicImport, function(self, output) {
1636+
if (self.phase) {
1637+
output.print("import." + self.phase);
1638+
} else {
1639+
output.print("import");
1640+
}
1641+
});
16311642
DEFPRINT(AST_NameMapping, function(self, output) {
16321643
var is_import = output.parent() instanceof AST_Import;
16331644
var definition = self.name.definition();

lib/parse.js

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ import {
9090
AST_Function,
9191
AST_Hole,
9292
AST_If,
93+
AST_DynamicImport,
9394
AST_Import,
9495
AST_IterationStatement,
9596
AST_Label,
@@ -1169,7 +1170,7 @@ function parse($TEXT, options) {
11691170
}
11701171
return function_(AST_Defun, false, true, is_export_default);
11711172
}
1172-
if (S.token.value == "import" && !is_token(peek(), "punc", "(")) {
1173+
if (S.token.value == "import" && !is_token(peek(), "punc", "(") && !is_token(peek(), "punc", ".")) {
11731174
next();
11741175
var node = import_();
11751176
semicolon();
@@ -2217,6 +2218,9 @@ function parse($TEXT, options) {
22172218
if (is("operator", "new")) {
22182219
return new_(allow_calls);
22192220
}
2221+
if (is("name", "import") && is_token(peek(), "punc", ".")) {
2222+
return import_phase_call(allow_calls);
2223+
}
22202224
var start = S.token;
22212225
var peeked;
22222226
var async = is("name", "async")
@@ -2594,6 +2598,21 @@ function parse($TEXT, options) {
25942598

25952599
function import_() {
25962600
var start = prev();
2601+
2602+
// Source-phase imports proposal: `import source NAME from "..."` and
2603+
// `import defer * as NS from "..."`. The phase keyword is a contextual
2604+
// identifier; disambiguate from a legitimate default-import named
2605+
// `source`/`defer` by peeking past it: if the next token is `from` or
2606+
// `,`, the identifier IS the default-imported binding, not a phase.
2607+
var phase = null;
2608+
if (is("name", "source") || is("name", "defer")) {
2609+
var peeked = peek();
2610+
if (!is_token(peeked, "name", "from") && !is_token(peeked, "punc", ",")) {
2611+
phase = S.token.value;
2612+
next();
2613+
}
2614+
}
2615+
25972616
var imported_name;
25982617
var imported_names;
25992618
if (is("name")) {
@@ -2624,10 +2643,33 @@ function parse($TEXT, options) {
26242643
quote: mod_str.quote,
26252644
end: mod_str,
26262645
}),
2646+
phase,
26272647
end: S.token,
26282648
});
26292649
}
26302650

2651+
// Phased dynamic imports: `import.source(...)` / `import.defer(...)`. The
2652+
// proposal restricts these to the callee position of a call expression;
2653+
// `import.meta` is unsupported in this fork.
2654+
function import_phase_call(allow_calls) {
2655+
var start = S.token;
2656+
expect_token("name", "import");
2657+
expect_token("punc", ".");
2658+
if (!is("name", "source") && !is("name", "defer")) {
2659+
unexpected();
2660+
}
2661+
var phase = S.token.value;
2662+
next();
2663+
if (!is("punc", "(")) {
2664+
croak("'import." + phase + "' can only be used in a dynamic import");
2665+
}
2666+
return subscripts(new AST_DynamicImport({
2667+
start: start,
2668+
phase: phase,
2669+
end: prev(),
2670+
}), allow_calls);
2671+
}
2672+
26312673
function map_name(is_import) {
26322674
function make_symbol(type) {
26332675
return new type({

lib/size.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
AST_Function,
3232
AST_Hole,
3333
AST_If,
34+
AST_DynamicImport,
3435
AST_Import,
3536
AST_Infinity,
3637
AST_LabeledStatement,
@@ -257,6 +258,11 @@ AST_Import.prototype._size = function () {
257258
return size;
258259
};
259260

261+
AST_DynamicImport.prototype._size = function () {
262+
// `import` (6) or `import.source` (13) or `import.defer` (12)
263+
return this.phase ? 7 + this.phase.length : 6;
264+
};
265+
260266
AST_Export.prototype._size = function () {
261267
let size = 7 + (this.is_default ? 8 : 0);
262268

0 commit comments

Comments
 (0)