From 7079603d3592bc2834a1d4bd16b1e8bff56fdb0f Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Tue, 23 Nov 2021 09:22:29 -0600 Subject: [PATCH] fix #1410: support typescript sibling namespaces --- CHANGELOG.md | 64 ++++++ internal/bundler/bundler_ts_test.go | 177 +++++++++++++++ internal/bundler/snapshots/snapshots_ts.txt | 235 ++++++++++++++++++++ internal/js_ast/js_ast.go | 112 ++++++++++ internal/js_parser/js_parser.go | 182 ++++++++++++--- internal/js_parser/ts_parser.go | 189 +++++++++++++++- 6 files changed, 917 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40047324419..d8e26b27303 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/internal/bundler/bundler_ts_test.go b/internal/bundler/bundler_ts_test.go index 1cb702635cc..5aecbc6e373 100644 --- a/internal/bundler/bundler_ts_test.go +++ b/internal/bundler/bundler_ts_test.go @@ -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{ @@ -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() + `, + "/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() } + `, + "/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")} + }, + }, + }, + }, + }, + }) +} diff --git a/internal/bundler/snapshots/snapshots_ts.txt b/internal/bundler/snapshots/snapshots_ts.txt index b1e7e402622..de767f1cd08 100644 --- a/internal/bundler/snapshots/snapshots_ts.txt +++ b/internal/bundler/snapshots/snapshots_ts.txt @@ -101,6 +101,67 @@ TestTSDeclareVar // entry.ts var foo = bar(); +================================================================================ +TestTSEnumDefine +---------- /out/entry.js ---------- +var a; +(function(a2) { + a2[a2["b"] = 123] = "b"; + a2[a2["c"] = 123] = "c"; +})(a || (a = {})); + +================================================================================ +TestTSEnumJSX +---------- /out/element.js ---------- +export var Foo; +(function(Foo2) { + Foo2["Div"] = "div"; +})(Foo || (Foo = {})); +console.log(/* @__PURE__ */ React.createElement("div", null)); + +---------- /out/fragment.js ---------- +export var React; +(function(React2) { + React2["Fragment"] = "div"; +})(React || (React = {})); +console.log(/* @__PURE__ */ React.createElement("div", null, "test")); + +---------- /out/nested-element.js ---------- +var x; +(function(x2) { + let y; + (function(y2) { + let Foo; + (function(Foo2) { + Foo2["Div"] = "div"; + })(Foo = y2.Foo || (y2.Foo = {})); + })(y = x2.y || (x2.y = {})); +})(x || (x = {})); +(function(x2) { + let y; + (function(y2) { + console.log(/* @__PURE__ */ React.createElement("div", null)); + })(y = x2.y || (x2.y = {})); +})(x || (x = {})); + +---------- /out/nested-fragment.js ---------- +var x; +(function(x2) { + let y; + (function(y2) { + let React; + (function(React2) { + React2["Fragment"] = "div"; + })(React = y2.React || (y2.React = {})); + })(y = x2.y || (x2.y = {})); +})(x || (x = {})); +(function(x2) { + let y; + (function(y2) { + console.log(/* @__PURE__ */ y2.React.createElement("div", null, "test")); + })(y = x2.y || (x2.y = {})); +})(x || (x = {})); + ================================================================================ TestTSExportDefaultTypeIssue316 ---------- /out.js ---------- @@ -353,6 +414,180 @@ var Foo;(function(e){let a;(function(p){foo(e,p)})(a=e.Bar||(e.Bar={}))})(Foo||( ---------- /b.js ---------- export var Foo;(function(e){let a;(function(p){foo(e,p)})(a=e.Bar||(e.Bar={}))})(Foo||(Foo={})); +================================================================================ +TestTSSiblingEnum +---------- /out/number.js ---------- +export var x; +(function(x2) { + x2[x2["y"] = 0] = "y"; + x2[x2["yy"] = 0] = "yy"; +})(x || (x = {})); +(function(x2) { + x2[x2["z"] = 1] = "z"; +})(x || (x = {})); +(function(x2) { + console.log(0, 1); +})(x || (x = {})); +console.log(0, 1); + +---------- /out/string.js ---------- +export var x; +(function(x2) { + x2["y"] = "a"; + x2["yy"] = "a"; +})(x || (x = {})); +(function(x2) { + x2["z"] = "a"; +})(x || (x = {})); +(function(x2) { + console.log("a", "a"); +})(x || (x = {})); +console.log("a", "a"); + +---------- /out/propagation.js ---------- +export var a; +(function(a2) { + a2[a2["b"] = 100] = "b"; +})(a || (a = {})); +export var x; +(function(x2) { + x2[x2["c"] = 100] = "c"; + x2[x2["d"] = 200] = "d"; + x2[x2["e"] = 4e4] = "e"; + x2[x2["f"] = 1e4] = "f"; +})(x || (x = {})); +(function(x2) { + x2[x2["g"] = 625] = "g"; +})(x || (x = {})); +console.log(100, 100, 625, 625); + +---------- /out/nested-number.js ---------- +export var foo; +(function(foo2) { + let x; + (function(x2) { + x2[x2["y"] = 0] = "y"; + x2[x2["yy"] = 0] = "yy"; + })(x = foo2.x || (foo2.x = {})); +})(foo || (foo = {})); +(function(foo2) { + let x; + (function(x2) { + x2[x2["z"] = 1] = "z"; + })(x = foo2.x || (foo2.x = {})); +})(foo || (foo = {})); +(function(foo2) { + let x; + (function(x2) { + console.log(0, 1); + console.log(0, 1); + })(x = foo2.x || (foo2.x = {})); +})(foo || (foo = {})); + +---------- /out/nested-string.js ---------- +export var foo; +(function(foo2) { + let x; + (function(x2) { + x2["y"] = "a"; + x2["yy"] = "a"; + })(x = foo2.x || (foo2.x = {})); +})(foo || (foo = {})); +(function(foo2) { + let x; + (function(x2) { + x2["z"] = "a"; + })(x = foo2.x || (foo2.x = {})); +})(foo || (foo = {})); +(function(foo2) { + let x; + (function(x2) { + console.log("a", "a"); + console.log("a", "a"); + })(x = foo2.x || (foo2.x = {})); +})(foo || (foo = {})); + +---------- /out/nested-propagation.js ---------- +export var n; +(function(n2) { + let a; + (function(a2) { + a2[a2["b"] = 100] = "b"; + })(a = n2.a || (n2.a = {})); +})(n || (n = {})); +(function(n2) { + let x; + (function(x2) { + x2[x2["c"] = 100] = "c"; + x2[x2["d"] = 200] = "d"; + x2[x2["e"] = 4e4] = "e"; + x2[x2["f"] = 1e4] = "f"; + })(x = n2.x || (n2.x = {})); +})(n || (n = {})); +(function(n2) { + let x; + (function(x2) { + x2[x2["g"] = 625] = "g"; + })(x = n2.x || (n2.x = {})); + console.log(100, 100, 100, 625, 625, 625); +})(n || (n = {})); + +================================================================================ +TestTSSiblingNamespace +---------- /out/let.js ---------- +export var x; +(function(x2) { + x2.y = 123; +})(x || (x = {})); +(function(x2) { + x2.z = x2.y; +})(x || (x = {})); + +---------- /out/function.js ---------- +export var x; +(function(x2) { + function y() { + } + x2.y = y; +})(x || (x = {})); +(function(x2) { + x2.z = x2.y; +})(x || (x = {})); + +---------- /out/class.js ---------- +export var x; +(function(x2) { + class y { + } + x2.y = y; +})(x || (x = {})); +(function(x2) { + x2.z = x2.y; +})(x || (x = {})); + +---------- /out/namespace.js ---------- +export var x; +(function(x2) { + let y; + (function(y2) { + 0; + })(y = x2.y || (x2.y = {})); +})(x || (x = {})); +(function(x2) { + x2.z = x2.y; +})(x || (x = {})); + +---------- /out/enum.js ---------- +export var x; +(function(x2) { + let y; + (function(y2) { + })(y = x2.y || (x2.y = {})); +})(x || (x = {})); +(function(x2) { + x2.z = x2.y; +})(x || (x = {})); + ================================================================================ TestTypeScriptDecorators ---------- /out.js ---------- diff --git a/internal/js_ast/js_ast.go b/internal/js_ast/js_ast.go index 56d3620fe17..73d81f117df 100644 --- a/internal/js_ast/js_ast.go +++ b/internal/js_ast/js_ast.go @@ -1630,6 +1630,9 @@ type Scope struct { Members map[string]ScopeMember Generated []Ref + // This will be non-nil if this is a TypeScript "namespace" or "enum" + TSNamespace *TSNamespaceScope + // The location of the "use strict" directive for ExplicitStrictMode UseStrictLoc logger.Loc @@ -1668,6 +1671,115 @@ func (s *Scope) RecursiveSetStrictMode(kind StrictModeKind) { } } +// This is for TypeScript "enum" and "namespace" blocks. Each block can +// potentially be instantiated multiple times. The exported members of each +// block are merged into a single namespace while the non-exported code is +// still scoped to just within that block: +// +// let x = 1; +// namespace Foo { +// let x = 2; +// export let y = 3; +// } +// namespace Foo { +// console.log(x); // 1 +// console.log(y); // 3 +// } +// +// Doing this also works inside an enum: +// +// enum Foo { +// A = 3, +// B = A + 1, +// } +// enum Foo { +// C = A + 2, +// } +// console.log(Foo.B) // 4 +// console.log(Foo.C) // 5 +// +// This is a form of identifier lookup that works differently than the +// hierarchical scope-based identifier lookup in JavaScript. Lookup now needs +// to search sibling scopes in addition to parent scopes. This is accomplished +// by sharing the map of exported members between all matching sibling scopes. +type TSNamespaceScope struct { + // This is shared between all sibling namespace blocks + ExportedMembers TSNamespaceMembers + + // This is specific to this namespace block. It's the argument of the + // immediately-invoked function expression that the namespace block is + // compiled into: + // + // var ns; + // (function (ns2) { + // ns2.x = 123; + // })(ns || (ns = {})); + // + // This variable is "ns2" in the above example. It's the symbol to use when + // generating property accesses off of this namespace when it's in scope. + ArgRef Ref + + // This is a lazily-generated map of identifiers that actually represent + // property accesses to this namespace's properties. For example: + // + // namespace x { + // export let y = 123 + // } + // namespace x { + // export let z = y + // } + // + // This should be compiled into the following code: + // + // var x; + // (function(x2) { + // x2.y = 123; + // })(x || (x = {})); + // (function(x3) { + // x3.z = x3.y; + // })(x || (x = {})); + // + // When we try to find the symbol "y", we instead return one of these lazily + // generated proxy symbols that represent the property access "x3.y". This + // map is unique per namespace block because "x3" is the argument symbol that + // is specific to that particular namespace block. + LazilyGeneratedProperyAccesses map[string]Ref +} + +type TSNamespaceMembers map[string]TSNamespaceMember + +type TSNamespaceMember struct { + Loc logger.Loc + Data TSNamespaceMemberData +} + +type TSNamespaceMemberData interface { + isTSNamespaceMember() +} + +func (TSNamespaceMemberProperty) isTSNamespaceMember() {} +func (TSNamespaceMemberNamespace) isTSNamespaceMember() {} +func (TSNamespaceMemberEnumNumber) isTSNamespaceMember() {} +func (TSNamespaceMemberEnumString) isTSNamespaceMember() {} + +// "namespace ns { export let it }" +type TSNamespaceMemberProperty struct{} + +// "namespace ns { export namespace it {} }" +type TSNamespaceMemberNamespace struct { + ExportedMembers TSNamespaceMembers +} + +// "enum ns { it }" +type TSNamespaceMemberEnumNumber struct { + Value float64 +} + +// "enum ns { it = 'it' }" +type TSNamespaceMemberEnumString struct { + Value []uint16 +} + type SymbolMap struct { // This could be represented as a "map[Ref]Symbol" but a two-level array was // more efficient in profiles. This appears to be because it doesn't involve diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index 41ead98639c..5461ac0cfeb 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -82,10 +82,29 @@ type parser struct { privateSetters map[js_ast.Ref]js_ast.Ref // These are for TypeScript + // + // We build up enough information about the TypeScript namespace hierarchy to + // be able to resolve scope lookups and property accesses for TypeScript enum + // and namespace features. Each JavaScript scope object inside a namespace + // has a reference to a map of exported namespace members from sibling scopes. + // + // In addition, there is a map from each relevant symbol reference to the data + // associated with that namespace or namespace member: "refToTSNamespaceMemberData". + // This gives enough info to be able to resolve queries into the namespace. + // + // When visiting expressions, namespace metadata is associated with the most + // recently visited node. If namespace metadata is present, "tsNamespaceTarget" + // will be set to the most recently visited node (as a way to mark that this + // node has metadata) and "tsNamespaceMemberData" will be set to the metadata. + // + // The "shouldFoldNumericConstants" flag is enabled inside each enum body block + // since TypeScript requires numeric constant folding in enum definitions. + refToTSNamespaceMemberData map[js_ast.Ref]js_ast.TSNamespaceMemberData + tsNamespaceTarget js_ast.E + tsNamespaceMemberData js_ast.TSNamespaceMemberData shouldFoldNumericConstants bool emittedNamespaceVars map[js_ast.Ref]bool isExportedInsideNamespace map[js_ast.Ref]js_ast.Ref - knownEnumValues map[js_ast.Ref]map[string]float64 localTypeNames map[string]bool // This is the reference to the generated function argument for the namespace, @@ -6975,6 +6994,31 @@ func (p *parser) findSymbol(loc logger.Loc, name string) findSymbolResult { break } + // Is the symbol a member of this scope's TypeScript namespace? + if tsNamespace := s.TSNamespace; tsNamespace != nil { + if member, ok := tsNamespace.ExportedMembers[name]; ok { + // If this is an identifier from a sibling TypeScript namespace, then we're + // going to have to generate a property access instead of a simple reference. + // Lazily-generate an identifier that represents this property access. + cache := tsNamespace.LazilyGeneratedProperyAccesses + if cache == nil { + cache = make(map[string]js_ast.Ref) + tsNamespace.LazilyGeneratedProperyAccesses = cache + } + ref, ok = cache[name] + if !ok { + ref = p.newSymbol(js_ast.SymbolOther, name) + p.symbols[ref.InnerIndex].NamespaceAlias = &js_ast.NamespaceAlias{ + NamespaceRef: tsNamespace.ArgRef, + Alias: name, + } + cache[name] = ref + } + declareLoc = member.Loc + break + } + } + s = s.Parent if s == nil { // Allocate an "unbound" symbol @@ -9311,11 +9355,8 @@ func (p *parser) visitAndAppendStmt(stmts []js_ast.Stmt, stmt js_ast.Stmt) []js_ hasNumericValue := true valueExprs := []js_ast.Expr{} - // Track values so they can be used by constant folding. We need to follow - // links here in case the enum was merged with a preceding namespace. - valuesSoFar := make(map[string]float64) - p.knownEnumValues[s.Name.Ref] = valuesSoFar - p.knownEnumValues[s.Arg] = valuesSoFar + // Update the exported members of this enum as we constant fold each one + exportedMembers := p.currentScope.TSNamespace.ExportedMembers // We normally don't fold numeric constants because they might increase code // size, but it's important to fold numeric constants inside enums since @@ -9332,16 +9373,28 @@ func (p *parser) visitAndAppendStmt(stmts []js_ast.Stmt, stmt js_ast.Stmt) []js_ if value.ValueOrNil.Data != nil { value.ValueOrNil = p.visitExpr(value.ValueOrNil) hasNumericValue = false + switch e := value.ValueOrNil.Data.(type) { case *js_ast.ENumber: - valuesSoFar[name] = e.Value + member := exportedMembers[name] + member.Data = &js_ast.TSNamespaceMemberEnumNumber{Value: e.Value} + exportedMembers[name] = member + p.refToTSNamespaceMemberData[value.Ref] = member.Data hasNumericValue = true nextNumericValue = e.Value + 1 + case *js_ast.EString: + member := exportedMembers[name] + member.Data = &js_ast.TSNamespaceMemberEnumString{Value: e.Value} + exportedMembers[name] = member + p.refToTSNamespaceMemberData[value.Ref] = member.Data hasStringValue = true } } else if hasNumericValue { - valuesSoFar[name] = nextNumericValue + member := exportedMembers[name] + member.Data = &js_ast.TSNamespaceMemberEnumNumber{Value: nextNumericValue} + exportedMembers[name] = member + p.refToTSNamespaceMemberData[value.Ref] = member.Data value.ValueOrNil = js_ast.Expr{Loc: value.Loc, Data: &js_ast.ENumber{Value: nextNumericValue}} nextNumericValue++ } else { @@ -10325,15 +10378,6 @@ func (p *parser) maybeRewritePropertyAccess( return js_ast.Expr{Loc: nameLoc, Data: &js_ast.EIdentifier{Ref: p.requireRef}}, true } } - - // If this is a known enum value, inline the value of the enum - if p.options.ts.Parse { - if enumValueMap, ok := p.knownEnumValues[id.Ref]; ok { - if number, ok := enumValueMap[name]; ok { - return js_ast.Expr{Loc: loc, Data: &js_ast.ENumber{Value: number}}, true - } - } - } } // Attempt to simplify statically-determined object literal property accesses @@ -10397,6 +10441,40 @@ func (p *parser) maybeRewritePropertyAccess( } } + // Handle references to namespaces or namespace members + if target.Data == p.tsNamespaceTarget && assignTarget == js_ast.AssignTargetNone && !isDeleteTarget { + if ns, ok := p.tsNamespaceMemberData.(*js_ast.TSNamespaceMemberNamespace); ok { + if member, ok := ns.ExportedMembers[name]; ok { + switch m := member.Data.(type) { + case *js_ast.TSNamespaceMemberEnumNumber: + return js_ast.Expr{Loc: loc, Data: &js_ast.ENumber{Value: m.Value}}, true + + case *js_ast.TSNamespaceMemberEnumString: + return js_ast.Expr{Loc: loc, Data: &js_ast.EString{Value: m.Value}}, true + + case *js_ast.TSNamespaceMemberNamespace: + // If this isn't a constant, return a clone of this property access + // but with the namespace member data associated with it so that + // more property accesses off of this property access are recognized. + if preferQuotedKey || !js_lexer.IsIdentifier(name) { + p.tsNamespaceTarget = &js_ast.EIndex{ + Target: target, + Index: js_ast.Expr{Loc: nameLoc, Data: &js_ast.EString{Value: js_lexer.StringToUTF16(name)}}, + } + } else { + p.tsNamespaceTarget = &js_ast.EDot{ + Target: target, + Name: name, + NameLoc: nameLoc, + } + } + p.tsNamespaceMemberData = member.Data + return js_ast.Expr{Loc: loc, Data: p.tsNamespaceTarget}, true + } + } + } + } + return js_ast.Expr{}, false } @@ -12851,6 +12929,38 @@ func (p *parser) handleIdentifier(loc logger.Loc, e *js_ast.EIdentifier, opts id p.log.AddRangeError(&p.tracker, r, fmt.Sprintf("Cannot assign to import %q", p.symbols[ref.InnerIndex].OriginalName)) } + // Substitute an EImportIdentifier now if this has a namespace alias + if opts.assignTarget == js_ast.AssignTargetNone && !opts.isDeleteTarget { + if nsAlias := p.symbols[ref.InnerIndex].NamespaceAlias; nsAlias != nil { + data := &js_ast.EImportIdentifier{ + Ref: ref, + PreferQuotedKey: opts.preferQuotedKey, + WasOriginallyIdentifier: opts.wasOriginallyIdentifier, + } + + // Handle references to namespaces or namespace members + if tsMemberData, ok := p.refToTSNamespaceMemberData[nsAlias.NamespaceRef]; ok { + if ns, ok := tsMemberData.(*js_ast.TSNamespaceMemberNamespace); ok { + if member, ok := ns.ExportedMembers[nsAlias.Alias]; ok { + switch m := member.Data.(type) { + case *js_ast.TSNamespaceMemberEnumNumber: + return js_ast.Expr{Loc: loc, Data: &js_ast.ENumber{Value: m.Value}} + + case *js_ast.TSNamespaceMemberEnumString: + return js_ast.Expr{Loc: loc, Data: &js_ast.EString{Value: m.Value}} + + case *js_ast.TSNamespaceMemberNamespace: + p.tsNamespaceTarget = data + p.tsNamespaceMemberData = member.Data + } + } + } + } + + return js_ast.Expr{Loc: loc, Data: data} + } + } + // Substitute an EImportIdentifier now if this is an import item if p.isImportItem[ref] { return js_ast.Expr{Loc: loc, Data: &js_ast.EImportIdentifier{ @@ -12860,25 +12970,37 @@ func (p *parser) handleIdentifier(loc logger.Loc, e *js_ast.EIdentifier, opts id }} } + // Handle references to namespaces or namespace members + if tsMemberData, ok := p.refToTSNamespaceMemberData[ref]; ok { + switch m := tsMemberData.(type) { + case *js_ast.TSNamespaceMemberEnumNumber: + return js_ast.Expr{Loc: loc, Data: &js_ast.ENumber{Value: m.Value}} + + case *js_ast.TSNamespaceMemberEnumString: + return js_ast.Expr{Loc: loc, Data: &js_ast.EString{Value: m.Value}} + + case *js_ast.TSNamespaceMemberNamespace: + p.tsNamespaceTarget = e + p.tsNamespaceMemberData = tsMemberData + } + } + // Substitute a namespace export reference now if appropriate if p.options.ts.Parse { if nsRef, ok := p.isExportedInsideNamespace[ref]; ok { name := p.symbols[ref.InnerIndex].OriginalName - // If this is a known enum value, inline the value of the enum - if enumValueMap, ok := p.knownEnumValues[nsRef]; ok { - if number, ok := enumValueMap[name]; ok { - return js_ast.Expr{Loc: loc, Data: &js_ast.ENumber{Value: number}} - } - } - // Otherwise, create a property access on the namespace p.recordUsage(nsRef) - return js_ast.Expr{Loc: loc, Data: &js_ast.EDot{ + propertyAccess := &js_ast.EDot{ Target: js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: nsRef}}, Name: name, NameLoc: loc, - }} + } + if p.tsNamespaceTarget == e { + p.tsNamespaceTarget = propertyAccess + } + return js_ast.Expr{Loc: loc, Data: propertyAccess} } } @@ -14032,10 +14154,10 @@ func newParser(log logger.Log, source logger.Source, lexer js_lexer.Lexer, optio privateSetters: make(map[js_ast.Ref]js_ast.Ref), // These are for TypeScript - emittedNamespaceVars: make(map[js_ast.Ref]bool), - isExportedInsideNamespace: make(map[js_ast.Ref]js_ast.Ref), - knownEnumValues: make(map[js_ast.Ref]map[string]float64), - localTypeNames: make(map[string]bool), + refToTSNamespaceMemberData: make(map[js_ast.Ref]js_ast.TSNamespaceMemberData), + emittedNamespaceVars: make(map[js_ast.Ref]bool), + isExportedInsideNamespace: make(map[js_ast.Ref]js_ast.Ref), + localTypeNames: make(map[string]bool), // These are for handling ES6 imports and exports importItemsForNamespace: make(map[js_ast.Ref]map[string]js_ast.LocRef), diff --git a/internal/js_parser/ts_parser.go b/internal/js_parser/ts_parser.go index 39dfa0410d5..ea44fe2dee2 100644 --- a/internal/js_parser/ts_parser.go +++ b/internal/js_parser/ts_parser.go @@ -895,12 +895,27 @@ func (p *parser) parseTypeScriptEnumStmt(loc logger.Loc, opts parseStmtOpts) js_ nameText := p.lexer.Identifier p.lexer.Expect(js_lexer.TIdentifier) name := js_ast.LocRef{Loc: nameLoc, Ref: js_ast.InvalidRef} - argRef := js_ast.InvalidRef + + // Generate the namespace object + exportedMembers := p.getOrCreateExportedNamespaceMembers(nameText, opts.isExport) + tsNamespace := &js_ast.TSNamespaceScope{ + ExportedMembers: exportedMembers, + ArgRef: js_ast.InvalidRef, + } + enumMemberData := &js_ast.TSNamespaceMemberNamespace{ + ExportedMembers: exportedMembers, + } + + // Declare the enum and create the scope if !opts.isTypeScriptDeclare { name.Ref = p.declareSymbol(js_ast.SymbolTSEnum, nameLoc, nameText) p.pushScopeForParsePass(js_ast.ScopeEntry, loc) + p.currentScope.TSNamespace = tsNamespace + p.refToTSNamespaceMemberData[name.Ref] = enumMemberData } + p.lexer.Expect(js_lexer.TOpenBrace) + values := []js_ast.EnumValue{} oldFnOrArrowData := p.fnOrArrowDataParse p.fnOrArrowDataParse = fnOrArrowDataParse{ @@ -908,7 +923,6 @@ func (p *parser) parseTypeScriptEnumStmt(loc logger.Loc, opts parseStmtOpts) js_ } // Parse the body - values := []js_ast.EnumValue{} for p.lexer.Token != js_lexer.TCloseBrace { value := js_ast.EnumValue{ Loc: p.lexer.Loc(), @@ -916,10 +930,13 @@ func (p *parser) parseTypeScriptEnumStmt(loc logger.Loc, opts parseStmtOpts) js_ } // Parse the name + var nameText string if p.lexer.Token == js_lexer.TStringLiteral { value.Name = p.lexer.StringLiteral() + nameText = js_lexer.UTF16ToString(value.Name) } else if p.lexer.IsIdentifierOrKeyword() { - value.Name = js_lexer.StringToUTF16(p.lexer.Identifier) + nameText = p.lexer.Identifier + value.Name = js_lexer.StringToUTF16(nameText) } else { p.lexer.Expect(js_lexer.TIdentifier) } @@ -938,6 +955,12 @@ func (p *parser) parseTypeScriptEnumStmt(loc logger.Loc, opts parseStmtOpts) js_ values = append(values, value) + // Add this enum value as a member of the enum's namespace + exportedMembers[nameText] = js_ast.TSNamespaceMember{ + Loc: value.Loc, + Data: &js_ast.TSNamespaceMemberProperty{}, + } + if p.lexer.Token != js_lexer.TComma && p.lexer.Token != js_lexer.TSemicolon { break } @@ -980,11 +1003,12 @@ func (p *parser) parseTypeScriptEnumStmt(loc logger.Loc, opts parseStmtOpts) js_ // Add a "_" to make tests easier to read, since non-bundler tests don't // run the renamer. For external-facing things the renamer will avoid // collisions automatically so this isn't important for correctness. - argRef = p.newSymbol(js_ast.SymbolHoisted, "_"+nameText) - p.currentScope.Generated = append(p.currentScope.Generated, argRef) + tsNamespace.ArgRef = p.newSymbol(js_ast.SymbolHoisted, "_"+nameText) + p.currentScope.Generated = append(p.currentScope.Generated, tsNamespace.ArgRef) } else { - argRef = p.declareSymbol(js_ast.SymbolHoisted, nameLoc, nameText) + tsNamespace.ArgRef = p.declareSymbol(js_ast.SymbolHoisted, nameLoc, nameText) } + p.refToTSNamespaceMemberData[tsNamespace.ArgRef] = enumMemberData p.popScope() } @@ -1001,7 +1025,7 @@ func (p *parser) parseTypeScriptEnumStmt(loc logger.Loc, opts parseStmtOpts) js_ return js_ast.Stmt{Loc: loc, Data: &js_ast.SEnum{ Name: name, - Arg: argRef, + Arg: tsNamespace.ArgRef, Values: values, IsExport: opts.isExport, }} @@ -1062,14 +1086,54 @@ func (p *parser) parseTypeScriptImportEqualsStmt(loc logger.Loc, opts parseStmtO }} } +// Generate a TypeScript namespace object for this namespace's scope. If this +// namespace is another block that is to be merged with an existing namespace, +// use that earlier namespace's object instead. +func (p *parser) getOrCreateExportedNamespaceMembers(name string, isExport bool) js_ast.TSNamespaceMembers { + // Merge with a sibling namespace from the same scope + if existingMember, ok := p.currentScope.Members[name]; ok { + if memberData, ok := p.refToTSNamespaceMemberData[existingMember.Ref]; ok { + if nsMemberData, ok := memberData.(*js_ast.TSNamespaceMemberNamespace); ok { + return nsMemberData.ExportedMembers + } + } + } + + // Merge with a sibling namespace from a different scope + if isExport { + if parentNamespace := p.currentScope.TSNamespace; parentNamespace != nil { + if existing, ok := parentNamespace.ExportedMembers[name]; ok { + if existing, ok := existing.Data.(*js_ast.TSNamespaceMemberNamespace); ok { + return existing.ExportedMembers + } + } + } + } + + // Otherwise, generate a new namespace object + return make(js_ast.TSNamespaceMembers) +} + func (p *parser) parseTypeScriptNamespaceStmt(loc logger.Loc, opts parseStmtOpts) js_ast.Stmt { // "namespace Foo {}" nameLoc := p.lexer.Loc() nameText := p.lexer.Identifier p.lexer.Next() + // Generate the namespace object + exportedMembers := p.getOrCreateExportedNamespaceMembers(nameText, opts.isExport) + tsNamespace := &js_ast.TSNamespaceScope{ + ExportedMembers: exportedMembers, + ArgRef: js_ast.InvalidRef, + } + nsMemberData := &js_ast.TSNamespaceMemberNamespace{ + ExportedMembers: exportedMembers, + } + + // Declare the namespace and create the scope name := js_ast.LocRef{Loc: nameLoc, Ref: js_ast.InvalidRef} scopeIndex := p.pushScopeForParsePass(js_ast.ScopeEntry, loc) + p.currentScope.TSNamespace = tsNamespace oldHasNonLocalExportDeclareInsideNamespace := p.hasNonLocalExportDeclareInsideNamespace oldFnOrArrowData := p.fnOrArrowDataParse @@ -1079,6 +1143,7 @@ func (p *parser) parseTypeScriptNamespaceStmt(loc logger.Loc, opts parseStmtOpts needsAsyncLoc: logger.Loc{Start: -1}, } + // Parse the statements inside the namespace var stmts []js_ast.Stmt if p.lexer.Token == js_lexer.TDot { dotLoc := p.lexer.Loc() @@ -1103,6 +1168,71 @@ func (p *parser) parseTypeScriptNamespaceStmt(loc logger.Loc, opts parseStmtOpts p.hasNonLocalExportDeclareInsideNamespace = oldHasNonLocalExportDeclareInsideNamespace p.fnOrArrowDataParse = oldFnOrArrowData + // Add any exported members from this namespace's body as members of the + // associated namespace object. + for _, stmt := range stmts { + switch s := stmt.Data.(type) { + case *js_ast.SFunction: + if s.IsExport { + name := p.symbols[s.Fn.Name.Ref.InnerIndex].OriginalName + member := js_ast.TSNamespaceMember{ + Loc: s.Fn.Name.Loc, + Data: &js_ast.TSNamespaceMemberProperty{}, + } + exportedMembers[name] = member + p.refToTSNamespaceMemberData[s.Fn.Name.Ref] = member.Data + } + + case *js_ast.SClass: + if s.IsExport { + name := p.symbols[s.Class.Name.Ref.InnerIndex].OriginalName + member := js_ast.TSNamespaceMember{ + Loc: s.Class.Name.Loc, + Data: &js_ast.TSNamespaceMemberProperty{}, + } + exportedMembers[name] = member + p.refToTSNamespaceMemberData[s.Class.Name.Ref] = member.Data + } + + case *js_ast.SNamespace: + if s.IsExport { + if memberData, ok := p.refToTSNamespaceMemberData[s.Name.Ref]; ok { + if nsMemberData, ok := memberData.(*js_ast.TSNamespaceMemberNamespace); ok { + member := js_ast.TSNamespaceMember{ + Loc: s.Name.Loc, + Data: &js_ast.TSNamespaceMemberNamespace{ + ExportedMembers: nsMemberData.ExportedMembers, + }, + } + exportedMembers[p.symbols[s.Name.Ref.InnerIndex].OriginalName] = member + p.refToTSNamespaceMemberData[s.Name.Ref] = member.Data + } + } + } + + case *js_ast.SEnum: + if s.IsExport { + if memberData, ok := p.refToTSNamespaceMemberData[s.Name.Ref]; ok { + if nsMemberData, ok := memberData.(*js_ast.TSNamespaceMemberNamespace); ok { + member := js_ast.TSNamespaceMember{ + Loc: s.Name.Loc, + Data: &js_ast.TSNamespaceMemberNamespace{ + ExportedMembers: nsMemberData.ExportedMembers, + }, + } + exportedMembers[p.symbols[s.Name.Ref.InnerIndex].OriginalName] = member + p.refToTSNamespaceMemberData[s.Name.Ref] = member.Data + } + } + } + + case *js_ast.SLocal: + if s.IsExport { + p.exportDeclsInsideNamespace(exportedMembers, s.Decls) + } + } + } + // Import assignments may be only used in type expressions, not value // expressions. If this is the case, the TypeScript compiler removes // them entirely from the output. That can cause the namespace itself @@ -1132,7 +1262,6 @@ func (p *parser) parseTypeScriptNamespaceStmt(loc logger.Loc, opts parseStmtOpts return js_ast.Stmt{Loc: loc, Data: &js_ast.STypeScript{}} } - argRef := js_ast.InvalidRef if !opts.isTypeScriptDeclare { // Avoid a collision with the namespace closure argument variable if the // namespace exports a symbol with the same name as the namespace itself: @@ -1154,25 +1283,61 @@ func (p *parser) parseTypeScriptNamespaceStmt(loc logger.Loc, opts parseStmtOpts // Add a "_" to make tests easier to read, since non-bundler tests don't // run the renamer. For external-facing things the renamer will avoid // collisions automatically so this isn't important for correctness. - argRef = p.newSymbol(js_ast.SymbolHoisted, "_"+nameText) - p.currentScope.Generated = append(p.currentScope.Generated, argRef) + tsNamespace.ArgRef = p.newSymbol(js_ast.SymbolHoisted, "_"+nameText) + p.currentScope.Generated = append(p.currentScope.Generated, tsNamespace.ArgRef) } else { - argRef = p.declareSymbol(js_ast.SymbolHoisted, nameLoc, nameText) + tsNamespace.ArgRef = p.declareSymbol(js_ast.SymbolHoisted, nameLoc, nameText) } + p.refToTSNamespaceMemberData[tsNamespace.ArgRef] = nsMemberData } p.popScope() if !opts.isTypeScriptDeclare { name.Ref = p.declareSymbol(js_ast.SymbolTSNamespace, nameLoc, nameText) + p.refToTSNamespaceMemberData[name.Ref] = nsMemberData } return js_ast.Stmt{Loc: loc, Data: &js_ast.SNamespace{ Name: name, - Arg: argRef, + Arg: tsNamespace.ArgRef, Stmts: stmts, IsExport: opts.isExport, }} } +func (p *parser) exportDeclsInsideNamespace(exportedMembers js_ast.TSNamespaceMembers, decls []js_ast.Decl) { + for _, decl := range decls { + p.exportBindingInsideNamespace(exportedMembers, decl.Binding) + } +} + +func (p *parser) exportBindingInsideNamespace(exportedMembers js_ast.TSNamespaceMembers, binding js_ast.Binding) { + switch b := binding.Data.(type) { + case *js_ast.BMissing: + + case *js_ast.BIdentifier: + name := p.symbols[b.Ref.InnerIndex].OriginalName + member := js_ast.TSNamespaceMember{ + Loc: binding.Loc, + Data: &js_ast.TSNamespaceMemberProperty{}, + } + exportedMembers[name] = member + p.refToTSNamespaceMemberData[b.Ref] = member.Data + + case *js_ast.BArray: + for _, item := range b.Items { + p.exportBindingInsideNamespace(exportedMembers, item.Binding) + } + + case *js_ast.BObject: + for _, property := range b.Properties { + p.exportBindingInsideNamespace(exportedMembers, property.Value) + } + + default: + panic("Internal error") + } +} + func (p *parser) generateClosureForTypeScriptNamespaceOrEnum( stmts []js_ast.Stmt, stmtLoc logger.Loc, isExport bool, nameLoc logger.Loc, nameRef js_ast.Ref, argRef js_ast.Ref, stmtsInsideClosure []js_ast.Stmt,