diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmBackendContext.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmBackendContext.kt index 3a128fde71d..6349b59737e 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmBackendContext.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmBackendContext.kt @@ -7,6 +7,7 @@ package org.jetbrains.kotlin.backend.wasm import org.jetbrains.kotlin.backend.common.ir.Ir import org.jetbrains.kotlin.backend.common.ir.Symbols +import org.jetbrains.kotlin.backend.wasm.ir2wasm.JsModuleAndQualifierReference import org.jetbrains.kotlin.backend.wasm.lower.WasmSharedVariablesManager import org.jetbrains.kotlin.backend.wasm.utils.WasmInlineClassesUtils import org.jetbrains.kotlin.config.CompilerConfiguration @@ -23,7 +24,6 @@ import org.jetbrains.kotlin.ir.declarations.impl.IrExternalPackageFragmentImpl import org.jetbrains.kotlin.ir.declarations.impl.IrFileImpl import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol import org.jetbrains.kotlin.ir.symbols.impl.DescriptorlessExternalPackageFragmentSymbol -import org.jetbrains.kotlin.ir.types.IrType import org.jetbrains.kotlin.ir.types.IrSimpleType import org.jetbrains.kotlin.ir.types.IrTypeSystemContext import org.jetbrains.kotlin.ir.types.IrTypeSystemContextImpl @@ -63,6 +63,9 @@ class WasmBackendContext( val jsClosureCallers = mutableMapOf() val jsToKotlinClosures = mutableMapOf() + val jsModuleAndQualifierReferences = + mutableSetOf() + override val coroutineSymbols = JsCommonCoroutineSymbols(symbolTable, module,this) diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/compiler.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/compiler.kt index 1ed08196cab..a5d6f17f006 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/compiler.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/compiler.kt @@ -8,6 +8,7 @@ package org.jetbrains.kotlin.backend.wasm import org.jetbrains.kotlin.backend.common.phaser.PhaseConfig import org.jetbrains.kotlin.backend.common.phaser.invokeToplevel import org.jetbrains.kotlin.backend.common.serialization.linkerissues.checkNoUnboundSymbols +import org.jetbrains.kotlin.backend.wasm.ir2wasm.JsModuleAndQualifierReference import org.jetbrains.kotlin.backend.wasm.ir2wasm.WasmCompiledModuleFragment import org.jetbrains.kotlin.backend.wasm.ir2wasm.WasmModuleFragmentGenerator import org.jetbrains.kotlin.backend.wasm.ir2wasm.toJsStringLiteral @@ -107,7 +108,10 @@ fun compileWasm( null } - val jsUninstantiatedWrapper = compiledWasmModule.generateAsyncJsWrapper("./$baseFileName.wasm") + val jsUninstantiatedWrapper = compiledWasmModule.generateAsyncJsWrapper( + "./$baseFileName.wasm", + backendContext.jsModuleAndQualifierReferences + ) val jsWrapper = generateEsmExportsWrapper("./$baseFileName.uninstantiated.mjs") val os = ByteArrayOutputStream() @@ -172,43 +176,74 @@ private fun generateSourceMap( return sourceMapBuilder.build() } -fun WasmCompiledModuleFragment.generateAsyncJsWrapper(wasmFilePath: String): String { +fun WasmCompiledModuleFragment.generateAsyncJsWrapper( + wasmFilePath: String, + jsModuleAndQualifierReferences: Set +): String { + val jsCodeBody = jsFuns.joinToString(",\n") { "${it.importName.toJsStringLiteral()} : ${it.jsCode}" } - val jsCodeBodyIndented = jsCodeBody.prependIndent(" ") + val jsCodeBodyIndented = jsCodeBody.prependIndent(" ") val imports = jsModuleImports .toList() .sorted() .joinToString("") { val moduleSpecifier = it.toJsStringLiteral() - " $moduleSpecifier: imports[$moduleSpecifier] ?? await import($moduleSpecifier),\n" + " $moduleSpecifier: await _importModule($moduleSpecifier),\n" } + val referencesToQualifiedAndImportedDeclarations = jsModuleAndQualifierReferences + .map { + val module = it.module + val qualifier = it.qualifier + buildString { + append(" const ") + append(it.jsVariableName) + append(" = ") + if (module != null) { + append("(await _importModule(${module.toJsStringLiteral()}))") + if (qualifier != null) + append(".") + } + if (qualifier != null) { + append(qualifier) + } + append(";") + } + }.sorted() + .joinToString("\n") + //language=js return """ -const externrefBoxes = new WeakMap(); -// ref must be non-null -function tryGetOrSetExternrefBox(ref, ifNotCached) { - if (typeof ref !== 'object') return ifNotCached; - const cachedBox = externrefBoxes.get(ref); - if (cachedBox !== void 0) return cachedBox; - externrefBoxes.set(ref, ifNotCached); - return ifNotCached; -} - -const js_code = { -$jsCodeBodyIndented -} - -// Placed here to give access to it from externals (js_code) -let wasmInstance; -let require; -let wasmExports; - export async function instantiate(imports={}, runInitializer=true) { + const externrefBoxes = new WeakMap(); + // ref must be non-null + function tryGetOrSetExternrefBox(ref, ifNotCached) { + if (typeof ref !== 'object') return ifNotCached; + const cachedBox = externrefBoxes.get(ref); + if (cachedBox !== void 0) return cachedBox; + externrefBoxes.set(ref, ifNotCached); + return ifNotCached; + } + + async function _importModule(x) { + return imports[x] ?? await import(x); + } + +$referencesToQualifiedAndImportedDeclarations + + const js_code = { +$jsCodeBodyIndented + } + + // Placed here to give access to it from externals (js_code) + let wasmInstance; + let require; + let wasmExports; + const isNodeJs = (typeof process !== 'undefined') && (process.release.name === 'node'); const isD8 = !isNodeJs && (typeof d8 !== 'undefined'); const isBrowser = !isNodeJs && !isD8 && (typeof window !== 'undefined'); diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/JsHelpers.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/JsHelpers.kt index fbb064c1101..332ceec014c 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/JsHelpers.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/JsHelpers.kt @@ -6,6 +6,20 @@ package org.jetbrains.kotlin.backend.wasm.ir2wasm import org.jetbrains.kotlin.js.backend.JsToStringGenerationVisitor +import java.util.Base64 fun String.toJsStringLiteral(): CharSequence = - JsToStringGenerationVisitor.javaScriptString(this) \ No newline at end of file + JsToStringGenerationVisitor.javaScriptString(this) + +data class JsModuleAndQualifierReference( + val module: String?, + val qualifier: String?, +) { + val jsVariableName = run { + // Encode variable name as base64 to have a valid unique JS identifier + val encoder = Base64.getEncoder().withoutPadding() + val moduleBase64 = module?.let { encoder.encodeToString(module.encodeToByteArray()) }.orEmpty() + val qualifierBase64 = qualifier?.let { encoder.encodeToString(qualifier.encodeToByteArray()) }.orEmpty() + "_ref_${moduleBase64}_$qualifierBase64" + } +} \ No newline at end of file diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/ComplexExternalDeclarationsToTopLevelFunctionsLowering.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/ComplexExternalDeclarationsToTopLevelFunctionsLowering.kt index 647746ca570..a409831e3bb 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/ComplexExternalDeclarationsToTopLevelFunctionsLowering.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/ComplexExternalDeclarationsToTopLevelFunctionsLowering.kt @@ -8,12 +8,15 @@ package org.jetbrains.kotlin.backend.wasm.lower import org.jetbrains.kotlin.backend.common.FileLoweringPass import org.jetbrains.kotlin.backend.common.lower.createIrBuilder import org.jetbrains.kotlin.backend.wasm.WasmBackendContext +import org.jetbrains.kotlin.backend.wasm.ir2wasm.JsModuleAndQualifierReference import org.jetbrains.kotlin.descriptors.ClassKind import org.jetbrains.kotlin.ir.IrElement import org.jetbrains.kotlin.backend.wasm.utils.getJsFunAnnotation import org.jetbrains.kotlin.backend.wasm.utils.getWasmImportDescriptor import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET +import org.jetbrains.kotlin.ir.backend.js.utils.getJsModule import org.jetbrains.kotlin.ir.backend.js.utils.getJsNameOrKotlinName +import org.jetbrains.kotlin.ir.backend.js.utils.getJsQualifier import org.jetbrains.kotlin.ir.backend.js.utils.realOverrideTarget import org.jetbrains.kotlin.ir.builders.declarations.addValueParameter import org.jetbrains.kotlin.ir.builders.declarations.buildFun @@ -108,7 +111,7 @@ class ComplexExternalDeclarationsToTopLevelFunctionsLowering(val context: WasmBa val dispatchReceiver = getter.dispatchReceiverParameter val jsCode = if (dispatchReceiver == null) - "() => $propName" + "() => ${referenceTopLevelExternalDeclaration(property)}" else "(_this) => _this.$propName" @@ -130,7 +133,7 @@ class ComplexExternalDeclarationsToTopLevelFunctionsLowering(val context: WasmBa val dispatchReceiver = setter.dispatchReceiverParameter val jsCode = if (dispatchReceiver == null) - "(v) => $propName = v" + "(v) => ${referenceTopLevelExternalDeclaration(property)} = v" else "(_this, v) => _this.$propName = v" @@ -159,12 +162,19 @@ class ComplexExternalDeclarationsToTopLevelFunctionsLowering(val context: WasmBa return } append('.') + append(klass.getJsNameOrKotlinName()) + } else { + append(referenceTopLevelExternalDeclaration(klass)) } - append(klass.getJsNameOrKotlinName()) } fun processExternalConstructor(constructor: IrConstructor) { val klass = constructor.constructedClass + + // External interfaces can have synthetic primary constructors in K/JS + if (klass.isInterface) + return + processFunctionOrConstructor( function = constructor, name = klass.name, @@ -183,14 +193,26 @@ class ComplexExternalDeclarationsToTopLevelFunctionsLowering(val context: WasmBa val jsFun = function.getJsFunAnnotation() // Wrap external functions without @JsFun to lambdas `foo` -> `(a, b) => foo(a, b)`. // This way we wouldn't fail if we don't call them. - if (jsFun != null && function.valueParameters.all { it.defaultValue == null && it.varargElementType == null }) + if (jsFun != null && + function.valueParameters.all { it.defaultValue == null && it.varargElementType == null } && + currentFile.getJsQualifier() == null && + currentFile.getJsModule() == null + ) { return + } + + val jsFunctionReference = when { + jsFun != null -> "($jsFun)" + function.isTopLevelDeclaration -> referenceTopLevelExternalDeclaration(function) + else -> function.getJsNameOrKotlinName().identifier + } + processFunctionOrConstructor( function = function, name = function.name, returnType = function.returnType, isConstructor = false, - jsFunctionReference = jsFun?.let { "($it)" } ?: function.getJsNameOrKotlinName().identifier + jsFunctionReference = jsFunctionReference ) } @@ -341,6 +363,25 @@ class ComplexExternalDeclarationsToTopLevelFunctionsLowering(val context: WasmBa addedDeclarations += res return res } + + private fun referenceTopLevelExternalDeclaration(declaration: IrDeclarationWithName): String { + var name = declaration.getJsNameOrKotlinName().identifier + + val qualifier = currentFile.getJsQualifier() + + val module = currentFile.getJsModule() + ?: declaration.getJsModule()?.also { + // JsModule on top level declarations imports "default" + name = "default" + } + + if (qualifier == null && module == null) + return name + + val qualifieReference = JsModuleAndQualifierReference(module, qualifier) + context.jsModuleAndQualifierReferences += qualifieReference + return qualifieReference.jsVariableName + "." + name + } } /** diff --git a/compiler/testData/codegen/boxWasmJsInterop/jsModule.kt b/compiler/testData/codegen/boxWasmJsInterop/jsModule.kt new file mode 100644 index 00000000000..1f68cb4ce7e --- /dev/null +++ b/compiler/testData/codegen/boxWasmJsInterop/jsModule.kt @@ -0,0 +1,52 @@ +// ES_MODULES +// FILE: jsModule.mjs +let x = 10; +function f() { return 10; }; +class C { + constructor(x) { + this.x = x; + } +} + +export { x, f, C }; +export default { defaultX: x, defaultF: f, defaultC: C }; + +// FILE: lib1.kt +@file:JsModule("./jsModule.mjs") + +package named + +external val x: Int +external fun f(): Int +external class C { + constructor(x: String) + val x: String +} + +// FILE: lib2.kt +package default + +@JsModule("./jsModule.mjs") +external object jsModule { + val defaultX: Int + fun defaultF(): Int + class defaultC { + constructor(x: String) + + val x: String + } +} + + +// FILE: main.kt +fun box(): String { + if (named.x != 10) return "Fail1" + if (named.f() != 10) return "Fail2" + if (named.C("10").x != "10") return "Fail3" + + if (default.jsModule.defaultX != 10) return "Fail4" + if (default.jsModule.defaultF() != 10) return "Fail5" + if (default.jsModule.defaultC("10").x != "10") return "Fail6" + + return "OK" +} \ No newline at end of file diff --git a/compiler/testData/codegen/boxWasmJsInterop/jsModuleWithQualifier.kt b/compiler/testData/codegen/boxWasmJsInterop/jsModuleWithQualifier.kt new file mode 100644 index 00000000000..69f6191a5b3 --- /dev/null +++ b/compiler/testData/codegen/boxWasmJsInterop/jsModuleWithQualifier.kt @@ -0,0 +1,102 @@ +// ES_MODULES +// FILE: jsModuleWithQualifier.mjs +let a = { + b: { + c: { + d: { + x: 10, + f() { return 10; }, + C: class C { + constructor(x) { + this.x = x; + } + } + } + } + } +}; + +export { a }; + +// FILE: lib1.kt +@file:JsQualifier("a.b.c.d") +@file:JsModule("./jsModuleWithQualifier.mjs") + +package abcd + +external var x: Int +external fun f(): Int +external class C { + constructor(x: String) + val x: String +} + +@JsName("x") +external var x2: Int + +@JsName("f") +external fun f2(): Int + +@JsName("C") +external class C2 { + constructor(x: String) + @JsName("x") + val x2: String +} + +// FILE: lib2.kt +@file:JsQualifier("a") +@file:JsModule("./jsModuleWithQualifier.mjs") +package a + +external object b { + class c { + companion object { + @JsName("d") + object d2 { + var x: Int + fun f(): Int + class C { + constructor(x: String) + val x: String + } + + @JsName("x") + var x2: Int + + @JsName("f") + fun f2(): Int + + @JsName("C") + class C2 { + constructor(x: String) + @JsName("x") + val x2: String + } + } + } + } +} + + + +// FILE: main.kt +fun box(): String { + if (abcd.x != 10) return "Fail1" + if (abcd.f() != 10) return "Fail2" + if (abcd.C("10").x != "10") return "Fail3" + + if (abcd.x2 != 10) return "Fail4" + if (abcd.f2() != 10) return "Fail5" + if (abcd.C2("10").x2 != "10") return "Fail6" + + if (a.b.c.Companion.d2.x != 10) return "Fail7" + if (a.b.c.Companion.d2.f() != 10) return "Fail8" + if (a.b.c.Companion.d2.C("10").x != "10") return "Fail9" + + if (a.b.c.Companion.d2.x2 != 10) return "Fail10" + if (a.b.c.Companion.d2.f2() != 10) return "Fail11" + if (a.b.c.Companion.d2.C2("10").x2 != "10") return "Fail12" + + return "OK" +} \ No newline at end of file diff --git a/compiler/testData/codegen/boxWasmJsInterop/jsQualifier.kt b/compiler/testData/codegen/boxWasmJsInterop/jsQualifier.kt new file mode 100644 index 00000000000..630f8c680a4 --- /dev/null +++ b/compiler/testData/codegen/boxWasmJsInterop/jsQualifier.kt @@ -0,0 +1,95 @@ +// FILE: jsQualifier.js +let a = { + b: { + c: { + d: { + x: 10, + f() { return 10; }, + C: class C { + constructor(x) { + this.x = x; + } + } + } + } + } +}; + +// FILE: lib1.kt +@file:JsQualifier("a.b.c.d") +package abcd + +external var x: Int +external fun f(): Int +external class C { + constructor(x: String) + val x: String +} + +@JsName("x") +external var x2: Int + +@JsName("f") +external fun f2(): Int + +@JsName("C") +external class C2 { + constructor(x: String) + @JsName("x") + val x2: String +} + +// FILE: lib2.kt +@file:JsQualifier("a") +package a + +external object b { + class c { + companion object { + @JsName("d") + object d2 { + var x: Int + fun f(): Int + class C { + constructor(x: String) + val x: String + } + + @JsName("x") + var x2: Int + + @JsName("f") + fun f2(): Int + + @JsName("C") + class C2 { + constructor(x: String) + @JsName("x") + val x2: String + } + } + } + } +} + + +// FILE: main.kt +fun box(): String { + if (abcd.x != 10) return "Fail1" + if (abcd.f() != 10) return "Fail2" + if (abcd.C("10").x != "10") return "Fail3" + + if (abcd.x2 != 10) return "Fail4" + if (abcd.f2() != 10) return "Fail5" + if (abcd.C2("10").x2 != "10") return "Fail6" + + if (a.b.c.Companion.d2.x != 10) return "Fail7" + if (a.b.c.Companion.d2.f() != 10) return "Fail8" + if (a.b.c.Companion.d2.C("10").x != "10") return "Fail9" + + if (a.b.c.Companion.d2.x2 != 10) return "Fail10" + if (a.b.c.Companion.d2.f2() != 10) return "Fail11" + if (a.b.c.Companion.d2.C2("10").x2 != "10") return "Fail12" + + return "OK" +} \ No newline at end of file diff --git a/js/js.tests/test/org/jetbrains/kotlin/generators/tests/GenerateJsTests.kt b/js/js.tests/test/org/jetbrains/kotlin/generators/tests/GenerateJsTests.kt index 2680fbde2be..47941621f82 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/generators/tests/GenerateJsTests.kt +++ b/js/js.tests/test/org/jetbrains/kotlin/generators/tests/GenerateJsTests.kt @@ -34,6 +34,15 @@ fun main(args: Array) { testClass { model("box/main", pattern = "^([^_](.+))\\.kt$", targetBackend = TargetBackend.WASM) model("box/native/", pattern = "^([^_](.+))\\.kt$", targetBackend = TargetBackend.WASM) + model("box/esModules/", pattern = "^([^_](.+))\\.kt$", targetBackend = TargetBackend.WASM, + excludeDirs = listOf( + // JsExport is not supported for classes + "jsExport", "native", "export", + // Multimodal infra is not supported. Also, we don't use ES modules for cross-module refs in Wasm + "crossModuleRef", "crossModuleRefPerFile", "crossModuleRefPerModule" + ) + ) + model("box/jsQualifier/", pattern = "^([^_](.+))\\.kt$", targetBackend = TargetBackend.WASM) } testClass { diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/fir/FirJsCodegenWasmJsInteropTestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/fir/FirJsCodegenWasmJsInteropTestGenerated.java index b9d35c4e9a9..75dd6df1572 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/fir/FirJsCodegenWasmJsInteropTestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/fir/FirJsCodegenWasmJsInteropTestGenerated.java @@ -55,6 +55,24 @@ public class FirJsCodegenWasmJsInteropTestGenerated extends AbstractFirJsCodegen runTest("compiler/testData/codegen/boxWasmJsInterop/jsExport.kt"); } + @Test + @TestMetadata("jsModule.kt") + public void testJsModule() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/jsModule.kt"); + } + + @Test + @TestMetadata("jsModuleWithQualifier.kt") + public void testJsModuleWithQualifier() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/jsModuleWithQualifier.kt"); + } + + @Test + @TestMetadata("jsQualifier.kt") + public void testJsQualifier() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/jsQualifier.kt"); + } + @Test @TestMetadata("jsToKotlinAdapters.kt") public void testJsToKotlinAdapters() throws Exception { diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrCodegenWasmJsInteropJsTestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrCodegenWasmJsInteropJsTestGenerated.java index 01ab2f479ca..6e399be418a 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrCodegenWasmJsInteropJsTestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrCodegenWasmJsInteropJsTestGenerated.java @@ -55,6 +55,24 @@ public class IrCodegenWasmJsInteropJsTestGenerated extends AbstractIrCodegenWasm runTest("compiler/testData/codegen/boxWasmJsInterop/jsExport.kt"); } + @Test + @TestMetadata("jsModule.kt") + public void testJsModule() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/jsModule.kt"); + } + + @Test + @TestMetadata("jsModuleWithQualifier.kt") + public void testJsModuleWithQualifier() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/jsModuleWithQualifier.kt"); + } + + @Test + @TestMetadata("jsQualifier.kt") + public void testJsQualifier() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/jsQualifier.kt"); + } + @Test @TestMetadata("jsToKotlinAdapters.kt") public void testJsToKotlinAdapters() throws Exception { diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/testOld/wasm/semantics/IrCodegenWasmJsInteropWasmTestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/testOld/wasm/semantics/IrCodegenWasmJsInteropWasmTestGenerated.java index 542200c1b9d..06f6854d788 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/testOld/wasm/semantics/IrCodegenWasmJsInteropWasmTestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/testOld/wasm/semantics/IrCodegenWasmJsInteropWasmTestGenerated.java @@ -70,6 +70,21 @@ public class IrCodegenWasmJsInteropWasmTestGenerated extends AbstractIrCodegenWa runTest("compiler/testData/codegen/boxWasmJsInterop/jsExport.kt"); } + @TestMetadata("jsModule.kt") + public void testJsModule() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/jsModule.kt"); + } + + @TestMetadata("jsModuleWithQualifier.kt") + public void testJsModuleWithQualifier() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/jsModuleWithQualifier.kt"); + } + + @TestMetadata("jsQualifier.kt") + public void testJsQualifier() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/jsQualifier.kt"); + } + @TestMetadata("jsToKotlinAdapters.kt") public void testJsToKotlinAdapters() throws Exception { runTest("compiler/testData/codegen/boxWasmJsInterop/jsToKotlinAdapters.kt"); diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/testOld/wasm/semantics/JsTranslatorWasmTestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/testOld/wasm/semantics/JsTranslatorWasmTestGenerated.java index fa5a2f2a872..c48dec1d86f 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/testOld/wasm/semantics/JsTranslatorWasmTestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/testOld/wasm/semantics/JsTranslatorWasmTestGenerated.java @@ -205,4 +205,162 @@ public class JsTranslatorWasmTestGenerated extends AbstractJsTranslatorWasmTest runTest("js/js.translator/testData/box/native/vararg.kt"); } } + + @TestMetadata("js/js.translator/testData/box/esModules") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class EsModules extends AbstractJsTranslatorWasmTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest0(this::doTest, TargetBackend.WASM, testDataFilePath); + } + + public void testAllFilesPresentInEsModules() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("js/js.translator/testData/box/esModules"), Pattern.compile("^([^_](.+))\\.kt$"), null, TargetBackend.WASM, true, "jsExport", "native", "export", "crossModuleRef", "crossModuleRefPerFile", "crossModuleRefPerModule"); + } + + @TestMetadata("js/js.translator/testData/box/esModules/incremental") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class Incremental extends AbstractJsTranslatorWasmTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest0(this::doTest, TargetBackend.WASM, testDataFilePath); + } + + public void testAllFilesPresentInIncremental() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("js/js.translator/testData/box/esModules/incremental"), Pattern.compile("^([^_](.+))\\.kt$"), null, TargetBackend.WASM, true); + } + + @TestMetadata("jsModule.kt") + public void testJsModule() throws Exception { + runTest("js/js.translator/testData/box/esModules/incremental/jsModule.kt"); + } + } + + @TestMetadata("js/js.translator/testData/box/esModules/inline") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class Inline extends AbstractJsTranslatorWasmTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest0(this::doTest, TargetBackend.WASM, testDataFilePath); + } + + public void testAllFilesPresentInInline() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("js/js.translator/testData/box/esModules/inline"), Pattern.compile("^([^_](.+))\\.kt$"), null, TargetBackend.WASM, true); + } + } + + @TestMetadata("js/js.translator/testData/box/esModules/jsModule") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class JsModule extends AbstractJsTranslatorWasmTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest0(this::doTest, TargetBackend.WASM, testDataFilePath); + } + + public void testAllFilesPresentInJsModule() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("js/js.translator/testData/box/esModules/jsModule"), Pattern.compile("^([^_](.+))\\.kt$"), null, TargetBackend.WASM, true); + } + + @TestMetadata("externalClass.kt") + public void testExternalClass() throws Exception { + runTest("js/js.translator/testData/box/esModules/jsModule/externalClass.kt"); + } + + @TestMetadata("externalClassNameClash.kt") + public void testExternalClassNameClash() throws Exception { + runTest("js/js.translator/testData/box/esModules/jsModule/externalClassNameClash.kt"); + } + + @TestMetadata("externalFunction.kt") + public void testExternalFunction() throws Exception { + runTest("js/js.translator/testData/box/esModules/jsModule/externalFunction.kt"); + } + + @TestMetadata("externalFunctionNameClash.kt") + public void testExternalFunctionNameClash() throws Exception { + runTest("js/js.translator/testData/box/esModules/jsModule/externalFunctionNameClash.kt"); + } + + @TestMetadata("externalObject.kt") + public void testExternalObject() throws Exception { + runTest("js/js.translator/testData/box/esModules/jsModule/externalObject.kt"); + } + + @TestMetadata("externalPackage.kt") + public void testExternalPackage() throws Exception { + runTest("js/js.translator/testData/box/esModules/jsModule/externalPackage.kt"); + } + + @TestMetadata("externalPackageInDifferentFile.kt") + public void testExternalPackageInDifferentFile() throws Exception { + runTest("js/js.translator/testData/box/esModules/jsModule/externalPackageInDifferentFile.kt"); + } + + @TestMetadata("externalProperty.kt") + public void testExternalProperty() throws Exception { + runTest("js/js.translator/testData/box/esModules/jsModule/externalProperty.kt"); + } + + @TestMetadata("interfaces.kt") + public void testInterfaces() throws Exception { + runTest("js/js.translator/testData/box/esModules/jsModule/interfaces.kt"); + } + + @TestMetadata("topLevelVarargFun.kt") + public void testTopLevelVarargFun() throws Exception { + runTest("js/js.translator/testData/box/esModules/jsModule/topLevelVarargFun.kt"); + } + } + + @TestMetadata("js/js.translator/testData/box/esModules/jsName") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class JsName extends AbstractJsTranslatorWasmTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest0(this::doTest, TargetBackend.WASM, testDataFilePath); + } + + public void testAllFilesPresentInJsName() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("js/js.translator/testData/box/esModules/jsName"), Pattern.compile("^([^_](.+))\\.kt$"), null, TargetBackend.WASM, true); + } + + @TestMetadata("defaultJsName.kt") + public void testDefaultJsName() throws Exception { + runTest("js/js.translator/testData/box/esModules/jsName/defaultJsName.kt"); + } + + @TestMetadata("jsTopLevelClashes.kt") + public void testJsTopLevelClashes() throws Exception { + runTest("js/js.translator/testData/box/esModules/jsName/jsTopLevelClashes.kt"); + } + } + } + + @TestMetadata("js/js.translator/testData/box/jsQualifier") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class JsQualifier extends AbstractJsTranslatorWasmTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest0(this::doTest, TargetBackend.WASM, testDataFilePath); + } + + public void testAllFilesPresentInJsQualifier() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("js/js.translator/testData/box/jsQualifier"), Pattern.compile("^([^_](.+))\\.kt$"), null, TargetBackend.WASM, true); + } + + @TestMetadata("classes.kt") + public void testClasses() throws Exception { + runTest("js/js.translator/testData/box/jsQualifier/classes.kt"); + } + + @TestMetadata("interfaces.kt") + public void testInterfaces() throws Exception { + runTest("js/js.translator/testData/box/jsQualifier/interfaces.kt"); + } + + @TestMetadata("simple.kt") + public void testSimple() throws Exception { + runTest("js/js.translator/testData/box/jsQualifier/simple.kt"); + } + } } diff --git a/js/js.translator/testData/box/esModules/inline/inlinedObjectLiteralIsCheck.kt b/js/js.translator/testData/box/esModules/inline/inlinedObjectLiteralIsCheck.kt index ec9cdcec83f..c0b2e267ee6 100644 --- a/js/js.translator/testData/box/esModules/inline/inlinedObjectLiteralIsCheck.kt +++ b/js/js.translator/testData/box/esModules/inline/inlinedObjectLiteralIsCheck.kt @@ -1,6 +1,9 @@ // DONT_TARGET_EXACT_BACKEND: JS // ES_MODULES +// Generated .mjs name is different in Wasm +// DONT_TARGET_EXACT_BACKEND: WASM + // FILE: main.kt interface I { fun ok(): String diff --git a/js/js.translator/testData/box/esModules/jsModule/externalClassWithDefaults.kt b/js/js.translator/testData/box/esModules/jsModule/externalClassWithDefaults.kt index 1abf29b8c15..89cb4c348ac 100644 --- a/js/js.translator/testData/box/esModules/jsModule/externalClassWithDefaults.kt +++ b/js/js.translator/testData/box/esModules/jsModule/externalClassWithDefaults.kt @@ -2,6 +2,9 @@ // ES_MODULES // DONT_TARGET_EXACT_BACKEND: JS +// DONT_TARGET_EXACT_BACKEND: WASM +// WASM_MUTE_REASON: UNSUPPORTED_JS_INTEROP + package foo @JsModule("./externalClassWithDefaults.mjs") diff --git a/js/js.translator/testData/box/esModules/jsModule/externalConstructor.kt b/js/js.translator/testData/box/esModules/jsModule/externalConstructor.kt index 47a9d24d3c8..8c04943e87f 100644 --- a/js/js.translator/testData/box/esModules/jsModule/externalConstructor.kt +++ b/js/js.translator/testData/box/esModules/jsModule/externalConstructor.kt @@ -2,6 +2,9 @@ // ES_MODULES // DONT_TARGET_EXACT_BACKEND: JS +// DONT_TARGET_EXACT_BACKEND: WASM +// WASM_MUTE_REASON: UNSUPPORTED_JS_INTEROP + package foo @JsModule("./externalConstructor.mjs") diff --git a/js/js.translator/testData/box/jsQualifier/umdFallback.kt b/js/js.translator/testData/box/jsQualifier/umdFallback.kt index ce401248a5d..bd791d9039c 100644 --- a/js/js.translator/testData/box/jsQualifier/umdFallback.kt +++ b/js/js.translator/testData/box/jsQualifier/umdFallback.kt @@ -1,5 +1,9 @@ // IGNORE_FIR // EXPECTED_REACHABLE_NODES: 1284 + +// DONT_TARGET_EXACT_BACKEND: WASM +// WASM_MUTE_REASON: MODULE_KIND + // NO_JS_MODULE_SYSTEM // MODULE: lib // MODULE_KIND: UMD diff --git a/js/js.translator/testData/box/jsQualifier/withModule.kt b/js/js.translator/testData/box/jsQualifier/withModule.kt index ba4703f4d49..6b74d9d00c8 100644 --- a/js/js.translator/testData/box/jsQualifier/withModule.kt +++ b/js/js.translator/testData/box/jsQualifier/withModule.kt @@ -1,5 +1,9 @@ // IGNORE_FIR // EXPECTED_REACHABLE_NODES: 1282 + +// DONT_TARGET_EXACT_BACKEND: WASM +// WASM_MUTE_REASON: MODULE_KIND + // MODULE: lib // MODULE_KIND: AMD // FILE: lib.kt diff --git a/libraries/stdlib/wasm/src/kotlin/js/annotations.kt b/libraries/stdlib/wasm/src/kotlin/js/annotations.kt index 29e0c38fd11..0905b5e76d3 100644 --- a/libraries/stdlib/wasm/src/kotlin/js/annotations.kt +++ b/libraries/stdlib/wasm/src/kotlin/js/annotations.kt @@ -37,4 +37,65 @@ public actual annotation class JsExport { AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER ) -public actual annotation class JsName(actual val name: String) \ No newline at end of file +public actual annotation class JsName(actual val name: String) + +/** + * Denotes an `external` declaration that must be imported from JavaScript module. + * + * The annotation can be used on top-level external declarations (classes, properties, functions) and files. + * In case of file (which can't be `external`) the following rule applies: all the declarations in + * the file must be `external`. By applying `@JsModule(...)` on a file you tell the compiler to import a JavaScript object + * that contain all the declarations from the file. + * + * Example: + * + * ``` kotlin + * @JsModule("jquery") + * external abstract class JQuery() { + * // some declarations here + * } + * + * @JsModule("jquery") + * external fun JQuery(element: Element): JQuery + * ``` + * + * @property import name of a module to import declaration from. + * It is not interpreted by the Kotlin compiler, it's passed as is directly to the target module system. + */ +@Retention(AnnotationRetention.BINARY) +@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION, AnnotationTarget.FILE) +public annotation class JsModule(val import: String) + + +/** + * Adds prefix to `external` declarations in a source file. + * + * JavaScript does not have concept of packages (namespaces). They are usually emulated by nested objects. + * The compiler turns references to `external` declarations either to plain unprefixed names + * or to plain imports. + * However, if a JavaScript library provides its declarations in packages, you won't be satisfied with this. + * You can tell the compiler to generate additional prefix before references to `external` declarations using the `@JsQualifier(...)` + * annotation. + * + * Note that a file marked with the `@JsQualifier(...)` annotation can't contain non-`external` declarations. + * + * Example: + * + * ``` + * @file:JsQualifier("my.jsPackageName") + * package some.kotlinPackage + * + * external fun foo(x: Int) + * + * external fun bar(): String + * ``` + * + * @property value the qualifier to add to the declarations in the generated code. + * It must be a sequence of valid JavaScript identifiers separated by the `.` character. + * Examples of valid qualifiers are: `foo`, `bar.Baz`, `_.$0.f`. + * + * @see JsModule + */ +@Retention(AnnotationRetention.BINARY) +@Target(AnnotationTarget.FILE) +public annotation class JsQualifier(val value: String) \ No newline at end of file