Skip to content

Commit

Permalink
fix #1410: support typescript sibling namespaces
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Nov 23, 2021
1 parent 5f6b41b commit 7079603
Show file tree
Hide file tree
Showing 6 changed files with 917 additions and 42 deletions.
64 changes: 64 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,70 @@ In addition to the breaking changes above, the following changes are also includ

* Forbidden keywords: `break`, `case`, `catch`, `class`, `const`, `continue`, `debugger`, `default`, `delete`, `do`, `else`, `enum`, `export`, `extends`, `finally`, `for`, `if`, `in`, `instanceof`, `return`, `super`, `switch`, `throw`, `try`, `var`, `while`, `with`

* Support sibling namespaces in TypeScript ([#1410](https://github.com/evanw/esbuild/issues/1410))

TypeScript has a feature where sibling namespaces with the same name can implicitly reference each other's exports without an explicit property access. This goes against how scope lookup works in JavaScript, so it previously didn't work with esbuild. This release adds support for this feature:

```ts
// Original TypeScript code
namespace x {
export let y = 123
}
namespace x {
export let z = y
}

// Old JavaScript output
var x;
(function(x2) {
x2.y = 123;
})(x || (x = {}));
(function(x2) {
x2.z = y;
})(x || (x = {}));

// New JavaScript output
var x;
(function(x2) {
x2.y = 123;
})(x || (x = {}));
(function(x2) {
x2.z = x2.y;
})(x || (x = {}));
```

Notice how the identifier `y` is now compiled to the property access `x2.y` which references the export named `y` on the namespace, instead of being left as the identifier `y` which references the global named `y`. This matches how the TypeScript compiler treats namespace objects. This new behavior also works for enums:

```ts
// Original TypeScript code
enum x {
y = 123
}
enum x {
z = y + 1
}

// Old JavaScript output
var x;
(function(x2) {
x2[x2["y"] = 123] = "y";
})(x || (x = {}));
(function(x2) {
x2[x2["z"] = y + 1] = "z";
})(x || (x = {}));

// New JavaScript output
var x;
(function(x2) {
x2[x2["y"] = 123] = "y";
})(x || (x = {}));
(function(x2) {
x2[x2["z"] = 124] = "z";
})(x || (x = {}));
```

Note that this behavior does **not** work across files. Each file is still compiled independently so the namespaces in each file are still resolved independently per-file. Implicit namespace cross-references still do not work across files. Getting this to work is counter to esbuild's parallel architecture and does not fit in with esbuild's design. It also doesn't make sense with esbuild's bundling model where input files are either in ESM or CommonJS format and therefore each have their own scope.

## 0.13.15

* Fix `super` in lowered `async` arrow functions ([#1777](https://github.com/evanw/esbuild/issues/1777))
Expand Down
177 changes: 177 additions & 0 deletions internal/bundler/bundler_ts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/evanw/esbuild/internal/compat"
"github.com/evanw/esbuild/internal/config"
"github.com/evanw/esbuild/internal/js_ast"
)

var ts_suite = suite{
Expand Down Expand Up @@ -1336,3 +1337,179 @@ node_modules/some-ts/package.json: note: "sideEffects" is false in the enclosing
`,
})
}

func TestTSSiblingNamespace(t *testing.T) {
ts_suite.expectBundled(t, bundled{
files: map[string]string{
"/let.ts": `
export namespace x { export let y = 123 }
export namespace x { export let z = y }
`,
"/function.ts": `
export namespace x { export function y() {} }
export namespace x { export let z = y }
`,
"/class.ts": `
export namespace x { export class y {} }
export namespace x { export let z = y }
`,
"/namespace.ts": `
export namespace x { export namespace y { 0 } }
export namespace x { export let z = y }
`,
"/enum.ts": `
export namespace x { export enum y {} }
export namespace x { export let z = y }
`,
},
entryPaths: []string{
"/let.ts",
"/function.ts",
"/class.ts",
"/namespace.ts",
"/enum.ts",
},
options: config.Options{
Mode: config.ModePassThrough,
AbsOutputDir: "/out",
},
})
}

func TestTSSiblingEnum(t *testing.T) {
ts_suite.expectBundled(t, bundled{
files: map[string]string{
"/number.ts": `
export enum x { y, yy = y }
export enum x { z = y + 1 }
declare let y: any, z: any
export namespace x { console.log(y, z) }
console.log(x.y, x.z)
`,
"/string.ts": `
export enum x { y = 'a', yy = y }
export enum x { z = y }
declare let y: any, z: any
export namespace x { console.log(y, z) }
console.log(x.y, x.z)
`,
"/propagation.ts": `
export enum a { b = 100 }
export enum x {
c = a.b,
d = c * 2,
e = x.d ** 2,
f = x['e'] / 4,
}
export enum x { g = f >> 4 }
console.log(a.b, a['b'], x.g, x['g'])
`,
"/nested-number.ts": `
export namespace foo { export enum x { y, yy = y } }
export namespace foo { export enum x { z = y + 1 } }
declare let y: any, z: any
export namespace foo.x {
console.log(y, z)
console.log(x.y, x.z)
}
`,
"/nested-string.ts": `
export namespace foo { export enum x { y = 'a', yy = y } }
export namespace foo { export enum x { z = y } }
declare let y: any, z: any
export namespace foo.x {
console.log(y, z)
console.log(x.y, x.z)
}
`,
"/nested-propagation.ts": `
export namespace n { export enum a { b = 100 } }
export namespace n {
export enum x {
c = n.a.b,
d = c * 2,
e = x.d ** 2,
f = x['e'] / 4,
}
}
export namespace n {
export enum x { g = f >> 4 }
console.log(a.b, n.a.b, n['a']['b'], x.g, n.x.g, n['x']['g'])
}
`,
},
entryPaths: []string{
"/number.ts",
"/string.ts",
"/propagation.ts",
"/nested-number.ts",
"/nested-string.ts",
"/nested-propagation.ts",
},
options: config.Options{
Mode: config.ModePassThrough,
AbsOutputDir: "/out",
},
})
}

func TestTSEnumJSX(t *testing.T) {
ts_suite.expectBundled(t, bundled{
files: map[string]string{
"/element.tsx": `
export enum Foo { Div = 'div' }
console.log(<Foo.Div />)
`,
"/fragment.tsx": `
export enum React { Fragment = 'div' }
console.log(<>test</>)
`,
"/nested-element.tsx": `
namespace x.y { export enum Foo { Div = 'div' } }
namespace x.y { console.log(<x.y.Foo.Div />) }
`,
"/nested-fragment.tsx": `
namespace x.y { export enum React { Fragment = 'div' } }
namespace x.y { console.log(<>test</>) }
`,
},
entryPaths: []string{
"/element.tsx",
"/fragment.tsx",
"/nested-element.tsx",
"/nested-fragment.tsx",
},
options: config.Options{
Mode: config.ModePassThrough,
AbsOutputDir: "/out",
},
})
}

func TestTSEnumDefine(t *testing.T) {
ts_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.ts": `
enum a { b = 123, c = d }
`,
},
entryPaths: []string{"/entry.ts"},
options: config.Options{
Mode: config.ModePassThrough,
AbsOutputDir: "/out",
Defines: &config.ProcessedDefines{
IdentifierDefines: map[string]config.DefineData{
"d": {
DefineFunc: func(args config.DefineArgs) js_ast.E {
return &js_ast.EIdentifier{Ref: args.FindSymbol(args.Loc, "b")}
},
},
},
},
},
})
}

0 comments on commit 7079603

Please sign in to comment.