[Wasm] Support @JsExport.Default in Kotlin/Wasm

The @JsExport.Default annotation was already available in Kotlin/Wasm
(same stdlib class as Kotlin/JS), but the Wasm backend ignored it and
treated the declaration as a regular named export instead of generating
an ES module default export.

Two things were missing:

1. TypeScript declarations (.d.mts): The Wasm ExportModelGenerator's
   withAttributesFor() was not setting ExportedAttribute.DefaultExport
   on declarations annotated with @JsExport.Default. Fix: check
   isJsExportDefault() and add the attribute, the same way the JS
   backend does in its ExportModelGenerator.

2. JS wrapper (.mjs): generateExports() was treating all @JsExport
   declarations as named exports. Fix: collect the set of
   default-exported function names from the export model (which is now
   always computed for wasm-js targets, not only when generateDts is
   on), partition Wasm IR exports into named vs. default, emit
   'export const { ... }' only for named exports, and append
   'export default exports.name;' for each default export.

The defaultExportNames set flows from compileToLoweredIr() through
LoweredIrWithExtraArtifacts and WasmIrModuleConfiguration to
generateExports(). IC and WASI builds default to emptySet() since
default-export semantics are not applicable there.

Test: updated typescript-export/wasm/default to import produceUByte
as a default export and to expect the correct .d.mts output.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

^KT-81801 Fixed
diff --git a/compiler/cli/cli-js/src/org/jetbrains/kotlin/cli/pipeline/web/wasm/KotlinIr2WasmIrCompiler.kt b/compiler/cli/cli-js/src/org/jetbrains/kotlin/cli/pipeline/web/wasm/KotlinIr2WasmIrCompiler.kt
index ac4e384..acfeedb 100644
--- a/compiler/cli/cli-js/src/org/jetbrains/kotlin/cli/pipeline/web/wasm/KotlinIr2WasmIrCompiler.kt
+++ b/compiler/cli/cli-js/src/org/jetbrains/kotlin/cli/pipeline/web/wasm/KotlinIr2WasmIrCompiler.kt
@@ -168,6 +168,7 @@
         typeScriptFragment = typeScriptFragment,
         baseFileName = configuration.outputName!!,
         multimoduleOptions = null,
+        defaultExportNames = loweredIr.defaultExportNames,
     )
 }
 
@@ -278,6 +279,7 @@
         typeScriptFragment = loweredIr.typeScriptFragment,
         baseFileName = mainModuleFragment.outputFileName,
         multimoduleOptions = multimoduleOptions,
+        defaultExportNames = loweredIr.defaultExportNames,
     )
 }
 
diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/export/ExportModelGenerator.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/export/ExportModelGenerator.kt
index 29c73cd..ce9df90 100644
--- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/export/ExportModelGenerator.kt
+++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/export/ExportModelGenerator.kt
@@ -17,6 +17,7 @@
 import org.jetbrains.kotlin.ir.backend.js.utils.getDeprecated
 import org.jetbrains.kotlin.ir.backend.js.utils.getFqNameWithJsNameWhenAvailable
 import org.jetbrains.kotlin.ir.backend.js.utils.isExplicitlyExported
+import org.jetbrains.kotlin.ir.backend.js.utils.isJsExportDefault
 import org.jetbrains.kotlin.ir.backend.js.utils.realOverrideTarget
 import org.jetbrains.kotlin.ir.declarations.*
 import org.jetbrains.kotlin.ir.symbols.IrClassSymbol
@@ -390,5 +391,6 @@
 
 private fun <T : ExportedDeclaration> T.withAttributesFor(declaration: IrDeclaration): T {
     declaration.getDeprecated()?.let { attributes.add(ExportedAttribute.DeprecatedAttribute(it)) }
+    if (declaration.isJsExportDefault()) attributes.add(ExportedAttribute.DefaultExport)
     return this
 }
diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/wasmCompiler.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/wasmCompiler.kt
index 03bf30b..2b33653 100644
--- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/wasmCompiler.kt
+++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/wasmCompiler.kt
@@ -25,6 +25,9 @@
 import org.jetbrains.kotlin.ir.backend.js.MainModule
 import org.jetbrains.kotlin.ir.backend.js.WholeWorldStageController
 import org.jetbrains.kotlin.ir.backend.js.tsexport.ExportModelToTsDeclarations
+import org.jetbrains.kotlin.ir.backend.js.tsexport.ExportedAttribute
+import org.jetbrains.kotlin.ir.backend.js.tsexport.ExportedFunction
+import org.jetbrains.kotlin.ir.backend.js.tsexport.ExportedMemberName
 import org.jetbrains.kotlin.ir.backend.js.tsexport.TypeScriptFragment
 import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
 import org.jetbrains.kotlin.ir.types.isString
@@ -80,6 +83,7 @@
     val backendContext: WasmBackendContext,
     val typeScriptFragment: TypeScriptFragment?,
     val moduleDependencies: (IrModuleFragment) -> Set<IrModuleFragment>,
+    val defaultExportNames: Set<String> = emptySet(),
 )
 
 fun linkIr(
@@ -137,13 +141,22 @@
         }
     }
 
-    val typeScriptFragment = runIf(configuration.generateDts) {
-        val exportModel = ExportModelGenerator(context).generateExport(allModules)
+    val exportModel = runIf(context.isWasmJsTarget) {
+        ExportModelGenerator(context).generateExport(allModules)
+    }
+
+    val typeScriptFragment = runIf(configuration.generateDts && exportModel != null) {
         val exportModelToDtsTranslator = ExportModelToTsDeclarations(ModuleKind.ES)
-        val fragment = exportModelToDtsTranslator.generateTypeScriptFragment(exportModel.declarations)
+        val fragment = exportModelToDtsTranslator.generateTypeScriptFragment(exportModel!!.declarations)
         TypeScriptFragment(exportModelToDtsTranslator.generateTypeScript("", listOf(fragment)))
     }
 
+    val defaultExportNames = exportModel?.declarations
+        ?.filter { ExportedAttribute.DefaultExport in it.attributes }
+        ?.filterIsInstance<ExportedFunction>()
+        ?.mapNotNull { (it.name as? ExportedMemberName.Identifier)?.value }
+        ?.toSet() ?: emptySet()
+
     lowerPreservingTags(
         allModules,
         context,
@@ -162,7 +175,8 @@
         loweredIr = allModules,
         backendContext = context,
         typeScriptFragment = typeScriptFragment,
-        moduleDependencies = dependencyTracker::getAllDependencies
+        moduleDependencies = dependencyTracker::getAllDependencies,
+        defaultExportNames = defaultExportNames,
     )
 }
 
@@ -211,6 +225,7 @@
     val baseFileName: String,
     val typeScriptFragment: TypeScriptFragment?,
     val multimoduleOptions: MultimoduleCompileOptions?,
+    val defaultExportNames: Set<String> = emptySet(),
 )
 
 fun linkWasmIr(moduleConfiguration: WasmIrModuleConfiguration): WasmModule {
@@ -361,7 +376,8 @@
             isStdlibModule = isStdlibModule,
             wholeProgramMode = wholeProgramMode,
             wasmStartFunctionDefined = wasmStartFunctionDefined,
-            wasmInitializeFunctionDefined = wasmInitializeFunctionDefined
+            wasmInitializeFunctionDefined = wasmInitializeFunctionDefined,
+            defaultExportNames = moduleConfiguration.defaultExportNames,
         )
 
     } else {
@@ -655,7 +671,8 @@
     isStdlibModule: Boolean,
     wholeProgramMode: Boolean,
     wasmStartFunctionDefined: Boolean,
-    wasmInitializeFunctionDefined: Boolean
+    wasmInitializeFunctionDefined: Boolean,
+    defaultExportNames: Set<String> = emptySet(),
 ): String {
 
     val commonStdlibExports = if (isStdlibModule) ", getCachedJsObject, __TAG as wasmTag" else ""
@@ -731,7 +748,7 @@
 setWasmExports(exports);
 $mainFunctionCall
 
-${generateExports(exports, wholeProgramMode, isStdlibModule)}
+${generateExports(exports, wholeProgramMode, isStdlibModule, defaultExportNames)}
 """
 }
 
@@ -784,12 +801,15 @@
     exports: List<WasmExport<*>>,
     wholeProgramMode: Boolean,
     isStdlibModule: Boolean,
+    defaultExportNames: Set<String> = emptySet(),
 ): String {
     // TODO: necessary to move export check onto common place
     val exportNames = exports
         .filterNot { it.isWasmInternalUsageExport }
 
-    val [validIdentifiers, notValidIdentifiers] = exportNames.partition { it.name.isValidES5Identifier() }
+    val (defaultExports, namedExports) = exportNames.partition { it.name in defaultExportNames }
+
+    val [validIdentifiers, notValidIdentifiers] = namedExports.partition { it.name.isValidES5Identifier() }
     val regularlyExportedVariables = validIdentifiers
         .ifNotEmpty {
             """
@@ -818,6 +838,14 @@
         }
         .orEmpty()
 
+    val defaultExportStatements = defaultExports
+        .joinToString("\n") { export ->
+            if (export.name.isValidES5Identifier())
+                "export default exports.${export.name};"
+            else
+                "export default exports['${export.name.replace("'", "\\'")}'];"
+        }
+
     val commonStdlibExports =
         if (isStdlibModule)
             """
@@ -840,6 +868,7 @@
 
 $regularlyExportedVariables
 $escapedExportedVariables
+$defaultExportStatements
 """
 }
 
diff --git a/js/js.translator/testData/typescript-export/wasm/default/default__main.ts b/js/js.translator/testData/typescript-export/wasm/default/default__main.ts
index 75fa126..4ac9042 100644
--- a/js/js.translator/testData/typescript-export/wasm/default/default__main.ts
+++ b/js/js.translator/testData/typescript-export/wasm/default/default__main.ts
@@ -1,5 +1,4 @@
-import {
-    produceUByte,
+import produceUByte, {
     produceUInt,
     produceUShort
 } from "./index.mjs"
diff --git a/js/js.translator/testData/typescript-export/wasm/default/index.d.mts b/js/js.translator/testData/typescript-export/wasm/default/index.d.mts
index bd2f496..365f6e3 100644
--- a/js/js.translator/testData/typescript-export/wasm/default/index.d.mts
+++ b/js/js.translator/testData/typescript-export/wasm/default/index.d.mts
@@ -2,4 +2,5 @@
 declare function KtSingleton<T>(): T & (abstract new() => any);
 export declare function produceUShort(): number;
 export declare function produceUInt(): number;
-export declare function produceUByte(): number;
+declare function produceUByte(): number;
+export default produceUByte;