diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts index d1fd6226e..4e386daa2 100644 --- a/src/LuaTransformer.ts +++ b/src/LuaTransformer.ts @@ -1585,6 +1585,13 @@ export class LuaTransformer { const symbol = this.checker.getSymbolAtLocation(statement.name); const hasExports = symbol !== undefined && this.checker.getExportsOfModule(symbol).length > 0; + const nameIdentifier = this.transformIdentifier(statement.name as ts.Identifier); + const exportScope = this.getIdentifierExportScope(nameIdentifier); + + // Non-module namespace could be merged if: + // - is top level + // - is nested and exported + const isNonModuleMergeable = !this.isModule && (!this.currentNamespace || exportScope); // This is NOT the first declaration if: // - declared as a module before this (ignore interfaces with same name) @@ -1594,9 +1601,19 @@ export class LuaTransformer { (symbol.declarations.findIndex(d => ts.isClassLike(d) || ts.isFunctionDeclaration(d)) === -1 && statement === symbol.declarations.find(ts.isModuleDeclaration)); - const nameIdentifier = this.transformIdentifier(statement.name as ts.Identifier); + if (isNonModuleMergeable) { + // 'local NS = NS or {}' or 'exportTable.NS = exportTable.NS or {}' + const localDeclaration = this.createLocalOrExportedOrGlobalDeclaration( + nameIdentifier, + tstl.createBinaryExpression( + this.addExportToIdentifier(nameIdentifier), + tstl.createTableExpression(), + tstl.SyntaxKind.OrOperator + ) + ); - if (isFirstDeclaration) { + result.push(...localDeclaration); + } else if (isFirstDeclaration) { // local NS = {} or exportTable.NS = {} const localDeclaration = this.createLocalOrExportedOrGlobalDeclaration( nameIdentifier, @@ -1604,17 +1621,21 @@ export class LuaTransformer { ); result.push(...localDeclaration); + } - const exportScope = this.getIdentifierExportScope(nameIdentifier); - if (exportScope && hasExports && tsHelper.moduleHasEmittedBody(statement)) { - // local NS = exportTable.NS - const localDeclaration = this.createHoistableVariableDeclarationStatement( - this.createModuleLocalNameIdentifier(statement), - this.createExportedIdentifier(nameIdentifier, exportScope) - ); + if ( + (isNonModuleMergeable || isFirstDeclaration) && + exportScope && + hasExports && + tsHelper.moduleHasEmittedBody(statement) + ) { + // local NS = exportTable.NS + const localDeclaration = this.createHoistableVariableDeclarationStatement( + this.createModuleLocalNameIdentifier(statement), + this.createExportedIdentifier(nameIdentifier, exportScope) + ); - result.push(localDeclaration); - } + result.push(localDeclaration); } // Set current namespace for nested NS diff --git a/test/translation/__snapshots__/transformation.spec.ts.snap b/test/translation/__snapshots__/transformation.spec.ts.snap index 58e99b895..a41b2d6dc 100644 --- a/test/translation/__snapshots__/transformation.spec.ts.snap +++ b/test/translation/__snapshots__/transformation.spec.ts.snap @@ -471,7 +471,7 @@ end return ____exports" `; -exports[`Transformation (modulesNamespaceNoExport) 1`] = `"TestSpace = {}"`; +exports[`Transformation (modulesNamespaceNoExport) 1`] = `"TestSpace = TestSpace or {}"`; exports[`Transformation (modulesNamespaceWithMemberExport) 1`] = ` "local ____exports = {} @@ -503,7 +503,7 @@ return ____exports" exports[`Transformation (modulesVariableNoExport) 1`] = `"local foo = \\"bar\\""`; exports[`Transformation (namespace) 1`] = ` -"myNamespace = {} +"myNamespace = myNamespace or {} do local function nsMember(self) end @@ -537,6 +537,7 @@ function MergedClass.prototype.methodB(self) self:methodA() self:propertyFunc() end +MergedClass = MergedClass or {} do function MergedClass.namespaceFunc(self) end @@ -549,7 +550,7 @@ MergedClass:namespaceFunc()" `; exports[`Transformation (namespaceNested) 1`] = ` -"myNamespace = {} +"myNamespace = myNamespace or {} do local myNestedNamespace = {} do diff --git a/test/unit/modules.spec.ts b/test/unit/modules.spec.ts index 8b52cfc1c..5134067b7 100644 --- a/test/unit/modules.spec.ts +++ b/test/unit/modules.spec.ts @@ -160,3 +160,23 @@ test("Module merged with interface", () => { const code = `return Foo.bar();`; expect(util.transpileAndExecute(code, undefined, undefined, header)).toBe("foobar"); }); + +test("module merged across files", () => { + const testA = ` + namespace NS { + export namespace Inner { + export const foo = "foo"; + } + } + `; + const testB = ` + namespace NS { + export namespace Inner { + export const bar = "bar"; + } + } + `; + const { transpiledFiles } = util.transpileStringsAsProject({ "testA.ts": testA, "testB.ts": testB }); + const lua = transpiledFiles.map(f => f.lua).join("\n") + "\nreturn NS.Inner.foo .. NS.Inner.bar"; + expect(util.executeLua(lua)).toBe("foobar"); +}); diff --git a/test/util.ts b/test/util.ts index 3da13b395..6c314780b 100644 --- a/test/util.ts +++ b/test/util.ts @@ -20,10 +20,10 @@ export function transpileString( return file.lua.trim(); } -export function transpileStringResult( - input: string | Record, +export function transpileStringsAsProject( + input: Record, options: tstl.CompilerOptions = {} -): Required { +): tstl.TranspileResult { const optionsWithDefaults = { luaTarget: tstl.LuaTarget.Lua53, noHeader: true, @@ -34,9 +34,16 @@ export function transpileStringResult( ...options, }; - const { diagnostics, transpiledFiles } = tstl.transpileVirtualProject( + return tstl.transpileVirtualProject(input, optionsWithDefaults); +} + +export function transpileStringResult( + input: string | Record, + options: tstl.CompilerOptions = {} +): Required { + const { diagnostics, transpiledFiles } = transpileStringsAsProject( typeof input === "string" ? { "main.ts": input } : input, - optionsWithDefaults + options ); const file = transpiledFiles.find(({ fileName }) => /\bmain\.[a-z]+$/.test(fileName));