diff --git a/crates/swc/tests/tsc-references/TwoInternalModulesThatMergeEachWithExportedClassesOfTheSameName.1.normal.js b/crates/swc/tests/tsc-references/TwoInternalModulesThatMergeEachWithExportedClassesOfTheSameName.1.normal.js
index a8b65e98e13a..9d862a22aec7 100644
--- a/crates/swc/tests/tsc-references/TwoInternalModulesThatMergeEachWithExportedClassesOfTheSameName.1.normal.js
+++ b/crates/swc/tests/tsc-references/TwoInternalModulesThatMergeEachWithExportedClassesOfTheSameName.1.normal.js
@@ -1,41 +1,21 @@
//// [TwoInternalModulesThatMergeEachWithExportedClassesOfTheSameName.ts]
-import { _ as _class_call_check } from "@swc/helpers/_/_class_call_check";
-(function(A) {
- var Point = function Point() {
- "use strict";
- _class_call_check(this, Point);
- };
- A.Point = Point;
-})(A || (A = {}));
-(function(A) {
- var Point = function Point() {
- "use strict";
- _class_call_check(this, Point);
- };
- // expected error
- A.Point = Point;
-})(A || (A = {}));
-(function(X) {
- (function(Y) {
- (function(Z) {
- var Line = function Line() {
- "use strict";
- _class_call_check(this, Line);
- };
- Z.Line = Line;
- })(Y.Z || (Y.Z = {}));
- })(X.Y || (X.Y = {}));
-})(X || (X = {}));
-(function(X) {
- (function(Y) {
- (function(Z) {
- var Line = function Line() {
- "use strict";
- _class_call_check(this, Line);
- };
- // expected error
- Z.Line = Line;
- })(Y.Z || (Y.Z = {}));
- })(X.Y || (X.Y = {}));
-})(X || (X = {}));
-var A, X;
+//! x the name `Point` is defined multiple times
+//! ,-[2:1]
+//! 1 | module A {
+//! 2 | export class Point {
+//! : ^^|^^
+//! : `-- previous definition of `Point` here
+//! 3 | x: number;
+//! 4 | y: number;
+//! 5 | }
+//! 6 | }
+//! 7 |
+//! 8 | module A{
+//! 9 | // expected error
+//! 10 | export class Point {
+//! : ^^|^^
+//! : `-- `Point` redefined here
+//! 11 | origin: number;
+//! 12 | angle: number;
+//! 13 | }
+//! `----
diff --git a/crates/swc/tests/tsc-references/TwoInternalModulesThatMergeEachWithExportedClassesOfTheSameName.2.minified.js b/crates/swc/tests/tsc-references/TwoInternalModulesThatMergeEachWithExportedClassesOfTheSameName.2.minified.js
index 026505170e33..9d862a22aec7 100644
--- a/crates/swc/tests/tsc-references/TwoInternalModulesThatMergeEachWithExportedClassesOfTheSameName.2.minified.js
+++ b/crates/swc/tests/tsc-references/TwoInternalModulesThatMergeEachWithExportedClassesOfTheSameName.2.minified.js
@@ -1,12 +1,21 @@
//// [TwoInternalModulesThatMergeEachWithExportedClassesOfTheSameName.ts]
-var A, X, X1, Y, X2, Y1;
-import { _ as _class_call_check } from "@swc/helpers/_/_class_call_check";
-(A || (A = {})).Point = function Point() {
- _class_call_check(this, Point);
-}, (A || (A = {})).Point = function Point() {
- _class_call_check(this, Point);
-}, ((Y = (X1 = X || (X = {})).Y || (X1.Y = {})).Z || (Y.Z = {})).Line = function Line() {
- _class_call_check(this, Line);
-}, ((Y1 = (X2 = X || (X = {})).Y || (X2.Y = {})).Z || (Y1.Z = {})).Line = function Line() {
- _class_call_check(this, Line);
-};
+//! x the name `Point` is defined multiple times
+//! ,-[2:1]
+//! 1 | module A {
+//! 2 | export class Point {
+//! : ^^|^^
+//! : `-- previous definition of `Point` here
+//! 3 | x: number;
+//! 4 | y: number;
+//! 5 | }
+//! 6 | }
+//! 7 |
+//! 8 | module A{
+//! 9 | // expected error
+//! 10 | export class Point {
+//! : ^^|^^
+//! : `-- `Point` redefined here
+//! 11 | origin: number;
+//! 12 | angle: number;
+//! 13 | }
+//! `----
diff --git a/crates/swc/tests/tsc-references/enumMergingErrors.1.normal.js b/crates/swc/tests/tsc-references/enumMergingErrors.1.normal.js
index 5fc005f359f4..a46c920fe089 100644
--- a/crates/swc/tests/tsc-references/enumMergingErrors.1.normal.js
+++ b/crates/swc/tests/tsc-references/enumMergingErrors.1.normal.js
@@ -14,24 +14,24 @@
(function(M) {
(function(E1) {
E1[E1["B"] = 'foo'.length] = "B";
- })(M.E1 || (M.E1 = {}));
+ })(M.E1);
(function(E2) {
E2[E2["B"] = 'foo'.length] = "B";
- })(M.E2 || (M.E2 = {}));
+ })(M.E2);
(function(E3) {
E3[E3["C"] = 0] = "C";
- })(M.E3 || (M.E3 = {}));
+ })(M.E3);
})(M || (M = {}));
(function(M) {
(function(E1) {
E1[E1["C"] = 0] = "C";
- })(M.E1 || (M.E1 = {}));
+ })(M.E1);
(function(E2) {
E2[E2["A"] = 0] = "A";
- })(M.E2 || (M.E2 = {}));
+ })(M.E2);
(function(E3) {
E3[E3["B"] = 'foo'.length] = "B";
- })(M.E3 || (M.E3 = {}));
+ })(M.E3);
})(M || (M = {}));
// Enum with no initializer in either declaration with constant members with the same root module
(function(M1) {
@@ -42,12 +42,12 @@
(function(M1) {
(function(E1) {
E1[E1["B"] = 0] = "B";
- })(M1.E1 || (M1.E1 = {}));
+ })(M1.E1);
})(M1 || (M1 = {}));
(function(M1) {
(function(E1) {
E1[E1["C"] = 0] = "C";
- })(M1.E1 || (M1.E1 = {}));
+ })(M1.E1);
})(M1 || (M1 = {}));
// Enum with initializer in only one of three declarations with constant members with the same root module
(function(M2) {
@@ -58,11 +58,11 @@
(function(M2) {
(function(E1) {
E1[E1["B"] = 0] = "B";
- })(M2.E1 || (M2.E1 = {}));
+ })(M2.E1);
})(M2 || (M2 = {}));
(function(M2) {
(function(E1) {
E1[E1["C"] = 0] = "C";
- })(M2.E1 || (M2.E1 = {}));
+ })(M2.E1);
})(M2 || (M2 = {}));
var M, M1, M2;
diff --git a/crates/swc/tests/tsc-references/enumMergingErrors.2.minified.js b/crates/swc/tests/tsc-references/enumMergingErrors.2.minified.js
index 46ddc84c87fb..995ebdea3ae8 100644
--- a/crates/swc/tests/tsc-references/enumMergingErrors.2.minified.js
+++ b/crates/swc/tests/tsc-references/enumMergingErrors.2.minified.js
@@ -1,3 +1,3 @@
//// [enumMergingErrors.ts]
-var M, M1, M2, M3, E1, E2, E3, M4, E11, E21, E31, M5, E12, E22, E32, M11, E13, M12, E14, M13, E15, M21, E16, M22, E17, M23, E18;
-(E1 = (M3 = M || (M = {})).E1 || (M3.E1 = {}))[E1.A = 0] = "A", (E2 = M3.E2 || (M3.E2 = {}))[E2.C = 0] = "C", (E3 = M3.E3 || (M3.E3 = {}))[E3.A = 0] = "A", (E11 = (M4 = M || (M = {})).E1 || (M4.E1 = {}))[E11.B = 3] = "B", (E21 = M4.E2 || (M4.E2 = {}))[E21.B = 3] = "B", (E31 = M4.E3 || (M4.E3 = {}))[E31.C = 0] = "C", (E12 = (M5 = M || (M = {})).E1 || (M5.E1 = {}))[E12.C = 0] = "C", (E22 = M5.E2 || (M5.E2 = {}))[E22.A = 0] = "A", (E32 = M5.E3 || (M5.E3 = {}))[E32.B = 3] = "B", (E13 = (M11 = M1 || (M1 = {})).E1 || (M11.E1 = {}))[E13.A = 0] = "A", (E14 = (M12 = M1 || (M1 = {})).E1 || (M12.E1 = {}))[E14.B = 0] = "B", (E15 = (M13 = M1 || (M1 = {})).E1 || (M13.E1 = {}))[E15.C = 0] = "C", (E16 = (M21 = M2 || (M2 = {})).E1 || (M21.E1 = {}))[E16.A = 0] = "A", (E17 = (M22 = M2 || (M2 = {})).E1 || (M22.E1 = {}))[E17.B = 0] = "B", (E18 = (M23 = M2 || (M2 = {})).E1 || (M23.E1 = {}))[E18.C = 0] = "C";
+var M, M1, M2, M3, E1, E2, E3, M4, E11, E21, E31, M5, E12, E22, E32, M11, E13, E14, E15, M21, E16, E17, E18;
+(E1 = (M3 = M || (M = {})).E1 || (M3.E1 = {}))[E1.A = 0] = "A", (E2 = M3.E2 || (M3.E2 = {}))[E2.C = 0] = "C", (E3 = M3.E3 || (M3.E3 = {}))[E3.A = 0] = "A", (E11 = (M4 = M || (M = {})).E1)[E11.B = 3] = "B", (E21 = M4.E2)[E21.B = 3] = "B", (E31 = M4.E3)[E31.C = 0] = "C", (E12 = (M5 = M || (M = {})).E1)[E12.C = 0] = "C", (E22 = M5.E2)[E22.A = 0] = "A", (E32 = M5.E3)[E32.B = 3] = "B", (E13 = (M11 = M1 || (M1 = {})).E1 || (M11.E1 = {}))[E13.A = 0] = "A", (E14 = (M1 || (M1 = {})).E1)[E14.B = 0] = "B", (E15 = (M1 || (M1 = {})).E1)[E15.C = 0] = "C", (E16 = (M21 = M2 || (M2 = {})).E1 || (M21.E1 = {}))[E16.A = 0] = "A", (E17 = (M2 || (M2 = {})).E1)[E17.B = 0] = "B", (E18 = (M2 || (M2 = {})).E1)[E18.C = 0] = "C";
diff --git a/crates/swc/tests/tsc-references/neverReturningFunctions1.1.normal.js b/crates/swc/tests/tsc-references/neverReturningFunctions1.1.normal.js
index 4b4ad3fc8413..f50fc0b48d5b 100644
--- a/crates/swc/tests/tsc-references/neverReturningFunctions1.1.normal.js
+++ b/crates/swc/tests/tsc-references/neverReturningFunctions1.1.normal.js
@@ -35,6 +35,7 @@ function f13(x, fail) {
fail();
x; // Unreachable
}
+(function(Debug) {})(Debug || (Debug = {}));
function f21(x) {
if (x === undefined) Debug.fail("undefined argument");
x.length; // string
@@ -214,4 +215,5 @@ function foo(services, s) {
return s;
}
}
+var Debug;
export { };
diff --git a/crates/swc/tests/tsc-references/neverReturningFunctions1.2.minified.js b/crates/swc/tests/tsc-references/neverReturningFunctions1.2.minified.js
index 5261e8d42e1c..196fcb55d1ff 100644
--- a/crates/swc/tests/tsc-references/neverReturningFunctions1.2.minified.js
+++ b/crates/swc/tests/tsc-references/neverReturningFunctions1.2.minified.js
@@ -1,10 +1,11 @@
//// [neverReturningFunctions1.ts]
+var Debug;
import "@swc/helpers/_/_call_super";
import "@swc/helpers/_/_class_call_check";
import "@swc/helpers/_/_get";
import "@swc/helpers/_/_get_prototype_of";
import "@swc/helpers/_/_inherits";
-registerComponent('test-component', {
+Debug || (Debug = {}), registerComponent('test-component', {
schema: {
myProperty: {
default: [],
diff --git a/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModule.1.normal.js b/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModule.1.normal.js
index eeaad3ee1594..af0b69361a93 100644
--- a/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModule.1.normal.js
+++ b/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModule.1.normal.js
@@ -2,10 +2,10 @@
// module export
(function(m) {})(m || (m = {}));
(function(m) {
- var z = x;
+ var z = m.x;
var y = {
- a: x,
- x: x
+ a: m.x,
+ x: m.x
};
})(m || (m = {}));
var m;
diff --git a/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModule.2.minified.js b/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModule.2.minified.js
index 1074b036fe54..7e8fdf1e5c7e 100644
--- a/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModule.2.minified.js
+++ b/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModule.2.minified.js
@@ -1,3 +1,3 @@
//// [objectLiteralShorthandPropertiesWithModule.ts]
-var m;
-m || (m = {}), m || (m = {}), x, x, x;
+var m, m1;
+m || (m = {}), (m1 = m || (m = {})).x, m1.x, m1.x;
diff --git a/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModuleES6.1.normal.js b/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModuleES6.1.normal.js
index 1c5649994451..e5cb573b0326 100644
--- a/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModuleES6.1.normal.js
+++ b/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModuleES6.1.normal.js
@@ -1,10 +1,10 @@
//// [objectLiteralShorthandPropertiesWithModuleES6.ts]
(function(m) {})(m || (m = {}));
(function(m) {
- var z = x;
+ var z = m.x;
var y = {
- a: x,
- x
+ a: m.x,
+ x: m.x
};
})(m || (m = {}));
var m;
diff --git a/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModuleES6.2.minified.js b/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModuleES6.2.minified.js
index e3252d596ba2..7c1736f374a1 100644
--- a/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModuleES6.2.minified.js
+++ b/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModuleES6.2.minified.js
@@ -1,3 +1,3 @@
//// [objectLiteralShorthandPropertiesWithModuleES6.ts]
-var m;
-m || (m = {}), m || (m = {}), x, x, x;
+var m, m1;
+m || (m = {}), (m1 = m || (m = {})).x, m1.x, m1.x;
diff --git a/crates/swc/tests/tsc-references/parserFunctionDeclaration7.1.normal.js b/crates/swc/tests/tsc-references/parserFunctionDeclaration7.1.normal.js
index 116b738821e0..d8b1cbd9fca5 100644
--- a/crates/swc/tests/tsc-references/parserFunctionDeclaration7.1.normal.js
+++ b/crates/swc/tests/tsc-references/parserFunctionDeclaration7.1.normal.js
@@ -1 +1,3 @@
//// [parserFunctionDeclaration7.ts]
+(function(M) {})(M || (M = {}));
+var M;
diff --git a/crates/swc/tests/tsc-references/parserFunctionDeclaration7.2.minified.js b/crates/swc/tests/tsc-references/parserFunctionDeclaration7.2.minified.js
index 116b738821e0..7945d5169d57 100644
--- a/crates/swc/tests/tsc-references/parserFunctionDeclaration7.2.minified.js
+++ b/crates/swc/tests/tsc-references/parserFunctionDeclaration7.2.minified.js
@@ -1 +1,3 @@
//// [parserFunctionDeclaration7.ts]
+var M;
+M || (M = {});
diff --git a/crates/swc/tests/tsc-references/tsxEmit3.1.normal.js b/crates/swc/tests/tsc-references/tsxEmit3.1.normal.js
index 441653975504..12ea9c29c2ce 100644
--- a/crates/swc/tests/tsc-references/tsxEmit3.1.normal.js
+++ b/crates/swc/tests/tsc-references/tsxEmit3.1.normal.js
@@ -1,38 +1,38 @@
//// [file.tsx]
import { _ as _class_call_check } from "@swc/helpers/_/_class_call_check";
(function(M) {
- var Foo1 = function Foo1() {
+ var Foo = function Foo() {
"use strict";
- _class_call_check(this, Foo1);
+ _class_call_check(this, Foo);
};
- M.Foo = Foo1;
- (function(S1) {
- var Bar1 = function Bar1() {
+ M.Foo = Foo;
+ (function(S) {
+ var Bar = function Bar() {
"use strict";
- _class_call_check(this, Bar1);
+ _class_call_check(this, Bar);
};
- S1.Bar = Bar1;
+ S.Bar = Bar;
// Emit Foo
// Foo, ;
})(M.S || (M.S = {}));
})(M || (M = {}));
(function(M) {
// Emit M.Foo
- Foo, /*#__PURE__*/ React.createElement(Foo, null);
- (function(S1) {
+ M.Foo, /*#__PURE__*/ React.createElement(M.Foo, null);
+ (function(S) {
// Emit M.Foo
- Foo, /*#__PURE__*/ React.createElement(Foo, null);
+ M.Foo, /*#__PURE__*/ React.createElement(M.Foo, null);
// Emit S.Bar
- Bar, /*#__PURE__*/ React.createElement(Bar, null);
+ S.Bar, /*#__PURE__*/ React.createElement(S.Bar, null);
})(M.S || (M.S = {}));
})(M || (M = {}));
(function(M) {
// Emit M.S.Bar
- S.Bar, /*#__PURE__*/ React.createElement(S.Bar, null);
+ M.S.Bar, /*#__PURE__*/ React.createElement(M.S.Bar, null);
})(M || (M = {}));
(function(M) {
var M1 = 100;
// Emit M_1.Foo
- Foo, /*#__PURE__*/ React.createElement(Foo, null);
+ M.Foo, /*#__PURE__*/ React.createElement(M.Foo, null);
})(M || (M = {}));
var M;
diff --git a/crates/swc/tests/tsc-references/tsxEmit3.2.minified.js b/crates/swc/tests/tsc-references/tsxEmit3.2.minified.js
index b90f0911964c..772e8a8bf417 100644
--- a/crates/swc/tests/tsc-references/tsxEmit3.2.minified.js
+++ b/crates/swc/tests/tsc-references/tsxEmit3.2.minified.js
@@ -1,8 +1,8 @@
//// [file.tsx]
-var M, M1, M2;
+var M, M1, M2, S, M3, M4;
import { _ as _class_call_check } from "@swc/helpers/_/_class_call_check";
-(M1 = M || (M = {})).Foo = function Foo1() {
- _class_call_check(this, Foo1);
-}, (M1.S || (M1.S = {})).Bar = function Bar1() {
- _class_call_check(this, Bar1);
-}, Foo, Foo, (M2 = M || (M = {})).S || (M2.S = {}), Foo, Foo, Bar, Bar, M || (M = {}), S.Bar, S.Bar, M || (M = {}), Foo, Foo;
+(M1 = M || (M = {})).Foo = function Foo() {
+ _class_call_check(this, Foo);
+}, (M1.S || (M1.S = {})).Bar = function Bar() {
+ _class_call_check(this, Bar);
+}, (M2 = M || (M = {})).Foo, M2.Foo, S = M2.S || (M2.S = {}), M2.Foo, M2.Foo, S.Bar, S.Bar, (M3 = M || (M = {})).S.Bar, M3.S.Bar, (M4 = M || (M = {})).Foo, M4.Foo;
diff --git a/crates/swc/tests/tsc-references/tsxPreserveEmit1.1.normal.js b/crates/swc/tests/tsc-references/tsxPreserveEmit1.1.normal.js
index 502bf2cedba6..60ee915578b1 100644
--- a/crates/swc/tests/tsc-references/tsxPreserveEmit1.1.normal.js
+++ b/crates/swc/tests/tsc-references/tsxPreserveEmit1.1.normal.js
@@ -23,7 +23,7 @@ define([
(function(M) {})(M || (M = {}));
(function(M) {
// Should emit 'M.X' in both opening and closing tags
- var y = /*#__PURE__*/ React.createElement(X, null);
+ var y = /*#__PURE__*/ React.createElement(M.X, null);
})(M || (M = {}));
var M;
});
diff --git a/crates/swc/tests/tsc-references/tsxPreserveEmit1.2.minified.js b/crates/swc/tests/tsc-references/tsxPreserveEmit1.2.minified.js
index 1ba744cf168b..4232266e885f 100644
--- a/crates/swc/tests/tsc-references/tsxPreserveEmit1.2.minified.js
+++ b/crates/swc/tests/tsc-references/tsxPreserveEmit1.2.minified.js
@@ -13,5 +13,5 @@ define([
"react-router"
], function(require, React, ReactRouter) {
var M;
- ReactRouter.Route, M || (M = {}), M || (M = {}), X;
+ ReactRouter.Route, M || (M = {}), (M || (M = {})).X;
});
diff --git a/crates/swc_ecma_transforms_base/src/lib.rs b/crates/swc_ecma_transforms_base/src/lib.rs
index 627a72304cdf..ecfc230ae5c2 100644
--- a/crates/swc_ecma_transforms_base/src/lib.rs
+++ b/crates/swc_ecma_transforms_base/src/lib.rs
@@ -2,7 +2,7 @@
#![allow(clippy::mutable_key_type)]
#![cfg_attr(not(test), allow(unused))]
-pub use self::resolver::resolver;
+pub use self::resolver::{resolver, ts_module_is_instantiated};
#[doc(hidden)]
pub mod ext;
diff --git a/crates/swc_ecma_transforms_base/src/resolver/mod.rs b/crates/swc_ecma_transforms_base/src/resolver/mod.rs
index f23df821f3c4..58f552504920 100644
--- a/crates/swc_ecma_transforms_base/src/resolver/mod.rs
+++ b/crates/swc_ecma_transforms_base/src/resolver/mod.rs
@@ -1,3 +1,5 @@
+use std::{cell::RefCell, rc::Rc};
+
use rustc_hash::{FxHashMap, FxHashSet};
use swc_atoms::Atom;
use swc_common::{Mark, SyntaxContext};
@@ -10,6 +12,318 @@ use tracing::{debug, span, Level};
use crate::scope::{DeclKind, IdentType, ScopeKind};
+/// Merged visibility table for one TypeScript namespace *identity*, shared
+/// across every re-open of that namespace regardless of syntactic form
+/// (block body, dotted body, or `export namespace` inside another body).
+/// Each entry stores the *declaring* re-open's binding [Mark], so a
+/// reference from a sibling body resolves to the exact context the
+/// declaration was rendered with. Non-exported declarations stay local to
+/// each re-open's own body scope and never enter these maps. `id` is a
+/// fresh, never-applied [Mark] used purely as a stable identity handle for
+/// keying nested namespaces (never-applied marks intern no [SyntaxContext]
+/// numbers, so allocating one is rendering-safe). Interior mutability lets
+/// sibling re-opens accumulate into the same storage even though the scope
+/// tree is stack-allocated.
+#[derive(Debug)]
+struct NamespaceMergedTable {
+ id: Mark,
+ symbols: FxHashMap,
+ types: FxHashMap,
+ /// Names whose current `symbols` entry was written by a namespace
+ /// declaration id ([Resolver::bind_namespace_id]). A later re-open's
+ /// namespace id may replace such an entry, but an entry claimed by a
+ /// real declaration (class, function, enum, ...) must not be
+ /// clobbered: TypeScript merges the namespace *into* that declaration,
+ /// so references bind to the declaration's mark.
+ namespace_owned_symbols: FxHashSet,
+ /// Type-space counterpart of `namespace_owned_symbols`.
+ namespace_owned_types: FxHashSet,
+}
+
+type NamespaceMergedTableRef = Rc>;
+
+type NamespaceTableCache = Rc>>;
+
+/// Per-re-open-instance binding marks, keyed by `(enclosing scope instance
+/// mark, namespace name)`. Two re-opens of one namespace nested under
+/// *different* parent bodies get distinct binding marks (their exported
+/// members render with per-body contexts), while re-opens under the same
+/// enclosing instance share one.
+type NamespaceInstanceCache = Rc>>;
+
+/// How an exported binding of the current namespace body picks its mark.
+#[derive(Debug, Clone, Copy)]
+enum NamespaceBindMark {
+ /// Block-form bodies bind exported members with the namespace
+ /// instance's mark.
+ Instance(Mark),
+ /// Dotted-form bodies resolve in the enclosing scope, so exported
+ /// members bind with the enclosing scope's own mark.
+ EnclosingScope,
+}
+
+/// The merge target for exported bindings of the current namespace body:
+/// the identity's merged table plus the mark to bind new exports with.
+#[derive(Debug, Clone)]
+struct NamespaceExportTarget {
+ table: NamespaceMergedTableRef,
+ bind_mark: NamespaceBindMark,
+}
+
+/// Cache keys for one namespace declaration, derived identically by the
+/// hoisting pre-pass and the phase-2 visitors (via
+/// [Resolver::namespace_keys]) so the two can never diverge.
+#[derive(Debug)]
+struct NamespaceKeys {
+ /// Identity of the enclosing merge parent: the enclosing namespace's
+ /// table id when this namespace is exported from a namespace body,
+ /// otherwise the enclosing scope's own mark. Keys both the merged
+ /// table and the instance binding mark, so every re-open of one merged
+ /// namespace shares one instance mark: the TypeScript transform can
+ /// then qualify a cross-body member reference with the alias of the
+ /// body the reference appears in. Non-exported namespaces key by the
+ /// enclosing scope's own mark, which isolates them per body.
+ parent_identity: Mark,
+ name: Atom,
+}
+
+/// Names that are exported by *this* re-open of a TS namespace, collected
+/// in a single pre-pass over the body before hoisting. The hoister uses
+/// these to route same-body re-declarations into the shared export scope
+/// regardless of the order in which `export ...` and its peer declarations
+/// appear (so `var a; export var a = 1;` merges the same way as
+/// `export var a = 1; var a;`). This also means non-exported re-opens of
+/// a sibling-exported name (e.g. `export enum E {}` in body 1 and
+/// `enum E {}` in body 2) stay isolated, since pre-scan looks at the
+/// *current* body only.
+#[derive(Debug, Default)]
+struct NamespaceExportNames {
+ values: FxHashMap,
+ /// Type-space exports of non-namespace declarations (interfaces, type
+ /// aliases, classes, enums, import aliases). Namespace declarations
+ /// are deliberately absent: their type-space visibility is registered
+ /// by [Resolver::bind_namespace_id] with a body-local mark.
+ types: FxHashSet,
+ /// Subset of `values` that are namespace declarations. Namespace ids
+ /// bind body-locally (via [Resolver::bind_namespace_id]) rather than
+ /// through the export routing in [Resolver::modify], and the hoisting
+ /// pre-pass must not seed them (their declaring mark does not exist
+ /// until the body is visited).
+ namespace_ids: FxHashSet,
+ /// Subset of `namespace_ids` whose name has no value meaning in this
+ /// body: every value-side occurrence of the name is a namespace
+ /// declaration that is not *instantiated* (transitively type-only,
+ /// per [ts_module_is_instantiated]). Such a name is fully erased and
+ /// must not claim a value-space slot in the merged table: a value
+ /// reference to the name resolves past it to an outer binding, as in
+ /// tsc. One value-producing declaration of the name keeps the value
+ /// meaning of the whole merged symbol, in either declaration order,
+ /// so a namespace that merges with a function (or an instantiated
+ /// re-declaration of itself) stays out of this set. Exportedness
+ /// judgement and type-space handling are unaffected.
+ erased_namespace_ids: FxHashSet,
+ /// Names with at least one value-producing declaration in this body:
+ /// a variable, function, class, enum, exported import alias, or an
+ /// instantiated namespace occurrence. Guards `erased_namespace_ids`
+ /// against a non-instantiated namespace occurrence erasing a name
+ /// that another occurrence gives value meaning.
+ value_producing_ids: FxHashSet,
+}
+
+impl NamespaceExportNames {
+ /// Records a value-producing occurrence of `sym`: the merged name has
+ /// value meaning in this body, so the value seed must keep it even
+ /// when another occurrence is a non-instantiated namespace
+ /// declaration (before or after this one).
+ fn add_value_producing(&mut self, sym: Atom) {
+ self.erased_namespace_ids.remove(&sym);
+ self.value_producing_ids.insert(sym);
+ }
+
+ /// Records a non-instantiated namespace occurrence of `sym`: erased
+ /// from the value seed unless another occurrence in this body
+ /// produces a value.
+ fn add_erased_namespace(&mut self, sym: Atom) {
+ if !self.value_producing_ids.contains(&sym) {
+ self.erased_namespace_ids.insert(sym);
+ }
+ }
+}
+
+type NamespaceExportNamesRef = Rc;
+
+/// Collect the names that the given namespace body exports. Only the
+/// top-level `export ...` declarations of the body are inspected; nested
+/// scopes are handled by their own pre-scan when they are visited.
+///
+/// `export import A = ...` (parsed as `TsImportEqualsDecl { is_export: true,
+/// .. }`) is also recognised so that the alias name is routed to the merged
+/// export scope alongside `export var`, `export class`, etc. Without this,
+/// the alias would be treated as body-local and references from sibling
+/// re-opens of the same namespace would fall through to outer/unresolved
+/// bindings, violating TypeScript's namespace-merge semantics.
+fn pre_scan_namespace_exports(body: &TsNamespaceBody) -> NamespaceExportNames {
+ let mut scan = NamespaceExportNames::default();
+ if let TsNamespaceBody::TsModuleBlock(block) = body {
+ block.body.iter().for_each(|item| match item {
+ ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => {
+ add_decl_export_names(&export.decl, &mut scan);
+ }
+ ModuleItem::ModuleDecl(ModuleDecl::TsImportEquals(import)) if import.is_export => {
+ // An exported import alias behaves like an exported namespace
+ // for merge purposes: visible in value-space (and in
+ // type-space when not a type-only alias) from sibling
+ // re-opens of the enclosing namespace.
+ if !import.is_type_only {
+ scan.values.insert(import.id.sym.clone(), DeclKind::Lexical);
+ scan.add_value_producing(import.id.sym.clone());
+ }
+ scan.types.insert(import.id.sym.clone());
+ }
+ _ => {}
+ });
+ } else if let TsNamespaceBody::TsNamespaceDecl(inner) = body {
+ // `namespace A.B { ... }` means `namespace A { export namespace B
+ // { ... } }`, so the dotted child is an implicit export of `A`.
+ scan.values.insert(inner.id.sym.clone(), DeclKind::Lexical);
+ scan.namespace_ids.insert(inner.id.sym.clone());
+ if ts_namespace_body_is_instantiated(&inner.body) {
+ scan.add_value_producing(inner.id.sym.clone());
+ } else {
+ scan.add_erased_namespace(inner.id.sym.clone());
+ }
+ }
+ scan
+}
+
+/// Whether a namespace declaration is *instantiated* in the TypeScript
+/// sense. A namespace whose members are (transitively) only interfaces,
+/// type aliases, type-only import aliases, and other non-instantiated
+/// namespaces is erased entirely and has no value meaning; TypeScript
+/// resolves a value reference to its name past it, to an outer binding
+/// (using the erased name itself as a value is a checker error). The
+/// `retain` module in swc_ecma_transforms_typescript consumes this
+/// predicate for its namespace emit decision, so the resolver and the
+/// emitter classify namespaces identically by construction.
+pub fn ts_module_is_instantiated(module: &TsModuleDecl) -> bool {
+ module
+ .body
+ .as_ref()
+ .map(ts_namespace_body_is_instantiated)
+ .unwrap_or_default()
+}
+
+fn ts_namespace_body_is_instantiated(body: &TsNamespaceBody) -> bool {
+ body.as_ts_module_block()
+ .map(|block| block.body.iter().any(module_item_is_instantiating))
+ .or_else(|| {
+ body.as_ts_namespace_decl()
+ .map(|inner| ts_namespace_body_is_instantiated(&inner.body))
+ })
+ .unwrap_or(true)
+}
+
+fn module_item_is_instantiating(item: &ModuleItem) -> bool {
+ item.as_module_decl()
+ .map(module_decl_is_instantiating)
+ .or_else(|| item.as_stmt().map(stmt_is_instantiating))
+ .unwrap_or(true)
+}
+
+/// Type-only import/export forms and `export as namespace` are erased;
+/// every other module-level form conservatively instantiates (most are
+/// not valid inside a namespace body anyway).
+fn module_decl_is_instantiating(module_decl: &ModuleDecl) -> bool {
+ module_decl
+ .as_export_decl()
+ .map(|export| decl_is_instantiating(&export.decl))
+ .unwrap_or_else(|| {
+ let erased = module_decl
+ .as_ts_import_equals()
+ .map(|import| import.is_type_only)
+ .or_else(|| module_decl.as_import().map(|import| import.type_only))
+ .or_else(|| module_decl.as_export_named().map(|export| export.type_only))
+ .or_else(|| module_decl.as_export_all().map(|export| export.type_only))
+ .unwrap_or_else(|| module_decl.is_ts_namespace_export());
+ !erased
+ })
+}
+
+fn stmt_is_instantiating(stmt: &Stmt) -> bool {
+ stmt.as_decl()
+ .map(decl_is_instantiating)
+ .unwrap_or_else(|| !stmt.is_empty())
+}
+
+/// Interfaces, type aliases, and (transitively) type-only namespaces do
+/// not instantiate; every other declaration does. A function declaration
+/// instantiates even without a body (an ambient `declare function` member
+/// or an overload signature): TypeScript's `getModuleInstanceState` maps
+/// every function declaration to `Instantiated`, so the enclosing
+/// namespace still gets its object even though the declaration itself is
+/// erased from the emit.
+fn decl_is_instantiating(decl: &Decl) -> bool {
+ let erased = decl.is_ts_interface()
+ || decl.is_ts_type_alias()
+ || decl
+ .as_ts_module()
+ .map(|module| !ts_module_is_instantiated(module))
+ .unwrap_or_default();
+ !erased
+}
+
+fn add_decl_export_names(decl: &Decl, scan: &mut NamespaceExportNames) {
+ match decl {
+ Decl::Var(v) => {
+ let kind = DeclKind::from(v.kind);
+ v.decls.iter().for_each(|d| {
+ find_pat_ids::<_, Id>(&d.name)
+ .into_iter()
+ .for_each(|(sym, _)| {
+ scan.values.insert(sym.clone(), kind);
+ scan.add_value_producing(sym);
+ });
+ })
+ }
+ Decl::Fn(f) => {
+ scan.values.insert(f.ident.sym.clone(), DeclKind::Function);
+ scan.add_value_producing(f.ident.sym.clone());
+ }
+ Decl::Class(c) => {
+ scan.values.insert(c.ident.sym.clone(), DeclKind::Lexical);
+ scan.types.insert(c.ident.sym.clone());
+ scan.add_value_producing(c.ident.sym.clone());
+ }
+ Decl::TsEnum(e) => {
+ scan.values.insert(e.id.sym.clone(), DeclKind::Lexical);
+ scan.types.insert(e.id.sym.clone());
+ scan.add_value_producing(e.id.sym.clone());
+ }
+ Decl::TsTypeAlias(a) => {
+ scan.types.insert(a.id.sym.clone());
+ }
+ Decl::TsInterface(i) => {
+ scan.types.insert(i.id.sym.clone());
+ }
+ Decl::TsModule(m) => {
+ if let TsModuleName::Ident(id) = &m.id {
+ scan.values
+ .entry(id.sym.clone())
+ .or_insert(DeclKind::Lexical);
+ scan.namespace_ids.insert(id.sym.clone());
+ if ts_module_is_instantiated(m) {
+ scan.add_value_producing(id.sym.clone());
+ } else {
+ scan.add_erased_namespace(id.sym.clone());
+ }
+ }
+ }
+ Decl::Using(_) => {}
+ #[cfg(swc_ast_unknown)]
+ _ => {}
+ }
+}
+
#[cfg(test)]
mod tests;
@@ -145,6 +459,7 @@ pub fn resolver(
current: Scope::new(ScopeKind::Fn, top_level_mark, None),
ident_type: IdentType::Ref,
in_type: false,
+ in_ts_qualifier: false,
is_module: false,
in_ts_module: false,
decl_kind: DeclKind::Lexical,
@@ -154,6 +469,11 @@ pub fn resolver(
unresolved_mark,
top_level_mark,
},
+ namespace_tables: Rc::new(RefCell::new(FxHashMap::default())),
+ namespace_instances: Rc::new(RefCell::new(FxHashMap::default())),
+ namespace_export: None,
+ namespace_identity: None,
+ namespace_export_names: None,
})
}
@@ -173,6 +493,17 @@ struct Scope<'a> {
/// All types declared in the scope
declared_types: FxHashSet,
+
+ /// When this scope is the *body* of a TypeScript namespace, `shared`
+ /// holds the merged table(s) layered onto this scope: the enclosing
+ /// namespace's table, plus (while a dotted-form child body is being
+ /// visited) the child namespace's table. Reference lookups walking
+ /// the scope chain consult these tables innermost-first (reverse push
+ /// order) in addition to `self.declared_symbols` / `declared_types`,
+ /// so identifiers exported from another re-open of the same namespace
+ /// resolve here, while non-exported declarations in this body remain
+ /// private to this re-open.
+ shared: Vec,
}
impl<'a> Scope<'a> {
@@ -183,12 +514,20 @@ impl<'a> Scope<'a> {
mark,
declared_symbols: Default::default(),
declared_types: Default::default(),
+ shared: Default::default(),
}
}
- fn is_declared(&self, symbol: &Atom) -> Option<&DeclKind> {
+ fn is_declared(&self, symbol: &Atom) -> Option {
self.declared_symbols
.get(symbol)
+ .copied()
+ .or_else(|| {
+ self.shared
+ .iter()
+ .rev()
+ .find_map(|table| table.borrow().symbols.get(symbol).map(|(kind, _)| *kind))
+ })
.or_else(|| self.parent?.is_declared(symbol))
}
}
@@ -202,12 +541,46 @@ struct Resolver<'a> {
current: Scope<'a>,
ident_type: IdentType,
in_type: bool,
+ in_ts_qualifier: bool,
is_module: bool,
in_ts_module: bool,
decl_kind: DeclKind,
strict_mode: bool,
config: InnerConfig,
+
+ /// Cache of merged namespace tables, keyed by `(parent identity mark,
+ /// namespace name)`. Shared across sibling scopes so that re-opened
+ /// TypeScript namespaces accumulate their exports into the same
+ /// [NamespaceMergedTable].
+ namespace_tables: NamespaceTableCache,
+
+ /// Cache of per-instance binding marks, keyed by `(enclosing scope
+ /// mark, namespace name)`. See [NamespaceInstanceCache].
+ namespace_instances: NamespaceInstanceCache,
+
+ /// When inside a TypeScript namespace body, the merge target of the
+ /// immediately enclosing namespace. `modify` registers bindings here
+ /// (in addition to applying the target's bind mark) for any name that
+ /// is part of [`Self::namespace_export_names`] (i.e. is exported by
+ /// the current body), so exported declarations land in the
+ /// namespace's merged table while non-exported re-opens stay on the
+ /// per-re-open body scope.
+ namespace_export: Option,
+
+ /// The enclosing namespace's merged-table id, used as the
+ /// parent-identity half of the cache key when a nested exported
+ /// namespace is itself declared inside this body.
+ namespace_identity: Option,
+
+ /// Set of names exported by the immediately enclosing namespace body,
+ /// computed once before hoisting starts. Used by `modify` to decide
+ /// whether a binding should be routed into the merged export scope.
+ /// Looking at the *current body's* exports (not the cumulative export
+ /// scope) is important: it lets same-body redeclarations such as
+ /// `export var a = 1; for (var a; …)` merge while keeping sibling
+ /// re-opens that don't export the name isolated from each other.
+ namespace_export_names: Option,
}
#[derive(Debug, Clone, Copy)]
@@ -225,11 +598,17 @@ impl<'a> Resolver<'a> {
current,
ident_type: IdentType::Ref,
in_type: false,
+ in_ts_qualifier: false,
is_module: false,
in_ts_module: false,
config,
decl_kind: DeclKind::Lexical,
strict_mode: false,
+ namespace_tables: Rc::new(RefCell::new(FxHashMap::default())),
+ namespace_instances: Rc::new(RefCell::new(FxHashMap::default())),
+ namespace_export: None,
+ namespace_identity: None,
+ namespace_export_names: None,
}
}
@@ -246,10 +625,26 @@ impl<'a> Resolver<'a> {
ident_type: IdentType::Ref,
config: self.config,
in_type: self.in_type,
+ in_ts_qualifier: self.in_ts_qualifier,
is_module: self.is_module,
in_ts_module: self.in_ts_module,
decl_kind: self.decl_kind,
strict_mode: self.strict_mode,
+ namespace_tables: self.namespace_tables.clone(),
+ namespace_instances: self.namespace_instances.clone(),
+ // The enclosing namespace's merge target is still relevant
+ // inside any nested scope (a function body inside a namespace,
+ // for example, may itself declare further nested namespaces
+ // that need to merge across re-opens of the outer namespace).
+ namespace_export: self.namespace_export.clone(),
+ namespace_identity: self.namespace_identity,
+ // The pre-scanned export name set applies only to declarations
+ // that live *directly* in the enclosing namespace body. A
+ // nested block or function body inside the body is a fresh
+ // scope, so a `let x` or `var x` declared there must not be
+ // re-routed into the namespace's export scope even if `x`
+ // happens to be exported at the namespace level.
+ namespace_export_names: None,
};
op(&mut child);
@@ -272,16 +667,39 @@ impl<'a> Resolver<'a> {
fn mark_for_ref_inner(&self, sym: &Atom, stop_an_fn_scope: bool) -> Option {
if self.config.handle_types && self.in_type {
- let mut mark = self.current.mark;
let mut scope = Some(&self.current);
while let Some(cur) = scope {
// if cur.declared_types.contains(sym) ||
// cur.hoisted_symbols.borrow().contains(sym) {
if cur.declared_types.contains(sym) {
- if mark == Mark::root() {
+ if cur.mark == Mark::root() {
break;
}
+ return Some(cur.mark);
+ }
+
+ // A namespace body's merged table(s) sit alongside the body
+ // in the chain; check them (innermost first) before walking
+ // to the enclosing scope so that an exported type declared
+ // by a sibling re-open of the same namespace resolves here,
+ // to the declaring body's own context.
+ // A slot owned purely by namespace declarations has
+ // namespace meaning but no type meaning of its own: a
+ // bare type reference skips it and keeps walking outward
+ // (tsc rejects a namespace-only symbol for type meaning,
+ // TS2709), while the left side of a qualified name
+ // resolves with namespace meaning and binds it.
+ if let Some(mark) = cur.shared.iter().rev().find_map(|table| {
+ let table = table.borrow();
+ table
+ .types
+ .get(sym)
+ .filter(|_| {
+ self.in_ts_qualifier || !table.namespace_owned_types.contains(sym)
+ })
+ .copied()
+ }) {
return Some(mark);
}
@@ -289,19 +707,15 @@ impl<'a> Resolver<'a> {
return None;
}
- if let Some(parent) = &cur.parent {
- mark = parent.mark;
- }
scope = cur.parent;
}
}
- let mut mark = self.current.mark;
let mut scope = Some(&self.current);
while let Some(cur) = scope {
if cur.declared_symbols.contains_key(sym) {
- if mark == Mark::root() {
+ if cur.mark == Mark::root() {
return None;
}
@@ -309,21 +723,32 @@ impl<'a> Resolver<'a> {
// https://tc39.es/ecma262/multipage/global-object.html#sec-value-properties-of-the-global-object-infinity
// non configurable global value
"undefined" | "NaN" | "Infinity"
- if mark == self.config.top_level_mark && !self.is_module =>
+ if cur.mark == self.config.top_level_mark && !self.is_module =>
{
Some(self.config.unresolved_mark)
}
- _ => Some(mark),
+ _ => Some(cur.mark),
};
}
+ // Mirror of the type-lookup branch above: an exported value
+ // declared in a sibling re-open of the same namespace lives in
+ // the merged table rather than this body's own
+ // `declared_symbols`, and resolves to the declaring re-open's
+ // stored mark.
+ if let Some(mark) = cur
+ .shared
+ .iter()
+ .rev()
+ .find_map(|table| table.borrow().symbols.get(sym).map(|(_, mark)| *mark))
+ {
+ return Some(mark);
+ }
+
if cur.kind == ScopeKind::Fn && stop_an_fn_scope {
return None;
}
- if let Some(parent) = &cur.parent {
- mark = parent.mark;
- }
scope = cur.parent;
}
@@ -331,6 +756,21 @@ impl<'a> Resolver<'a> {
}
/// Modifies a binding identifier.
+ ///
+ /// In a TS namespace body, a binding is routed to the namespace's
+ /// merged export scope (`namespace_export`) when its name appears in
+ /// the pre-scanned [`Self::namespace_export_names`] set for the
+ /// current body. Otherwise the binding lands on `self.current` and
+ /// stays local to this re-open.
+ ///
+ /// The pre-scan looks only at the body's own `export` declarations,
+ /// so:
+ ///
+ /// - `export var a = 1; for (var a; …)` puts both `a`s on the export scope
+ /// (TypeScript's var-merge inside one body); and
+ /// - `namespace N { export enum E { … } }` followed by `namespace N { enum
+ /// E { … } }` keeps the second `E` private to the second re-open, since
+ /// the second body never exports `E`.
fn modify(&mut self, id: &mut Ident, kind: DeclKind) {
if cfg!(debug_assertions) && LOG {
#[cfg(debug_assertions)]
@@ -344,19 +784,255 @@ impl<'a> Resolver<'a> {
return;
}
- if self.in_type {
- self.current.declared_types.insert(id.sym.clone());
- } else {
- self.current.declared_symbols.insert(id.sym.clone(), kind);
- }
+ let route_to_export = self
+ .namespace_export_names
+ .as_ref()
+ .and_then(|names| {
+ let listed = if self.in_type {
+ names.types.contains(&id.sym)
+ } else {
+ // Namespace ids never reach `modify` (they bind
+ // body-locally via `bind_namespace_id`), so a value
+ // declaration that shares its name with an exported
+ // namespace (class/function/enum merging with the
+ // namespace) routes to the merged scope like any other
+ // export.
+ names.values.contains_key(&id.sym)
+ };
+ listed.then_some(self.namespace_export.as_ref())
+ })
+ .flatten();
- let mark = self.current.mark;
+ let mark = match route_to_export {
+ Some(target) => {
+ let mark = match target.bind_mark {
+ NamespaceBindMark::Instance(instance) => instance,
+ NamespaceBindMark::EnclosingScope => self.current.mark,
+ };
+ let mut table = target.table.borrow_mut();
+ if self.in_type {
+ table.namespace_owned_types.remove(&id.sym);
+ table.types.insert(id.sym.clone(), mark);
+ } else {
+ table.namespace_owned_symbols.remove(&id.sym);
+ table.symbols.insert(id.sym.clone(), (kind, mark));
+ }
+ mark
+ }
+ None => {
+ if self.in_type {
+ self.current.declared_types.insert(id.sym.clone());
+ } else {
+ self.current.declared_symbols.insert(id.sym.clone(), kind);
+ }
+ self.current.mark
+ }
+ };
if mark != Mark::root() {
id.ctxt = id.ctxt.apply_mark(mark);
}
}
+ /// Derives the cache keys for a namespace declaration named `name`
+ /// appearing directly in the current scope. Used identically by the
+ /// hoisting pre-pass and by the phase-2 visitors
+ /// ([`Self::visit_mut_ts_module_decl`] /
+ /// [`Self::visit_mut_ts_namespace_decl`]), so the two phases can never
+ /// derive diverging keys.
+ ///
+ /// Exportedness is judged on *value* exports only: a sibling
+ /// `export interface N` populates just the type set, and a type-only
+ /// export must not drag a non-exported namespace `N` into the merged
+ /// keying.
+ fn namespace_keys(&self, name: &Atom) -> NamespaceKeys {
+ let exported = self
+ .namespace_export_names
+ .as_ref()
+ .is_some_and(|names| names.values.contains_key(name));
+ let parent_identity = if exported {
+ self.namespace_identity.unwrap_or(self.current.mark)
+ } else {
+ self.current.mark
+ };
+ NamespaceKeys {
+ parent_identity,
+ name: name.clone(),
+ }
+ }
+
+ /// Gets or creates the merged table for `keys`. The table's `id` is a
+ /// fresh [Mark] that is never applied to any syntax context; it serves
+ /// only as the stable parent-identity handle for namespaces nested
+ /// inside this one.
+ fn namespace_table(&self, keys: &NamespaceKeys) -> NamespaceMergedTableRef {
+ self.namespace_tables
+ .borrow_mut()
+ .entry((keys.parent_identity, keys.name.clone()))
+ .or_insert_with(|| {
+ Rc::new(RefCell::new(NamespaceMergedTable {
+ id: Mark::fresh(self.config.top_level_mark),
+ symbols: Default::default(),
+ types: Default::default(),
+ namespace_owned_symbols: Default::default(),
+ namespace_owned_types: Default::default(),
+ }))
+ })
+ .clone()
+ }
+
+ /// Gets or creates the instance binding mark for `keys`. One mark per
+ /// merged-namespace identity: every re-open of an exported namespace
+ /// binds its exported members with the same mark, which is what lets
+ /// the TypeScript transform qualify a cross-body member reference with
+ /// the current body's alias. The mark is applied only when a binding
+ /// is actually rendered, so creating it eagerly (during the hoisting
+ /// pre-pass) interns no syntax-context numbers.
+ fn namespace_instance_mark(&self, keys: &NamespaceKeys) -> Mark {
+ *self
+ .namespace_instances
+ .borrow_mut()
+ .entry((keys.parent_identity, keys.name.clone()))
+ .or_insert_with(|| Mark::fresh(self.config.top_level_mark))
+ }
+
+ /// Binds a namespace declaration's own id.
+ ///
+ /// Namespace ids always bind body-locally with the current scope's
+ /// mark (namespace *declarations* do not unify across re-opens; only
+ /// member references resolve through the merged table). When the
+ /// enclosing namespace body exports the name, the name is also
+ /// registered in the enclosing merged table (both spaces) so that
+ /// sibling re-opens referencing the namespace by name resolve it. The
+ /// value-space registration uses the enclosing target's bind mark (the
+ /// mark every exported value member carries), not the id's own
+ /// body-local mark: a cross-body value reference then reads as a member
+ /// of the enclosing namespace instance, which the TypeScript transform
+ /// qualifies with the alias of the body the reference appears in. The
+ /// type-space registration keeps the declaring body's mark.
+ /// Registration yields to a slot already claimed by a non-namespace
+ /// declaration (a class, function, enum, or interface the namespace
+ /// merges with): references then bind that declaration, per
+ /// TypeScript's declaration-merge semantics.
+ ///
+ /// `instantiated` reports whether this declaration's body is
+ /// instantiated (per [ts_module_is_instantiated]). A non-instantiated
+ /// declaration registers in type space only: the namespace is fully
+ /// erased, so its name has no value meaning and must not shadow an
+ /// outer value binding for sibling re-opens. (Another re-open of the
+ /// same name that does have value members claims the value slot via
+ /// its own registration or the pre-seed.)
+ fn bind_namespace_id(&mut self, id: &mut Ident, instantiated: bool) {
+ if id.ctxt == SyntaxContext::empty() {
+ self.current
+ .declared_symbols
+ .insert(id.sym.clone(), DeclKind::Lexical);
+
+ let mark = self.current.mark;
+ if mark != Mark::root() {
+ id.ctxt = id.ctxt.apply_mark(mark);
+ }
+
+ let exported = self.namespace_export_names.as_ref().is_some_and(|names| {
+ names.values.contains_key(&id.sym) || names.namespace_ids.contains(&id.sym)
+ });
+ if exported {
+ self.namespace_export.iter().for_each(|target| {
+ let member_mark = match target.bind_mark {
+ NamespaceBindMark::Instance(instance) => instance,
+ NamespaceBindMark::EnclosingScope => mark,
+ };
+ let mut table = target.table.borrow_mut();
+ let owns_value = !table.symbols.contains_key(&id.sym)
+ || table.namespace_owned_symbols.contains(&id.sym);
+ if owns_value && instantiated {
+ table
+ .symbols
+ .insert(id.sym.clone(), (DeclKind::Lexical, member_mark));
+ table.namespace_owned_symbols.insert(id.sym.clone());
+ }
+ let owns_type = !table.types.contains_key(&id.sym)
+ || table.namespace_owned_types.contains(&id.sym);
+ if owns_type {
+ table.types.insert(id.sym.clone(), mark);
+ table.namespace_owned_types.insert(id.sym.clone());
+ }
+ });
+ }
+ }
+ }
+
+ /// Pre-seeds the merged table for the namespace named `name` with the
+ /// exports of `body`, so that a reference in an *earlier* re-open can
+ /// resolve a name exported only by a *later* sibling re-open (forward
+ /// references across bodies).
+ ///
+ /// Runs from the hoisting pre-pass, which sweeps the enclosing
+ /// statement list before any namespace body is resolved. Seeding
+ /// inserts into the maps only and applies no marks, so syntax-context
+ /// interning order is untouched. Exported namespace ids seed the value
+ /// side like every other export, with the instance mark: an earlier
+ /// re-open can then resolve a namespace that only a later sibling
+ /// re-open declares, and the reference reads as a member of the
+ /// enclosing instance (matching the registration in
+ /// [`Self::bind_namespace_id`]). Non-instantiated namespace ids are
+ /// excluded from the value seed (see
+ /// [NamespaceExportNames::erased_namespace_ids]): an erased namespace
+ /// has no value meaning, so references resolve past it to an outer
+ /// binding.
+ ///
+ /// Namespace ids also seed the *type* side (separately from the
+ /// pre-scan's type set, which holds only non-namespace type
+ /// declarations): every namespace declaration has namespace meaning,
+ /// erased or not, so the left side of an earlier re-open's qualified
+ /// type reference must not fall through to an outer binding. (A
+ /// *bare* type reference is different: a namespace alone has no type
+ /// meaning, so that lookup skips slots that only a namespace owns;
+ /// see [`Self::mark_for_ref_inner`].) The type seed claims only a
+ /// vacant slot, with the instance mark, and records namespace
+ /// ownership so that the declaring body's own registration overwrites
+ /// it with the body mark (see [`Self::bind_namespace_id`]). A slot
+ /// already claimed by a non-namespace type declaration is left alone,
+ /// and a non-namespace type declaration seen by any re-open revokes
+ /// namespace ownership of the slot, in either seeding order:
+ /// TypeScript merges the namespace into that declaration, so the
+ /// namespace registration must leave the merged slot in place and
+ /// every reference then agrees on the instance mark. Dotted bodies'
+ /// inner members are not seeded; forward references across sibling
+ /// re-opens therefore reach one nesting level deep.
+ fn seed_namespace_exports(&self, name: &Atom, body: &TsNamespaceBody) {
+ let keys = self.namespace_keys(name);
+ let table = self.namespace_table(&keys);
+ let instance_mark = self.namespace_instance_mark(&keys);
+
+ let scan = pre_scan_namespace_exports(body);
+ let mut table = table.borrow_mut();
+ let NamespaceMergedTable {
+ symbols,
+ types,
+ namespace_owned_types,
+ ..
+ } = &mut *table;
+ scan.values
+ .iter()
+ .filter(|(sym, _)| !scan.erased_namespace_ids.contains(*sym))
+ .for_each(|(sym, kind)| {
+ symbols
+ .entry(sym.clone())
+ .or_insert((*kind, instance_mark));
+ });
+ scan.types.iter().for_each(|sym| {
+ namespace_owned_types.remove(sym);
+ types.entry(sym.clone()).or_insert(instance_mark);
+ });
+ scan.namespace_ids.iter().for_each(|sym| {
+ types.entry(sym.clone()).or_insert_with(|| {
+ namespace_owned_types.insert(sym.clone());
+ instance_mark
+ });
+ });
+ }
+
fn mark_block(&mut self, ctxt: &mut SyntaxContext) {
if *ctxt != SyntaxContext::empty() {
return;
@@ -369,6 +1045,39 @@ impl<'a> Resolver<'a> {
}
}
+ /// Records a TypeScript type binding in the merged export scope when
+ /// the name is exported by the current namespace body (per the
+ /// pre-scanned [`Self::namespace_export_names`]). Mirrors the
+ /// [`Self::modify`] routing for callers that only need the side-table
+ /// insert without applying a [`Mark`] to an identifier (the class
+ /// hoister, in particular).
+ fn declare_type_in_current(&mut self, name: Atom) {
+ let route_to_export = self
+ .namespace_export_names
+ .as_ref()
+ .and_then(|names| {
+ names
+ .types
+ .contains(&name)
+ .then_some(self.namespace_export.as_ref())
+ })
+ .flatten();
+ match route_to_export {
+ Some(target) => {
+ let mark = match target.bind_mark {
+ NamespaceBindMark::Instance(instance) => instance,
+ NamespaceBindMark::EnclosingScope => self.current.mark,
+ };
+ let mut table = target.table.borrow_mut();
+ table.namespace_owned_types.remove(&name);
+ table.types.insert(name, mark);
+ }
+ None => {
+ self.current.declared_types.insert(name);
+ }
+ }
+ }
+
fn try_resolving_as_type(&mut self, i: &mut Ident) {
if i.ctxt.outer() == self.config.unresolved_mark {
i.ctxt = SyntaxContext::empty()
@@ -1346,7 +2055,15 @@ impl VisitMut for Resolver<'_> {
}
fn visit_mut_ts_import_equals_decl(&mut self, n: &mut TsImportEqualsDecl) {
+ // A type-only alias (`import type A = ...`) introduces a
+ // type-space binding only: it must not create a value-space
+ // binding, and an exported one has to register in the enclosing
+ // namespace's merged *type* scope so that sibling re-opens
+ // resolve it.
+ let old_in_type = self.in_type;
+ self.in_type = n.is_type_only;
self.modify(&mut n.id, DeclKind::Lexical);
+ self.in_type = old_in_type;
n.module_ref.visit_mut_with(self);
}
@@ -1423,18 +2140,74 @@ impl VisitMut for Resolver<'_> {
return;
}
- match &mut decl.id {
+ let instantiated = ts_module_is_instantiated(decl);
+ let namespace_name = match &mut decl.id {
TsModuleName::Ident(i) => {
- self.modify(i, DeclKind::Lexical);
+ // Marks the id and registers it in the enclosing merged
+ // table when exported. Usually a no-op here: the hoisting
+ // pre-pass has already bound the id.
+ self.bind_namespace_id(i, instantiated);
+ Some(i.sym.clone())
}
- TsModuleName::Str(_) => {}
+ TsModuleName::Str(_) => None,
#[cfg(swc_ast_unknown)]
- _ => {}
- }
+ _ => None,
+ };
+
+ // Key derivation (see [Self::namespace_keys]):
+ //
+ // * Top-level namespaces key their table on the enclosing scope's stable mark,
+ // so sibling re-opens collide on the same entry and merge.
+ //
+ // * Nested namespaces *exported* from the enclosing body key on the enclosing
+ // namespace's table id, so every re-open reaches the same table regardless of
+ // which outer re-open (or syntactic form) declares it.
+ //
+ // * Nested namespaces *not exported* from the enclosing body key on the
+ // per-re-open body mark, so they land in disjoint entries and stay isolated,
+ // matching TypeScript's rule that non-exported members are local to each
+ // declaration body.
+ //
+ // The per-instance binding mark is keyed on the enclosing scope
+ // instance, so re-opens under one parent body share a binding mark
+ // while re-opens under distinct parent bodies render with distinct
+ // contexts (their references still unify through the table).
+ let merge = namespace_name.map(|name| {
+ let keys = self.namespace_keys(&name);
+ (
+ self.namespace_table(&keys),
+ self.namespace_instance_mark(&keys),
+ )
+ });
+
+ // Pre-scan this body's exports. Doing it once up front makes the
+ // routing decision in `modify` order-independent and avoids
+ // leaking exports across re-opens of the same namespace.
+ let export_names: Option = decl
+ .body
+ .as_ref()
+ .map(|body| Rc::new(pre_scan_namespace_exports(body)));
self.with_child(ScopeKind::Block, |child| {
child.in_ts_module = true;
+ // Attach the merged table so that lookups for sibling-declared
+ // exports succeed, and route the body's own exported
+ // declarations into the same shared storage. A
+ // `TsModuleName::Str` body has no merge identity: it neither
+ // reads from nor registers into any enclosing table.
+ child.namespace_export = None;
+ child.namespace_identity = None;
+ merge.into_iter().for_each(|(table, instance_mark)| {
+ child.current.shared.push(table.clone());
+ child.namespace_identity = Some(table.borrow().id);
+ child.namespace_export = Some(NamespaceExportTarget {
+ table,
+ bind_mark: NamespaceBindMark::Instance(instance_mark),
+ });
+ });
+ child.namespace_export_names = export_names;
+
decl.body.visit_mut_children_with(child);
});
}
@@ -1444,9 +2217,36 @@ impl VisitMut for Resolver<'_> {
return;
}
- self.modify(&mut n.id, DeclKind::Lexical);
+ // A dotted body (`namespace A.B { ... }`) is sugar for an exported
+ // nested namespace, but it resolves *in the enclosing scope*: the
+ // id and every exported member bind with the enclosing body's own
+ // mark ([NamespaceBindMark::EnclosingScope]). The child's merged
+ // table is layered onto the current scope for the duration of the
+ // body so that references resolve through the merge, then popped.
+ let instantiated = ts_namespace_body_is_instantiated(&n.body);
+ self.bind_namespace_id(&mut n.id, instantiated);
+
+ let keys = self.namespace_keys(&n.id.sym);
+ let table = self.namespace_table(&keys);
+ let table_id = table.borrow().id;
+
+ let body_names: Option =
+ Some(Rc::new(pre_scan_namespace_exports(&n.body)));
+
+ self.current.shared.push(table.clone());
+ let saved_export = self.namespace_export.replace(NamespaceExportTarget {
+ table,
+ bind_mark: NamespaceBindMark::EnclosingScope,
+ });
+ let saved_identity = self.namespace_identity.replace(table_id);
+ let saved_names = std::mem::replace(&mut self.namespace_export_names, body_names);
n.body.visit_mut_with(self);
+
+ self.namespace_export_names = saved_names;
+ self.namespace_identity = saved_identity;
+ self.namespace_export = saved_export;
+ self.current.shared.pop();
}
fn visit_mut_ts_param_prop_param(&mut self, n: &mut TsParamPropParam) {
@@ -1473,7 +2273,14 @@ impl VisitMut for Resolver<'_> {
fn visit_mut_ts_qualified_name(&mut self, n: &mut TsQualifiedName) {
self.ident_type = IdentType::Ref;
- n.left.visit_mut_with(self)
+ // The left side resolves with namespace meaning: merged-table
+ // type slots that only a namespace declaration owns are visible
+ // here, unlike for a bare type reference (see
+ // `mark_for_ref_inner`).
+ let in_ts_qualifier = self.in_ts_qualifier;
+ self.in_ts_qualifier = true;
+ n.left.visit_mut_with(self);
+ self.in_ts_qualifier = in_ts_qualifier;
}
fn visit_mut_ts_satisfies_expr(&mut self, n: &mut TsSatisfiesExpr) {
@@ -1722,9 +2529,7 @@ impl VisitMut for Hoister<'_, '_> {
if self.resolver.config.handle_types {
self.resolver
- .current
- .declared_types
- .insert(node.ident.sym.clone());
+ .declare_type_in_current(node.ident.sym.clone());
}
}
@@ -1772,11 +2577,17 @@ impl VisitMut for Hoister<'_, '_> {
},
) && !self.in_block =>
{
- let old_in_type = self.resolver.in_type;
- self.resolver.in_type = false;
- let id = v.id.as_mut_ident().unwrap();
- self.resolver.modify(id, DeclKind::Lexical);
- self.resolver.in_type = old_in_type;
+ // Bind the namespace id body-locally, then pre-seed the
+ // merged table with the body's exports so that an
+ // earlier sibling re-open can forward-reference them.
+ let instantiated = ts_module_is_instantiated(v);
+ if let TsModuleName::Ident(id) = &mut v.id {
+ self.resolver.bind_namespace_id(id, instantiated);
+ let name = id.sym.clone();
+ v.body.iter().for_each(|body| {
+ self.resolver.seed_namespace_exports(&name, body);
+ });
+ }
}
_ => {}
}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/internalModules/codeGeneration/importStatements/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/internalModules/codeGeneration/importStatements/output.ts
index 80dabef10c24..604391263aa2 100644
--- a/crates/swc_ecma_transforms_base/tests/ts-resolver/internalModules/codeGeneration/importStatements/output.ts
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/internalModules/codeGeneration/importStatements/output.ts
@@ -21,8 +21,8 @@ namespace D__2 {
var p__7 = new a__7.Point(1, 1);
}
namespace E__2 {
- import a__8 = A__2;
- export function xDist__8(x__9: a__8.Point) {
- return a__8.Origin.x - x__9.x;
+ import a__9 = A__2;
+ export function xDist__8(x__10: a__9.Point) {
+ return a__9.Origin.x - x__10.x;
}
}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen/input.ts
new file mode 100644
index 000000000000..bf46cd6f04ab
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen/input.ts
@@ -0,0 +1,9 @@
+namespace Test {
+ export const a = 1;
+}
+
+const a = "out";
+
+namespace Test {
+ export const b = a + 1;
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen/output.ts
new file mode 100644
index 000000000000..a971ad7aa3aa
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen/output.ts
@@ -0,0 +1,7 @@
+namespace Test__2 {
+ export const a__3 = 1;
+}
+const a__2 = "out";
+namespace Test__2 {
+ export const b__3 = a__3 + 1;
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_ambient_fn_instantiates/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_ambient_fn_instantiates/input.ts
new file mode 100644
index 000000000000..46d43d147890
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_ambient_fn_instantiates/input.ts
@@ -0,0 +1,18 @@
+var C = "outer";
+namespace N {
+ export const x = C;
+}
+namespace N {
+ export namespace C {
+ export declare function f(): void;
+ }
+}
+var D = "outer";
+namespace M {
+ export const y = D;
+}
+namespace M {
+ export declare namespace D {
+ function g(): void;
+ }
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_ambient_fn_instantiates/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_ambient_fn_instantiates/output.ts
new file mode 100644
index 000000000000..535a565b909b
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_ambient_fn_instantiates/output.ts
@@ -0,0 +1,18 @@
+var C__2 = "outer";
+namespace N__2 {
+ export const x__3 = C__3;
+}
+namespace N__2 {
+ export namespace C__4 {
+ export declare function f__5(): void;
+ }
+}
+var D__2 = "outer";
+namespace M__2 {
+ export const y__7 = D__7;
+}
+namespace M__2 {
+ export declare namespace D__8 {
+ function g__9(): void;
+ }
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_bare_type_ref_outer/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_bare_type_ref_outer/input.ts
new file mode 100644
index 000000000000..b273d3f06a85
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_bare_type_ref_outer/input.ts
@@ -0,0 +1,9 @@
+type C = string;
+namespace N {
+ export let v: C;
+ export type W = C2.Q;
+}
+namespace N {
+ export namespace C { export const q = 1; }
+ export namespace C2 { export type Q = number; }
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_bare_type_ref_outer/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_bare_type_ref_outer/output.ts
new file mode 100644
index 000000000000..2881e5c83031
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_bare_type_ref_outer/output.ts
@@ -0,0 +1,13 @@
+type C__2 = string;
+namespace N__2 {
+ export let v__3: C__2;
+ export type W__3 = C2__3.Q;
+}
+namespace N__2 {
+ export namespace C__4 {
+ export const q__5 = 1;
+ }
+ export namespace C2__4 {
+ export type Q__6 = number;
+ }
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_class_namespace_merge/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_class_namespace_merge/input.ts
new file mode 100644
index 000000000000..2d3c6fb567dc
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_class_namespace_merge/input.ts
@@ -0,0 +1,14 @@
+namespace N {
+ export class C {}
+}
+
+namespace N {
+ export namespace C {
+ export const x = 1;
+ }
+}
+
+namespace N {
+ export const y = new C();
+ export let v: C;
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_class_namespace_merge/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_class_namespace_merge/output.ts
new file mode 100644
index 000000000000..8940442bf77d
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_class_namespace_merge/output.ts
@@ -0,0 +1,13 @@
+namespace N__2 {
+ export class C__3 {
+ }
+}
+namespace N__2 {
+ export namespace C__4 {
+ export const x__5 = 1;
+ }
+}
+namespace N__2 {
+ export const y__3 = new C__3();
+ export let v__3: C__3;
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_dotted_exports/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_dotted_exports/input.ts
new file mode 100644
index 000000000000..3784a495d3a6
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_dotted_exports/input.ts
@@ -0,0 +1,15 @@
+namespace Outer.Inner {
+ export class C {}
+ export function f() {}
+}
+
+namespace Outer.Inner {
+ export const c = new C();
+ export const g = f();
+}
+
+namespace Outer {
+ export namespace Inner {
+ export const a = c;
+ }
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_dotted_exports/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_dotted_exports/output.ts
new file mode 100644
index 000000000000..44e1d3bfe0ba
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_dotted_exports/output.ts
@@ -0,0 +1,14 @@
+namespace Outer__2.Inner__3 {
+ export class C__3 {
+ }
+ export function f__3() {}
+}
+namespace Outer__2.Inner__5 {
+ export const c__5 = new C__3();
+ export const g__5 = f__3();
+}
+namespace Outer__2 {
+ export namespace Inner__6 {
+ export const a__7 = c__5;
+ }
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_erased_namespace_no_shadow/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_erased_namespace_no_shadow/input.ts
new file mode 100644
index 000000000000..e490096758f5
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_erased_namespace_no_shadow/input.ts
@@ -0,0 +1,16 @@
+var C = "outer";
+namespace N {
+ export const x = C;
+}
+namespace N {
+ export namespace C {
+ export interface I {}
+ }
+}
+var D = "outer";
+namespace M {
+ export namespace D {}
+}
+namespace M {
+ export const y = D;
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_erased_namespace_no_shadow/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_erased_namespace_no_shadow/output.ts
new file mode 100644
index 000000000000..3442aab2b553
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_erased_namespace_no_shadow/output.ts
@@ -0,0 +1,18 @@
+var C__2 = "outer";
+namespace N__2 {
+ export const x__3 = C__2;
+}
+namespace N__2 {
+ export namespace C__4 {
+ export interface I__5 {
+ }
+ }
+}
+var D__2 = "outer";
+namespace M__2 {
+ export namespace D__6 {
+ }
+}
+namespace M__2 {
+ export const y__7 = D__2;
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_export_import/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_export_import/input.ts
new file mode 100644
index 000000000000..8023828b316a
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_export_import/input.ts
@@ -0,0 +1,11 @@
+namespace Outer {
+ export const x = 1;
+}
+
+namespace N {
+ export import A = Outer;
+}
+
+namespace N {
+ export const b = A.x + 1;
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_export_import/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_export_import/output.ts
new file mode 100644
index 000000000000..0aa2ca29fd2c
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_export_import/output.ts
@@ -0,0 +1,9 @@
+namespace Outer__2 {
+ export const x__3 = 1;
+}
+namespace N__2 {
+ export import A__4 = Outer__2;
+}
+namespace N__2 {
+ export const b__4 = A__4.x + 1;
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_fn_ns_value_merge/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_fn_ns_value_merge/input.ts
new file mode 100644
index 000000000000..6a252bcd2e16
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_fn_ns_value_merge/input.ts
@@ -0,0 +1,18 @@
+namespace N {
+ export const x = foo;
+}
+namespace N {
+ export function foo() { return 1; }
+ export namespace foo { export interface Options {} }
+}
+namespace M {
+ export const before = B;
+}
+namespace M {
+ export namespace B {
+ export interface I {}
+ }
+ export namespace B {
+ export const c = 1;
+ }
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_fn_ns_value_merge/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_fn_ns_value_merge/output.ts
new file mode 100644
index 000000000000..0ad938dddb9d
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_fn_ns_value_merge/output.ts
@@ -0,0 +1,24 @@
+namespace N__2 {
+ export const x__3 = foo__3;
+}
+namespace N__2 {
+ export function foo__3() {
+ return 1;
+ }
+ export namespace foo__4 {
+ export interface Options__6 {
+ }
+ }
+}
+namespace M__2 {
+ export const before__7 = B__7;
+}
+namespace M__2 {
+ export namespace B__8 {
+ export interface I__9 {
+ }
+ }
+ export namespace B__8 {
+ export const c__9 = 1;
+ }
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_forward_type_ref/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_forward_type_ref/input.ts
new file mode 100644
index 000000000000..df0ec0dc99a5
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_forward_type_ref/input.ts
@@ -0,0 +1,29 @@
+namespace COut {
+ export interface I {}
+}
+import C = COut;
+namespace N {
+ export type T = C.I;
+}
+namespace N {
+ export namespace C {
+ export interface I {}
+ }
+}
+namespace M {
+ export type U = E.J;
+}
+namespace M {
+ export namespace E {
+ export interface J {}
+ export const v = 1;
+ }
+}
+namespace P {
+ export namespace F {
+ export interface K {}
+ }
+}
+namespace P {
+ export type W = F.K;
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_forward_type_ref/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_forward_type_ref/output.ts
new file mode 100644
index 000000000000..7e97e2a57a7d
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_forward_type_ref/output.ts
@@ -0,0 +1,33 @@
+namespace COut__2 {
+ export interface I__3 {
+ }
+}
+import C__2 = COut__2;
+namespace N__2 {
+ export type T__4 = C__4.I;
+}
+namespace N__2 {
+ export namespace C__5 {
+ export interface I__6 {
+ }
+ }
+}
+namespace M__2 {
+ export type U__7 = E__7.J;
+}
+namespace M__2 {
+ export namespace E__8 {
+ export interface J__9 {
+ }
+ export const v__9 = 1;
+ }
+}
+namespace P__2 {
+ export namespace F__10 {
+ export interface K__11 {
+ }
+ }
+}
+namespace P__2 {
+ export type W__12 = F__10.K;
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_interface_namespace_forward/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_interface_namespace_forward/input.ts
new file mode 100644
index 000000000000..7bea148f0a65
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_interface_namespace_forward/input.ts
@@ -0,0 +1,10 @@
+namespace Outer {
+ export type U = Inner;
+}
+
+namespace Outer {
+ export interface Inner {}
+ export namespace Inner {
+ export const a = 1;
+ }
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_interface_namespace_forward/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_interface_namespace_forward/output.ts
new file mode 100644
index 000000000000..11c518445e5e
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_interface_namespace_forward/output.ts
@@ -0,0 +1,10 @@
+namespace Outer__2 {
+ export type U__3 = Inner__3;
+}
+namespace Outer__2 {
+ export interface Inner__3 {
+ }
+ export namespace Inner__4 {
+ export const a__5 = 1;
+ }
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_late_export/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_late_export/input.ts
new file mode 100644
index 000000000000..132a58e76ac4
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_late_export/input.ts
@@ -0,0 +1,7 @@
+namespace Test {
+ export var b = a + 1;
+}
+
+namespace Test {
+ export var a = 1;
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_late_export/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_late_export/output.ts
new file mode 100644
index 000000000000..7e62c02f4946
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_late_export/output.ts
@@ -0,0 +1,6 @@
+namespace Test__2 {
+ export var b__3 = a__3 + 1;
+}
+namespace Test__2 {
+ export var a__3 = 1;
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_late_namespace_export/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_late_namespace_export/input.ts
new file mode 100644
index 000000000000..c0fe0745237c
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_late_namespace_export/input.ts
@@ -0,0 +1,9 @@
+namespace N {
+ export const x = C;
+}
+namespace N {
+ export namespace C {
+ export const a = 1;
+ }
+}
+const C = "outer";
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_late_namespace_export/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_late_namespace_export/output.ts
new file mode 100644
index 000000000000..6bef540bead9
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_late_namespace_export/output.ts
@@ -0,0 +1,9 @@
+namespace N__2 {
+ export const x__3 = C__3;
+}
+namespace N__2 {
+ export namespace C__4 {
+ export const a__5 = 1;
+ }
+}
+const C__2 = "outer";
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_private/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_private/input.ts
new file mode 100644
index 000000000000..b6c231f2fa22
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_private/input.ts
@@ -0,0 +1,11 @@
+namespace Outer {
+ namespace Inner {
+ export const a = 1;
+ }
+}
+
+namespace Outer {
+ namespace Inner {
+ export const b = a;
+ }
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_private/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_private/output.ts
new file mode 100644
index 000000000000..d8d6d51a013e
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_private/output.ts
@@ -0,0 +1,10 @@
+namespace Outer__2 {
+ namespace Inner__3 {
+ export const a__4 = 1;
+ }
+}
+namespace Outer__2 {
+ namespace Inner__5 {
+ export const b__6 = a;
+ }
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_reopen/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_reopen/input.ts
new file mode 100644
index 000000000000..08f34286047d
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_reopen/input.ts
@@ -0,0 +1,10 @@
+namespace N {
+ export namespace I {
+ export const a = 1;
+ }
+}
+namespace N {
+ export namespace I {
+ export const b = a;
+ }
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_reopen/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_reopen/output.ts
new file mode 100644
index 000000000000..d57630a36472
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_reopen/output.ts
@@ -0,0 +1,10 @@
+namespace N__2 {
+ export namespace I__3 {
+ export const a__4 = 1;
+ }
+}
+namespace N__2 {
+ export namespace I__5 {
+ export const b__4 = a__4;
+ }
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_type_only_export/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_type_only_export/input.ts
new file mode 100644
index 000000000000..445cf45b6030
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_type_only_export/input.ts
@@ -0,0 +1,13 @@
+namespace Outer {
+ export interface Inner {}
+ export namespace Inner {
+ export const a = 1;
+ }
+}
+
+namespace Outer {
+ export interface Inner {}
+ export namespace Inner {
+ export const b = a;
+ }
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_type_only_export/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_type_only_export/output.ts
new file mode 100644
index 000000000000..6c80f6869e06
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_type_only_export/output.ts
@@ -0,0 +1,14 @@
+namespace Outer__2 {
+ export interface Inner__3 {
+ }
+ export namespace Inner__4 {
+ export const a__5 = 1;
+ }
+}
+namespace Outer__2 {
+ export interface Inner__3 {
+ }
+ export namespace Inner__6 {
+ export const b__5 = a__5;
+ }
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_ns_iface_type_merge/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_ns_iface_type_merge/input.ts
new file mode 100644
index 000000000000..b23f39c883e6
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_ns_iface_type_merge/input.ts
@@ -0,0 +1,11 @@
+namespace A {
+ export namespace B {
+ export const c = 1;
+ export type T = number;
+ }
+ export let v: B;
+ export let w: B.T;
+}
+namespace A {
+ export interface B { z: number }
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_ns_iface_type_merge/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_ns_iface_type_merge/output.ts
new file mode 100644
index 000000000000..2776f59d8533
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_ns_iface_type_merge/output.ts
@@ -0,0 +1,13 @@
+namespace A__2 {
+ export namespace B__3 {
+ export const c__5 = 1;
+ export type T__5 = number;
+ }
+ export let v__4: B__4;
+ export let w__4: B__4.T;
+}
+namespace A__2 {
+ export interface B__4 {
+ z__0: number;
+ }
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_private/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_private/input.ts
new file mode 100644
index 000000000000..83adec678206
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_private/input.ts
@@ -0,0 +1,13 @@
+namespace Foo {
+ export var a;
+ var b;
+}
+
+var a, b, c;
+
+namespace Foo {
+ var b;
+ foo(a);
+ foo(b);
+ foo(c);
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_private/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_private/output.ts
new file mode 100644
index 000000000000..0e5c17e6b023
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_private/output.ts
@@ -0,0 +1,11 @@
+namespace Foo__2 {
+ export var a__3;
+ var b__4;
+}
+var a__2, b__2, c__2;
+namespace Foo__2 {
+ var b__5;
+ foo(a__3);
+ foo(b__5);
+ foo(c__2);
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_private_export_import_shadow/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_private_export_import_shadow/input.ts
new file mode 100644
index 000000000000..602f7eb3f9a1
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_private_export_import_shadow/input.ts
@@ -0,0 +1,15 @@
+namespace O {
+ export const o = 1;
+}
+namespace Outer {
+ export import Inner = O;
+ namespace Inner {
+ export const a = 1;
+ }
+}
+const a = 2;
+namespace Outer {
+ namespace Inner {
+ export const b = a;
+ }
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_private_export_import_shadow/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_private_export_import_shadow/output.ts
new file mode 100644
index 000000000000..8250b3bc303e
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_private_export_import_shadow/output.ts
@@ -0,0 +1,15 @@
+namespace O__2 {
+ export const o__3 = 1;
+}
+namespace Outer__2 {
+ export import Inner__5 = O__2;
+ namespace Inner__4 {
+ export const a__6 = 1;
+ }
+}
+const a__2 = 2;
+namespace Outer__2 {
+ namespace Inner__7 {
+ export const b__8 = a__2;
+ }
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_type_only_export_import/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_type_only_export_import/input.ts
new file mode 100644
index 000000000000..25a246e4fb3a
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_type_only_export_import/input.ts
@@ -0,0 +1,11 @@
+namespace Outer {
+ export interface I {}
+}
+
+namespace N {
+ export import type A = Outer;
+}
+
+namespace N {
+ export type T = A.I;
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_type_only_export_import/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_type_only_export_import/output.ts
new file mode 100644
index 000000000000..4da2c1d433b4
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_type_only_export_import/output.ts
@@ -0,0 +1,10 @@
+namespace Outer__2 {
+ export interface I__3 {
+ }
+}
+namespace N__2 {
+ export import type A__4 = Outer__2;
+}
+namespace N__2 {
+ export type T__4 = A__4.I;
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/type-alias/hoisting/1/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/type-alias/hoisting/1/output.ts
index 93b4d279f20b..a611ff52892d 100644
--- a/crates/swc_ecma_transforms_base/tests/ts-resolver/type-alias/hoisting/1/output.ts
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/type-alias/hoisting/1/output.ts
@@ -3,9 +3,9 @@ namespace Foo__2 {
export class IncomingMessage__3 extends http.IncomingMessage {
originalUrl?: http.IncomingMessage["url"] | undefined;
}
- type NextFunction__3 = (err?: any) => void;
+ type NextFunction__4 = (err?: any) => void;
export type SimpleHandleFunction__3 = (req: IncomingMessage__3, res: http.ServerResponse) => void;
- export type NextHandleFunction__3 = (req: IncomingMessage__3, res: http.ServerResponse, next: NextFunction__3) => void;
- export type ErrorHandleFunction__3 = (err: any, req: IncomingMessage__3, res: http.ServerResponse, next: NextFunction__3) => void;
+ export type NextHandleFunction__3 = (req: IncomingMessage__3, res: http.ServerResponse, next: NextFunction__4) => void;
+ export type ErrorHandleFunction__3 = (err: any, req: IncomingMessage__3, res: http.ServerResponse, next: NextFunction__4) => void;
export type HandleFunction__3 = SimpleHandleFunction__3 | NextHandleFunction__3 | ErrorHandleFunction__3;
}
diff --git a/crates/swc_ecma_transforms_typescript/src/retain.rs b/crates/swc_ecma_transforms_typescript/src/retain.rs
index c9277ff2941e..a6d52f2baf26 100644
--- a/crates/swc_ecma_transforms_typescript/src/retain.rs
+++ b/crates/swc_ecma_transforms_typescript/src/retain.rs
@@ -1,4 +1,5 @@
use swc_ecma_ast::*;
+use swc_ecma_transforms_base::ts_module_is_instantiated;
/// Returns true if a module item should survive TS type-stripping.
pub(crate) fn should_retain_module_item(module_item: &ModuleItem, in_namespace: bool) -> bool {
@@ -37,25 +38,15 @@ pub(crate) trait IsConcrete {
fn is_concrete(&self) -> bool;
}
+/// Delegates to the resolver's instantiation predicate so the emitter and
+/// the resolver classify namespaces identically. A namespace whose only
+/// members are bodyless function declarations (ambient `declare function`
+/// members or overload signatures) is instantiated: TypeScript still
+/// creates the namespace object for it, even though the function
+/// declarations themselves are erased.
impl IsConcrete for TsModuleDecl {
fn is_concrete(&self) -> bool {
- self.body
- .as_ref()
- .map(|body| body.is_concrete())
- .unwrap_or_default()
- }
-}
-
-impl IsConcrete for TsNamespaceBody {
- fn is_concrete(&self) -> bool {
- match self {
- Self::TsModuleBlock(ts_module_block) => {
- ts_module_block.body.iter().any(|item| item.is_concrete())
- }
- Self::TsNamespaceDecl(ts_namespace_decl) => ts_namespace_decl.body.is_concrete(),
- #[cfg(swc_ast_unknown)]
- _ => panic!("unable to access unknown nodes"),
- }
+ ts_module_is_instantiated(self)
}
}
diff --git a/crates/swc_ecma_transforms_typescript/src/semantic.rs b/crates/swc_ecma_transforms_typescript/src/semantic.rs
index 8d0e138c74ae..5279ca261f41 100644
--- a/crates/swc_ecma_transforms_typescript/src/semantic.rs
+++ b/crates/swc_ecma_transforms_typescript/src/semantic.rs
@@ -1,4 +1,5 @@
use rustc_hash::{FxHashMap, FxHashSet};
+use swc_atoms::Atom;
use swc_common::{Mark, Span, SyntaxContext};
use swc_ecma_ast::*;
use swc_ecma_utils::{find_pat_ids, stack_size::maybe_grow_default};
@@ -16,6 +17,24 @@ pub(crate) struct SemanticInfo {
pub id_type: FxHashSet,
pub id_value: FxHashSet,
pub exported_binding: FxHashMap>,
+ /// One shared [`SyntaxContext`] per namespace declaration id.
+ ///
+ /// The resolver binds every exported value member of a merged namespace
+ /// (across all re-opens) with one shared instance mark, so every routed
+ /// member of one namespace carries the same context. The transform uses
+ /// this to recognize refs to members of the namespace instance currently
+ /// being emitted, including forward refs to members declared by a later
+ /// re-open.
+ pub member_ctxt: FxHashMap,
+ /// Per namespace body (keyed by the declaration's span, which is
+ /// unique per re-open even when re-opened declarations share an id):
+ /// the exported member names whose declarations stay local bindings in
+ /// that body's emit (an exported function or class keeps
+ /// `function f() {}` / `class C {}` plus a trailing `N.f = f`
+ /// assignment). A same-body reference to such a member must stay bare
+ /// (the local binding is the one in scope, and function references
+ /// rely on hoisting), while a cross-body reference must be qualified.
+ pub local_member: FxHashMap>,
pub enum_record: TsEnumRecord,
pub const_enum: FxHashSet,
pub namespace_import_equals_usage: FxHashSet,
@@ -58,6 +77,8 @@ pub(crate) fn analyze_program(
import_chain: Default::default(),
namespace_block_stack: Default::default(),
namespace_id: None,
+ namespace_span: None,
+ namespace_parent: Default::default(),
skip_transform_info: false,
flow_syntax,
};
@@ -73,6 +94,12 @@ struct SemanticAnalyzer {
import_chain: FxHashMap,
namespace_block_stack: Vec,
namespace_id: Option,
+ /// Span of the namespace declaration whose body is being visited; keys
+ /// [SemanticInfo::local_member] per body.
+ namespace_span: Option,
+ /// Exported nested namespace declaration id -> enclosing namespace
+ /// body id; drives [Self::propagate_member_ctxt].
+ namespace_parent: FxHashMap,
skip_transform_info: bool,
flow_syntax: bool,
}
@@ -118,6 +145,7 @@ impl NamespaceBlock {
impl SemanticAnalyzer {
fn finish(mut self) -> SemanticInfo {
self.analyze_import_chain();
+ self.propagate_member_ctxt();
self.info
}
@@ -143,6 +171,70 @@ impl SemanticAnalyzer {
self.info.usage.extend(new_usage);
}
+ /// Record the shared member context of the current namespace. All routed
+ /// members of one namespace carry the same context, so the first insert
+ /// wins.
+ fn record_member_ctxt(&mut self, ctxt: SyntaxContext) {
+ if let Some(namespace_id) = self.namespace_id.clone() {
+ self.info.member_ctxt.entry(namespace_id).or_insert(ctxt);
+ }
+ }
+
+ /// Record an exported member of the current namespace body whose
+ /// declaration keeps a local binding in the emitted body.
+ fn record_local_member(&mut self, sym: Atom) {
+ if let Some(namespace_span) = self.namespace_span {
+ self.info
+ .local_member
+ .entry(namespace_span)
+ .or_default()
+ .insert(sym);
+ }
+ }
+
+ /// The canonical grouping key of a namespace body id: the chain of
+ /// exported-namespace names from the outermost registered ancestor.
+ /// Re-opened bodies of one merged namespace map to one key: top-level
+ /// (and same-parent private) re-opens share their declaration id, and
+ /// exported nested re-opens under different parent bodies share the
+ /// ancestor chain.
+ fn canonical_namespace_key(&self, id: &Id) -> (Id, Vec) {
+ self.namespace_parent
+ .get(id)
+ .map(|parent| {
+ let (root, mut path) = self.canonical_namespace_key(parent);
+ path.push(id.0.clone());
+ (root, path)
+ })
+ .unwrap_or_else(|| (id.clone(), Vec::new()))
+ }
+
+ /// Share the recorded member context across re-opened bodies of one
+ /// merged namespace. A body whose exports are all namespaces records
+ /// no member context itself, and exported nested re-opens under
+ /// different parent bodies have distinct declaration ids: group the
+ /// bodies by canonical key, then fill each unrecorded body from its
+ /// group, so every body arms the cross-body reference rewrite.
+ fn propagate_member_ctxt(&mut self) {
+ let group_ctxt: FxHashMap<(Id, Vec), SyntaxContext> = self
+ .info
+ .member_ctxt
+ .iter()
+ .map(|(id, ctxt)| (self.canonical_namespace_key(id), *ctxt))
+ .collect();
+ let filled: Vec<(Id, SyntaxContext)> = self
+ .namespace_parent
+ .keys()
+ .filter(|id| !self.info.member_ctxt.contains_key(*id))
+ .filter_map(|id| {
+ group_ctxt
+ .get(&self.canonical_namespace_key(id))
+ .map(|ctxt| (id.clone(), *ctxt))
+ })
+ .collect();
+ self.info.member_ctxt.extend(filled);
+ }
+
fn collect_top_level_module_item(&mut self, item: &ModuleItem) {
match item {
ModuleItem::Stmt(Stmt::Decl(decl)) => self.collect_decl(decl),
@@ -362,6 +454,9 @@ impl Visit for SemanticAnalyzer {
self.info
.exported_binding
.insert(node.id.to_id(), self.namespace_id.clone());
+ if !node.is_type_only {
+ self.record_member_ctxt(node.id.ctxt);
+ }
}
if node.is_type_only {
@@ -404,6 +499,7 @@ impl Visit for SemanticAnalyzer {
match &node.decl {
Decl::Var(var_decl) => {
let ids: Vec = find_pat_ids(&var_decl.decls);
+ ids.iter().for_each(|id| self.record_member_ctxt(id.1));
self.info.exported_binding.extend(
ids.into_iter()
.zip(std::iter::repeat(self.namespace_id.clone())),
@@ -413,12 +509,31 @@ impl Visit for SemanticAnalyzer {
self.info
.exported_binding
.insert(ts_enum_decl.id.to_id(), self.namespace_id.clone());
+ self.record_member_ctxt(ts_enum_decl.id.ctxt);
+ }
+ // Exported functions and classes are deliberately absent from
+ // `exported_binding` (their declarations keep local bindings in
+ // the emitted body), but they carry the shared instance context
+ // like every routed value member: record it so bodies whose
+ // exports are only functions/classes still arm the
+ // cross-body-reference rewrite, and record the name as locally
+ // bound so same-body references stay bare.
+ Decl::Fn(fn_decl) => {
+ self.record_member_ctxt(fn_decl.ident.ctxt);
+ self.record_local_member(fn_decl.ident.sym.clone());
+ }
+ Decl::Class(class_decl) => {
+ self.record_member_ctxt(class_decl.ident.ctxt);
+ self.record_local_member(class_decl.ident.sym.clone());
}
Decl::TsModule(ts_module_decl) => {
if let TsModuleName::Ident(ident) = &ts_module_decl.id {
self.info
.exported_binding
.insert(ident.to_id(), self.namespace_id.clone());
+ if let Some(parent) = self.namespace_id.clone() {
+ self.namespace_parent.insert(ident.to_id(), parent);
+ }
}
}
_ => {}
@@ -473,10 +588,12 @@ impl Visit for SemanticAnalyzer {
}
let namespace_id = self.namespace_id.replace(node.id.to_id());
+ let namespace_span = self.namespace_span.replace(node.span);
node.body.visit_with(self);
self.namespace_id = namespace_id;
+ self.namespace_span = namespace_span;
}
fn visit_ts_module_decl(&mut self, node: &TsModuleDecl) {
@@ -499,9 +616,11 @@ impl Visit for SemanticAnalyzer {
};
let namespace_id = self.namespace_id.replace(id);
+ let namespace_span = self.namespace_span.replace(node.span);
body.visit_with(self);
self.namespace_id = namespace_id;
+ self.namespace_span = namespace_span;
}
fn visit_ts_module_block(&mut self, node: &TsModuleBlock) {
diff --git a/crates/swc_ecma_transforms_typescript/src/transform.rs b/crates/swc_ecma_transforms_typescript/src/transform.rs
index 0f530f21b19b..f1ed1c56bf43 100644
--- a/crates/swc_ecma_transforms_typescript/src/transform.rs
+++ b/crates/swc_ecma_transforms_typescript/src/transform.rs
@@ -120,10 +120,11 @@ impl VisitMut for Transform {
);
fn visit_mut_program(&mut self, node: &mut Program) {
- if !self.semantic.exported_binding.is_empty() {
+ if !self.semantic.exported_binding.is_empty() || !self.semantic.member_ctxt.is_empty() {
self.ref_rewriter = Some(RefRewriter {
query: ExportQuery {
export_name: self.semantic.exported_binding.clone(),
+ namespace_stack: Vec::new(),
},
});
}
@@ -326,21 +327,25 @@ impl VisitMut for Transform {
fn visit_mut_ts_namespace_decl(&mut self, node: &mut TsNamespaceDecl) {
let id = node.id.to_id();
+ let pushed = self.enter_current_namespace(&id, node.span);
let namespace_id = self.namespace_id.replace(id);
node.body.visit_mut_with(self);
self.namespace_id = namespace_id;
+ self.exit_current_namespace(pushed);
}
fn visit_mut_ts_module_decl(&mut self, node: &mut TsModuleDecl) {
let id = node.id.to_id();
+ let pushed = self.enter_current_namespace(&id, node.span);
let namespace_id = self.namespace_id.replace(id);
node.body.visit_mut_with(self);
self.namespace_id = namespace_id;
+ self.exit_current_namespace(pushed);
}
fn visit_mut_stmt(&mut self, node: &mut Stmt) {
@@ -680,6 +685,49 @@ enum FoldedDecl {
}
impl Transform {
+ /// Push the namespace body being entered onto the export query's
+ /// namespace stack: the body's alias id, the shared member context the
+ /// analyzer recorded for it, and the body's locally-bound member names
+ /// (keyed by the declaration's span, which is unique per re-open).
+ /// Returns whether a frame was pushed. Nothing is pushed when the body
+ /// has no routed value members, when no rewriter is active, or when the
+ /// recorded context equals the namespace id's own binding context: that
+ /// happens for dotted bodies (`namespace A.B`), whose exported members
+ /// bind with the enclosing scope's mark, so context equality would match
+ /// every body-local binding rather than just members.
+ fn enter_current_namespace(&mut self, id: &Id, span: Span) -> bool {
+ let frame = self
+ .semantic
+ .member_ctxt
+ .get(id)
+ .filter(|ctxt| **ctxt != id.1)
+ .map(|ctxt| NamespaceFrame {
+ alias: id.clone(),
+ member_ctxt: *ctxt,
+ local_members: self
+ .semantic
+ .local_member
+ .get(&span)
+ .cloned()
+ .unwrap_or_default(),
+ });
+
+ self.ref_rewriter
+ .as_mut()
+ .zip(frame)
+ .map(|(ref_rewriter, frame)| ref_rewriter.query.namespace_stack.push(frame))
+ .is_some()
+ }
+
+ /// Pop the frame pushed by [`Self::enter_current_namespace`], if any.
+ fn exit_current_namespace(&mut self, pushed: bool) {
+ let _ = self
+ .ref_rewriter
+ .as_mut()
+ .filter(|_| pushed)
+ .and_then(|ref_rewriter| ref_rewriter.query.namespace_stack.pop());
+ }
+
fn normalize_flow_static_constructor_key(
&self,
key: &mut PropName,
@@ -1868,16 +1916,56 @@ impl Transform {
}
}
+/// One enclosing namespace body during emit: the body's alias id, the
+/// shared member [`SyntaxContext`] of the namespace instance, and the
+/// member names whose declarations stay local bindings in this body's
+/// emit.
+struct NamespaceFrame {
+ alias: Id,
+ member_ctxt: SyntaxContext,
+ local_members: FxHashSet,
+}
+
struct ExportQuery {
export_name: FxHashMap>,
+ /// The namespace bodies currently being emitted, innermost last. A ref
+ /// whose context equals a frame's member context is a member of that
+ /// namespace instance, even when its symbol is absent from
+ /// `export_name` (a forward ref to a member declared by a later
+ /// re-open).
+ namespace_stack: Vec,
+}
+
+impl ExportQuery {
+ /// The alias to qualify `ident` with when it refers to a member of an
+ /// enclosing namespace instance currently being emitted. Detected by
+ /// context equality: every routed member of one namespace instance
+ /// carries one shared context, so refs to members seeded by another
+ /// re-open body match without an `export_name` entry. A member whose
+ /// declaration keeps a local binding in the matched body (an exported
+ /// function or class) stays unqualified: the local binding is in scope
+ /// at the reference site (from nested namespace bodies too), and
+ /// function references rely on hoisting.
+ fn query_current_member(&self, ident: &Ident) -> Option {
+ self.namespace_stack
+ .iter()
+ .rev()
+ .find(|frame| frame.member_ctxt == ident.ctxt)
+ .filter(|frame| !frame.local_members.contains(&ident.sym))
+ .map(|frame| frame.alias.clone())
+ }
}
impl QueryRef for ExportQuery {
fn query_ref(&self, export_name: &Ident) -> Option> {
- self.export_name
- .get(&export_name.to_id())?
- .clone()
- .map(|namespace_id| namespace_id.make_member(export_name.clone().into()).into())
+ self.query_current_member(export_name)
+ .map(|alias| alias.make_member(export_name.clone().into()).into())
+ .or_else(|| {
+ self.export_name
+ .get(&export_name.to_id())?
+ .clone()
+ .map(|namespace_id| namespace_id.make_member(export_name.clone().into()).into())
+ })
}
fn query_lhs(&self, ident: &Ident) -> Option> {
@@ -1885,17 +1973,28 @@ impl QueryRef for ExportQuery {
}
fn query_jsx(&self, ident: &Ident) -> Option {
- self.export_name
- .get(&ident.to_id())?
- .clone()
- .map(|namespace_id| {
+ self.query_current_member(ident)
+ .map(|alias| {
JSXMemberExpr {
span: DUMMY_SP,
- obj: JSXObject::Ident(namespace_id.into()),
+ obj: JSXObject::Ident(alias.into()),
prop: ident.clone().into(),
}
.into()
})
+ .or_else(|| {
+ self.export_name
+ .get(&ident.to_id())?
+ .clone()
+ .map(|namespace_id| {
+ JSXMemberExpr {
+ span: DUMMY_SP,
+ obj: JSXObject::Ident(namespace_id.into()),
+ prop: ident.clone().into(),
+ }
+ .into()
+ })
+ })
}
}
diff --git a/crates/swc_ecma_transforms_typescript/tests/__swc_snapshots__/tests/strip.rs/namespace_004.js b/crates/swc_ecma_transforms_typescript/tests/__swc_snapshots__/tests/strip.rs/namespace_004.js
index bf2ef8ece779..cb829dc2cf42 100644
--- a/crates/swc_ecma_transforms_typescript/tests/__swc_snapshots__/tests/strip.rs/namespace_004.js
+++ b/crates/swc_ecma_transforms_typescript/tests/__swc_snapshots__/tests/strip.rs/namespace_004.js
@@ -11,7 +11,7 @@
(function(MyNamespace) {
(function(MyEnum) {
MyEnum[MyEnum["B"] = 1] = "B";
- })(MyNamespace.MyEnum || (MyNamespace.MyEnum = {}));
+ })(MyNamespace.MyEnum);
(function(MyInnerNamespace) {
MyInnerNamespace.Dec2 = 2;
})(MyNamespace.MyInnerNamespace || (MyNamespace.MyInnerNamespace = {}));
diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-and-enum/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-and-enum/output.js
index 5b4f907dfc7c..d42cde3c20fe 100644
--- a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-and-enum/output.js
+++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-and-enum/output.js
@@ -30,7 +30,7 @@ var E1 = /*#__PURE__*/ function(E1) {
(function(N) {
(function(E) {
E[E["c"] = 3] = "c";
- })(N.E || (N.E = {}));
+ })(N.E);
(function(E) {
E[E["d"] = 4] = "d";
})(N.E);
@@ -42,7 +42,7 @@ var E1 = /*#__PURE__*/ function(E1) {
(function(N1) {
(function(E2) {
E2[E2["f"] = 6] = "f";
- })(N1.E2 || (N1.E2 = {}));
+ })(N1.E2);
})(N.N1 || (N.N1 = {}));
(function(N2) {
let E3 = /*#__PURE__*/ function(E3) {
@@ -60,6 +60,6 @@ var E1 = /*#__PURE__*/ function(E1) {
(function(N1) {
(function(E2) {
E2[E2["f"] = 6] = "f";
- })(N1.E2 || (N1.E2 = {}));
+ })(N1.E2);
})(N1 || (N1 = {}));
export var N, N1;
diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/ambient-fn-namespace/input.ts b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/ambient-fn-namespace/input.ts
new file mode 100644
index 000000000000..d0448ac50af2
--- /dev/null
+++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/ambient-fn-namespace/input.ts
@@ -0,0 +1,16 @@
+namespace N {
+ export const x = C;
+}
+namespace N {
+ export namespace C {
+ export declare function f(): void;
+ }
+}
+namespace M {
+ export const y = D;
+}
+namespace M {
+ export namespace D {
+ declare function g(): void;
+ }
+}
diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/ambient-fn-namespace/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/ambient-fn-namespace/output.js
new file mode 100644
index 000000000000..91d85b5fae4b
--- /dev/null
+++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/ambient-fn-namespace/output.js
@@ -0,0 +1,13 @@
+(function(N) {
+ N.x = N.C;
+})(N || (N = {}));
+(function(N) {
+ (function(C) {})(N.C || (N.C = {}));
+})(N || (N = {}));
+(function(M) {
+ M.y = M.D;
+})(M || (M = {}));
+(function(M) {
+ (function(D) {})(M.D || (M.D = {}));
+})(M || (M = {}));
+var N, M;
diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/declare-namespace-member/input.ts b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/declare-namespace-member/input.ts
new file mode 100644
index 000000000000..cb43b775c697
--- /dev/null
+++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/declare-namespace-member/input.ts
@@ -0,0 +1,8 @@
+namespace N {
+ export const x = C;
+}
+namespace N {
+ export declare namespace C {
+ function f(): void;
+ }
+}
diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/declare-namespace-member/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/declare-namespace-member/output.js
new file mode 100644
index 000000000000..2643bca108f2
--- /dev/null
+++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/declare-namespace-member/output.js
@@ -0,0 +1,5 @@
+(function(N) {
+ N.x = N.C;
+})(N || (N = {}));
+(function(N) {})(N || (N = {}));
+var N;
diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/deep-ambient-fn/input.ts b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/deep-ambient-fn/input.ts
new file mode 100644
index 000000000000..8d9b4add418c
--- /dev/null
+++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/deep-ambient-fn/input.ts
@@ -0,0 +1,10 @@
+namespace N {
+ export const x = C;
+}
+namespace N {
+ export namespace C {
+ export namespace D {
+ export declare function f(): void;
+ }
+ }
+}
diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/deep-ambient-fn/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/deep-ambient-fn/output.js
new file mode 100644
index 000000000000..966ea6a1f725
--- /dev/null
+++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/deep-ambient-fn/output.js
@@ -0,0 +1,9 @@
+(function(N) {
+ N.x = N.C;
+})(N || (N = {}));
+(function(N) {
+ (function(C) {
+ (function(D) {})(C.D || (C.D = {}));
+ })(N.C || (N.C = {}));
+})(N || (N = {}));
+var N;
diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/dotted-body-locals/input.ts b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/dotted-body-locals/input.ts
new file mode 100644
index 000000000000..a2246d52c94d
--- /dev/null
+++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/dotted-body-locals/input.ts
@@ -0,0 +1,5 @@
+namespace A.B {
+ const t = 1;
+ export const x = t;
+ export const y = B.x;
+}
diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/dotted-body-locals/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/dotted-body-locals/output.js
new file mode 100644
index 000000000000..76ea06b7719e
--- /dev/null
+++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/dotted-body-locals/output.js
@@ -0,0 +1,8 @@
+(function(A) {
+ (function(B) {
+ const t = 1;
+ B.x = t;
+ B.y = B.x;
+ })(A.B || (A.B = {}));
+})(A || (A = {}));
+var A;
diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-class-members/input.ts b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-class-members/input.ts
new file mode 100644
index 000000000000..b53bda30226d
--- /dev/null
+++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-class-members/input.ts
@@ -0,0 +1,15 @@
+namespace T {
+ export function f() {
+ return 10;
+ }
+}
+namespace T {
+ export function g() {
+ return f();
+ }
+ export class K {
+ v = 2;
+ }
+ export const k = new K().v;
+ export const h = g() * k;
+}
diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-class-members/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-class-members/output.js
new file mode 100644
index 000000000000..ca7f593c3bbd
--- /dev/null
+++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-class-members/output.js
@@ -0,0 +1,21 @@
+(function(T) {
+ function f() {
+ return 10;
+ }
+ T.f = f;
+})(T || (T = {}));
+(function(T) {
+ function g() {
+ return T.f();
+ }
+ T.g = g;
+ class K {
+ constructor(){
+ this.v = 2;
+ }
+ }
+ T.K = K;
+ T.k = new K().v;
+ T.h = g() * T.k;
+})(T || (T = {}));
+var T;
diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-class-only-bodies/input.ts b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-class-only-bodies/input.ts
new file mode 100644
index 000000000000..2f219f2ec662
--- /dev/null
+++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-class-only-bodies/input.ts
@@ -0,0 +1,10 @@
+namespace U {
+ export class C {
+ w = 3;
+ }
+}
+namespace U {
+ export function make() {
+ return new C().w;
+ }
+}
diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-class-only-bodies/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-class-only-bodies/output.js
new file mode 100644
index 000000000000..f17f7242651e
--- /dev/null
+++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-class-only-bodies/output.js
@@ -0,0 +1,15 @@
+(function(U) {
+ class C {
+ constructor(){
+ this.w = 3;
+ }
+ }
+ U.C = C;
+})(U || (U = {}));
+(function(U) {
+ function make() {
+ return new U.C().w;
+ }
+ U.make = make;
+})(U || (U = {}));
+var U;
diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-ns-value-merge/input.ts b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-ns-value-merge/input.ts
new file mode 100644
index 000000000000..6a252bcd2e16
--- /dev/null
+++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-ns-value-merge/input.ts
@@ -0,0 +1,18 @@
+namespace N {
+ export const x = foo;
+}
+namespace N {
+ export function foo() { return 1; }
+ export namespace foo { export interface Options {} }
+}
+namespace M {
+ export const before = B;
+}
+namespace M {
+ export namespace B {
+ export interface I {}
+ }
+ export namespace B {
+ export const c = 1;
+ }
+}
diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-ns-value-merge/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-ns-value-merge/output.js
new file mode 100644
index 000000000000..1522f1b8f24f
--- /dev/null
+++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-ns-value-merge/output.js
@@ -0,0 +1,18 @@
+(function(N) {
+ N.x = N.foo;
+})(N || (N = {}));
+(function(N) {
+ function foo() {
+ return 1;
+ }
+ N.foo = foo;
+})(N || (N = {}));
+(function(M) {
+ M.before = M.B;
+})(M || (M = {}));
+(function(M) {
+ (function(B) {
+ B.c = 1;
+ })(M.B || (M.B = {}));
+})(M || (M = {}));
+var N, M;
diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/late-namespace-export/input.ts b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/late-namespace-export/input.ts
new file mode 100644
index 000000000000..541d838409e0
--- /dev/null
+++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/late-namespace-export/input.ts
@@ -0,0 +1,12 @@
+namespace N {
+ export const x = C;
+}
+namespace N {
+ export namespace C {
+ export const a = 1;
+ }
+}
+namespace N {
+ export const y = C;
+}
+const C = "outer";
diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/late-namespace-export/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/late-namespace-export/output.js
new file mode 100644
index 000000000000..cbb89b8fe9dc
--- /dev/null
+++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/late-namespace-export/output.js
@@ -0,0 +1,13 @@
+(function(N) {
+ N.x = N.C;
+})(N || (N = {}));
+(function(N) {
+ (function(C) {
+ C.a = 1;
+ })(N.C || (N.C = {}));
+})(N || (N = {}));
+(function(N) {
+ N.y = N.C;
+})(N || (N = {}));
+const C = "outer";
+var N;
diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-chain/input.ts b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-chain/input.ts
new file mode 100644
index 000000000000..faadcdfb3222
--- /dev/null
+++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-chain/input.ts
@@ -0,0 +1,12 @@
+namespace P {
+ export namespace N {
+ export const a = 1;
+ }
+}
+namespace P {
+ export namespace N {
+ export namespace M {
+ export const c = a;
+ }
+ }
+}
diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-chain/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-chain/output.js
new file mode 100644
index 000000000000..22e0923566f8
--- /dev/null
+++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-chain/output.js
@@ -0,0 +1,13 @@
+(function(P) {
+ (function(N) {
+ N.a = 1;
+ })(P.N || (P.N = {}));
+})(P || (P = {}));
+(function(P) {
+ (function(N) {
+ (function(M) {
+ M.c = N.a;
+ })(N.M || (N.M = {}));
+ })(P.N || (P.N = {}));
+})(P || (P = {}));
+var P;
diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-fn-ref/input.ts b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-fn-ref/input.ts
new file mode 100644
index 000000000000..894539f4bcb8
--- /dev/null
+++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-fn-ref/input.ts
@@ -0,0 +1,8 @@
+namespace T {
+ export function f() {
+ return 5;
+ }
+ export namespace Sub {
+ export const v = f();
+ }
+}
diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-fn-ref/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-fn-ref/output.js
new file mode 100644
index 000000000000..255de7180d0b
--- /dev/null
+++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-fn-ref/output.js
@@ -0,0 +1,10 @@
+(function(T) {
+ function f() {
+ return 5;
+ }
+ T.f = f;
+ (function(Sub) {
+ Sub.v = f();
+ })(T.Sub || (T.Sub = {}));
+})(T || (T = {}));
+var T;
diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-reopen/input.ts b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-reopen/input.ts
new file mode 100644
index 000000000000..08f34286047d
--- /dev/null
+++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-reopen/input.ts
@@ -0,0 +1,10 @@
+namespace N {
+ export namespace I {
+ export const a = 1;
+ }
+}
+namespace N {
+ export namespace I {
+ export const b = a;
+ }
+}
diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-reopen/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-reopen/output.js
new file mode 100644
index 000000000000..36dfa725f919
--- /dev/null
+++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-reopen/output.js
@@ -0,0 +1,11 @@
+(function(N) {
+ (function(I) {
+ I.a = 1;
+ })(N.I || (N.I = {}));
+})(N || (N = {}));
+(function(N) {
+ (function(I) {
+ I.b = I.a;
+ })(N.I || (N.I = {}));
+})(N || (N = {}));
+var N;
diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/private-alias-shadow/input.ts b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/private-alias-shadow/input.ts
new file mode 100644
index 000000000000..602f7eb3f9a1
--- /dev/null
+++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/private-alias-shadow/input.ts
@@ -0,0 +1,15 @@
+namespace O {
+ export const o = 1;
+}
+namespace Outer {
+ export import Inner = O;
+ namespace Inner {
+ export const a = 1;
+ }
+}
+const a = 2;
+namespace Outer {
+ namespace Inner {
+ export const b = a;
+ }
+}
diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/private-alias-shadow/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/private-alias-shadow/output.js
new file mode 100644
index 000000000000..d0b8fe5775c8
--- /dev/null
+++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/private-alias-shadow/output.js
@@ -0,0 +1,18 @@
+(function(O) {
+ O.o = 1;
+})(O || (O = {}));
+(function(Outer) {
+ Outer.Inner = O;
+ (function(Inner) {
+ Inner.a = 1;
+ })(Inner || (Inner = {}));
+ var Inner;
+})(Outer || (Outer = {}));
+const a = 2;
+(function(Outer) {
+ (function(Inner) {
+ Inner.b = a;
+ })(Inner || (Inner = {}));
+ var Inner;
+})(Outer || (Outer = {}));
+var O, Outer;
diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/type-only-namespace-no-shadow/input.ts b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/type-only-namespace-no-shadow/input.ts
new file mode 100644
index 000000000000..fa2ce674ae52
--- /dev/null
+++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/type-only-namespace-no-shadow/input.ts
@@ -0,0 +1,16 @@
+var C = "outer";
+namespace N {
+ export namespace C {
+ export interface I {}
+ }
+}
+namespace N {
+ export const x = C;
+}
+var D = "outer";
+namespace M {
+ export const y = D;
+}
+namespace M {
+ export namespace D {}
+}
diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/type-only-namespace-no-shadow/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/type-only-namespace-no-shadow/output.js
new file mode 100644
index 000000000000..5b07a8b6e2fb
--- /dev/null
+++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/type-only-namespace-no-shadow/output.js
@@ -0,0 +1,9 @@
+var C = "outer";
+(function(N) {
+ N.x = C;
+})(N || (N = {}));
+var D = "outer";
+(function(M) {
+ M.y = D;
+})(M || (M = {}));
+var N, M;