diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrModuleToJsTransformer.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrModuleToJsTransformer.kt index d2ac492ce2f..3589b56b6c9 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrModuleToJsTransformer.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrModuleToJsTransformer.kt @@ -154,7 +154,7 @@ class IrModuleToJsTransformer( ) val moduleBody = generateModuleBody(modules, staticContext) - val internalModuleName = JsName("_", false) + val internalModuleName = ReservedJsNames.makeInternalModuleName() val globalNames = NameTable(namer.globalNames) val exportStatements = ExportModelToJsStatements(nameGenerator) { globalNames.declareFreshName(it, it) } .generateModuleExport(exportedModule, internalModuleName) @@ -256,7 +256,7 @@ class IrModuleToJsTransformer( modules += JsImportedModule(module.externalModuleName(), moduleName, null, relativeRequirePath) names.forEach { - imports += JsVars(JsVars.JsVar(JsName(it, false), JsNameRef(it, JsNameRef("\$crossModule\$", moduleName.makeRef())))) + imports += JsVars(JsVars.JsVar(JsName(it, false), JsNameRef(it, ReservedJsNames.makeCrossModuleNameRef(moduleName)))) } } @@ -271,15 +271,15 @@ class IrModuleToJsTransformer( return modules.flatMap { refInfo.exports(it).map { jsAssignment( - JsNameRef(it, JsNameRef("\$crossModule\$", internalModuleName.makeRef())), + JsNameRef(it, ReservedJsNames.makeCrossModuleNameRef(internalModuleName)), JsNameRef(it) ).makeStmt() } }.let { if (!it.isEmpty()) { val createExportBlock = jsAssignment( - JsNameRef("\$crossModule\$", internalModuleName.makeRef()), - JsAstUtils.or(JsNameRef("\$crossModule\$", internalModuleName.makeRef()), JsObjectLiteral()) + ReservedJsNames.makeCrossModuleNameRef(internalModuleName), + JsAstUtils.or(ReservedJsNames.makeCrossModuleNameRef(internalModuleName), JsObjectLiteral()) ).makeStmt() return listOf(createExportBlock) + it } else it diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrModuleToJsTransformerTmp.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrModuleToJsTransformerTmp.kt index 1756a7e2a0d..c6e64c7c984 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrModuleToJsTransformerTmp.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrModuleToJsTransformerTmp.kt @@ -173,7 +173,7 @@ class IrModuleToJsTransformerTmp( val result = JsIrProgramFragment(file.fqName.asString()) - val internalModuleName = JsName("_", false) + val internalModuleName = ReservedJsNames.makeInternalModuleName() val globalNames = NameTable(globalNameScope) val exportStatements = ExportModelToJsStatements(staticContext, { globalNames.declareFreshName(it, it) }).generateModuleExport( @@ -316,8 +316,8 @@ fun generateWrappedModuleBody( val moduleToRef = program.crossModuleDependencies(relativeRequirePath) - val main = program.modules.last() - val others = program.modules.dropLast(1) + val main = program.mainModule + val others = program.otherModules val mainModule = generateSingleWrappedModuleBody( mainModuleName, diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/JsIrProgramFragment.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/JsIrProgramFragment.kt index 324c38c8fcc..cd6a993e2f7 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/JsIrProgramFragment.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/JsIrProgramFragment.kt @@ -26,11 +26,14 @@ class JsIrProgramFragment(val packageFqn: String) { class JsIrModule(val moduleName: String, val externalModuleName: String, val fragments: List) class JsIrProgram(val modules: List) { + val mainModule = modules.last() + val otherModules = modules.dropLast(1) fun crossModuleDependencies(relativeRequirePath: Boolean): Map { val moduleToBuilder = modules.associateWith { JsIrModuleCrossModuleReferecenceBuilder(it, relativeRequirePath) } val definitionModule = mutableMapOf() + moduleToBuilder[mainModule]!!.transitiveJsExportFrom = otherModules for (module in modules) { val moduleBuilder = moduleToBuilder[module]!! for (fragment in module.fragments) { @@ -65,6 +68,7 @@ private class CrossModuleRef(val module: JsIrModuleCrossModuleReferecenceBuilder private class JsIrModuleCrossModuleReferecenceBuilder(val module: JsIrModule, val relativeRequirePath: Boolean) { val imports = mutableListOf() val exports = mutableSetOf() + var transitiveJsExportFrom = emptyList() private lateinit var exportNames: Map // tag -> name @@ -108,12 +112,15 @@ private class JsIrModuleCrossModuleReferecenceBuilder(val module: JsIrModule, va val importedAs = tagToName[tag]!! val moduleName = it.module.module.import() - val importStatement = JsVars.JsVar(importedAs, JsNameRef(exportedAs, JsNameRef("\$crossModule\$", moduleName.makeRef()))) + val importStatement = JsVars.JsVar(importedAs, JsNameRef(exportedAs, ReservedJsNames.makeCrossModuleNameRef(moduleName))) tag to importStatement } - return CrossModuleReferences(importedModules.values.toList(), resultImports, exportNames) + val transitiveExport = transitiveJsExportFrom.mapNotNull { + it.fragments.find { f -> !f.exports.isEmpty }?.run { it.import() } + } + return CrossModuleReferences(importedModules.values.toList(), resultImports, exportNames, transitiveExport) } } @@ -121,8 +128,9 @@ class CrossModuleReferences( val importedModules: List, // additional Kotlin imported modules val imports: Map, // tag -> import statement val exports: Map, // tag -> name + val transitiveJsExportFrom: List // the list of modules which provide their js exports for transitive export ) { companion object { - val Empty = CrossModuleReferences(listOf(), emptyMap(), emptyMap()) + val Empty = CrossModuleReferences(listOf(), emptyMap(), emptyMap(), emptyList()) } } \ No newline at end of file diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/Merger.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/Merger.kt index e805660c6bd..3da97026820 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/Merger.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/Merger.kt @@ -63,20 +63,18 @@ class Merger( } if (crossModuleReferences.exports.isNotEmpty()) { - val internalModuleName = JsName("_", false) + val internalModuleName = ReservedJsNames.makeInternalModuleName() val createExportBlock = jsAssignment( - JsNameRef("\$crossModule\$", internalModuleName.makeRef()), - JsAstUtils.or(JsNameRef("\$crossModule\$", internalModuleName.makeRef()), JsObjectLiteral()) + ReservedJsNames.makeCrossModuleNameRef(internalModuleName), + JsAstUtils.or(ReservedJsNames.makeCrossModuleNameRef(internalModuleName), JsObjectLiteral()) ).makeStmt() additionalExports += createExportBlock crossModuleReferences.exports.entries.forEach { (tag, name) -> val internalName = nameMap[tag] ?: error("Missing name for declaration '$tag'") - additionalExports += jsAssignment( - JsNameRef(name, JsNameRef("\$crossModule\$", JsName("_", false).makeRef())), - JsNameRef(internalName) - ).makeStmt() + val crossModuleRef = ReservedJsNames.makeCrossModuleNameRef(ReservedJsNames.makeInternalModuleName()) + additionalExports += jsAssignment(JsNameRef(name, crossModuleRef), JsNameRef(internalName)).makeStmt() } } } @@ -126,6 +124,36 @@ class Merger( } } + private fun declareAndCallJsExporter(): List { + val exportBody = JsBlock(fragments.flatMap { it.exports.statements }) + if (exportBody.isEmpty) { + return emptyList() + } + + val internalModuleName = ReservedJsNames.makeInternalModuleName() + val exporterName = ReservedJsNames.makeJsExporterName() + val jsExporterFunction = JsFunction(emptyScope, "js exporter function").apply { + body = exportBody + name = exporterName + parameters.add(JsParameter(internalModuleName)) + } + val jsExporterCall = JsInvocation(exporterName.makeRef(), internalModuleName.makeRef()) + val result = mutableListOf(jsExporterFunction.makeStmt(), jsExporterCall.makeStmt()) + if (!generateCallToMain) { + val exportExporter = jsAssignment(JsNameRef(exporterName, internalModuleName.makeRef()), exporterName.makeRef()) + result += exportExporter.makeStmt() + } + return result + } + + private fun transitiveJsExport(): List { + val internalModuleName = ReservedJsNames.makeInternalModuleName() + val exporterName = ReservedJsNames.makeJsExporterName() + return crossModuleReferences.transitiveJsExportFrom.map { + JsInvocation(JsNameRef(exporterName, it.makeRef()), internalModuleName.makeRef()).makeStmt() + } + } + fun merge(): JsProgram { assertSingleDefinition() @@ -176,13 +204,11 @@ class Merger( val callToMain = fragments.sortedBy { it.packageFqn }.firstNotNullOfOrNull { it.mainFunction } - val exportStatements = fragments.flatMap { it.exports.statements } + additionalExports + val exportStatements = declareAndCallJsExporter() + additionalExports + transitiveJsExport() val importedJsModules = this.importedModulesMap.values.toList() + this.crossModuleReferences.importedModules val importStatements = this.importStatements.values.toList() - val internalModuleName = JsName("_", false) - val program = JsProgram() if (generateScriptModule) { @@ -192,6 +218,7 @@ class Merger( this.statements.addWithComment("block: exports", exportStatements) } } else { + val internalModuleName = ReservedJsNames.makeInternalModuleName() val rootFunction = JsFunction(program.rootScope, JsBlock(), "root function").apply { parameters += JsParameter(internalModuleName) parameters += (importedJsModules).map { JsParameter(it.internalName) } diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/ReservedJsNames.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/ReservedJsNames.kt new file mode 100644 index 00000000000..e44caeba0cf --- /dev/null +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/ReservedJsNames.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.ir.backend.js.transformers.irToJs + +import org.jetbrains.kotlin.js.backend.ast.JsName +import org.jetbrains.kotlin.js.backend.ast.JsNameRef + +class ReservedJsNames { + companion object { + fun makeInternalModuleName() = JsName("_", false) + fun makeJsExporterName() = JsName("\$jsExportAll\$", false) + fun makeCrossModuleNameRef(moduleName: JsName) = JsNameRef("\$crossModule\$", moduleName.makeRef()) + } +} \ No newline at end of file diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrBoxJsTestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrBoxJsTestGenerated.java index 8ea340324f8..031fff1d748 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrBoxJsTestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrBoxJsTestGenerated.java @@ -7258,6 +7258,12 @@ public class IrBoxJsTestGenerated extends AbstractIrBoxJsTest { runTest("js/js.translator/testData/box/multiModule/clashedInternalDeclarations.kt"); } + @Test + @TestMetadata("exportFromModules.kt") + public void testExportFromModules() throws Exception { + runTest("js/js.translator/testData/box/multiModule/exportFromModules.kt"); + } + @Test @TestMetadata("interfaceMethodWithDefaultParameter.kt") public void testInterfaceMethodWithDefaultParameter() throws Exception { diff --git a/js/js.translator/testData/box/multiModule/exportFromModules.js b/js/js.translator/testData/box/multiModule/exportFromModules.js new file mode 100644 index 00000000000..cd2d0b98425 --- /dev/null +++ b/js/js.translator/testData/box/multiModule/exportFromModules.js @@ -0,0 +1,18 @@ +$kotlin_test_internal$.beginModule(); + +module.exports = function() { + var demoPackage = require("main").demoPackage + + var a1 = demoPackage.a1.ModuleA1Class + var a2 = demoPackage.a2.moduleA2Function + var bb = demoPackage.b.moduleBFunction + + return { + "moduleA1": (new a1()).toString(), + "moduleA2": a2().toString(), + "moduleB": bb() + }; +}; + +$kotlin_test_internal$.endModule("lib"); + diff --git a/js/js.translator/testData/box/multiModule/exportFromModules.kt b/js/js.translator/testData/box/multiModule/exportFromModules.kt new file mode 100644 index 00000000000..c323feb6494 --- /dev/null +++ b/js/js.translator/testData/box/multiModule/exportFromModules.kt @@ -0,0 +1,54 @@ +// DONT_TARGET_EXACT_BACKEND: JS +// KJS_WITH_FULL_RUNTIME + +// MODULE: ModuleA1 +// FILE: modulea1.kt +package demoPackage.a1 + +@JsExport +data class ModuleA1Class(private val string: String = "A1") { + override fun toString(): String = string +} + +// MODULE: ModuleA2(ModuleA1) +// FILE: modulea2.kt +package demoPackage.a2 + +import demoPackage.a1.ModuleA1Class + +@JsExport +fun moduleA2Function() = ModuleA1Class("A2") + +// MODULE: ModuleB +// FILE: moduleb.kt +package demoPackage.b + +@JsExport +fun moduleBFunction() = "B" + +// MODULE: main(ModuleA2, ModuleB) +// MODULE_KIND: COMMON_JS +// FILE: main.kt + +package mainPackage + +import demoPackage.* + +external interface JsResult { + val moduleA1: String + val moduleA2: String + val moduleB: String +} + +@JsModule("lib") +external fun jsBox(): JsResult + +fun box(): String { + assertEquals(demoPackage.b.moduleBFunction(), "B") + + val res = jsBox() + assertEquals(res.moduleA1, "A1") + assertEquals(res.moduleA2, "A2") + assertEquals(res.moduleB, "B") + return "OK" +} \ No newline at end of file