[JS IR] Re-export all JS-exports from main module in multi-module mode

Add an implicit transitive re-export through
main module for all other module's JS-exports.
This commit is contained in:
Alexander Korepanov
2022-01-11 17:27:02 +03:00
committed by Space
parent d52fa8dd9b
commit b40227b66e
8 changed files with 151 additions and 21 deletions
@@ -154,7 +154,7 @@ class IrModuleToJsTransformer(
)
val moduleBody = generateModuleBody(modules, staticContext)
val internalModuleName = JsName("_", false)
val internalModuleName = ReservedJsNames.makeInternalModuleName()
val globalNames = NameTable<String>(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
@@ -173,7 +173,7 @@ class IrModuleToJsTransformerTmp(
val result = JsIrProgramFragment(file.fqName.asString())
val internalModuleName = JsName("_", false)
val internalModuleName = ReservedJsNames.makeInternalModuleName()
val globalNames = NameTable<String>(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,
@@ -26,11 +26,14 @@ class JsIrProgramFragment(val packageFqn: String) {
class JsIrModule(val moduleName: String, val externalModuleName: String, val fragments: List<JsIrProgramFragment>)
class JsIrProgram(val modules: List<JsIrModule>) {
val mainModule = modules.last()
val otherModules = modules.dropLast(1)
fun crossModuleDependencies(relativeRequirePath: Boolean): Map<JsIrModule, CrossModuleReferences> {
val moduleToBuilder = modules.associateWith { JsIrModuleCrossModuleReferecenceBuilder(it, relativeRequirePath) }
val definitionModule = mutableMapOf<String, JsIrModuleCrossModuleReferecenceBuilder>()
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<CrossModuleRef>()
val exports = mutableSetOf<String>()
var transitiveJsExportFrom = emptyList<JsIrModule>()
private lateinit var exportNames: Map<String, String> // 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<JsImportedModule>, // additional Kotlin imported modules
val imports: Map<String, JsVars.JsVar>, // tag -> import statement
val exports: Map<String, String>, // tag -> name
val transitiveJsExportFrom: List<JsName> // 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())
}
}
@@ -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<JsStatement> {
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<JsStatement> {
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) }
@@ -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())
}
}
@@ -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 {
@@ -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");
@@ -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"
}