Hi, I’m writing a simple compiler plugin to do sli...
# compiler
k
Hi, I’m writing a simple compiler plugin to do slight modifications in the generated code for our JS target. I’m invoking the
compileProductionLibraryKotlinJs
task and it seems JS gets compiled twice - once into a klib where the compiler plugin is called correctly and later into a JS library where the compiler plugin is not invoked at all. This means that my changes are visible in the klib but not in the resulting JS library code. Is this an expected behavior? Can I get the IR plugin to execute every time a JS compilation is running? It also seems inefficient to run it twice (I don’t necessarily need the klib...). I’m on Kotlin 1.5.31, Gradle 7.2. My Gradle configuration:
Copy code
js(IR) {
    moduleName = "facemoji-core"
    browser {
        webpackTask {
            output.libraryTarget = "commonjs-module"
        }
    }
    binaries.library()

    compilations.all {
        kotlinOptions {
            freeCompilerArgs = freeCompilerArgs + "-Xopt-in=kotlin.ExperimentalStdlibApi -Xopt-in=kotlin.RequiresOptIn"
        }
    }
}
I’ve tracked it through the compiler code and in the first pass only the first condition is true, klib directory gets generated and the plugin runs. In the second pass the JS gets produced, only the second condition is true and the plugin doesn’t get invoked.
s
The JS libraries are distributed and linked as klibs, so compiler creates a "Kotlin library" first and compiles it into JS as a second step. As you correctly noticed, plugin is only applied during first compilation for now. This causes quite a few headaches sometimes, if the plugin changes public API of the module🙃
k
@shikasd Ah, snap 🙂 Is there a(nother) way to add things to the public JS API during compilation (not necessarily using IR)?
s
Adding is simple, doesn't really differ from other platforms, in my experience, but changing existing methods could be painful
k
Very useful article, thank you. I’m basically just trying to automatically add JsExport annotations to our public APIs. If I understand it correctly it seems I’ll need to create copies of everything (decoys) as adding an annotation actually changes the class/function signature?
s
I think you might be able to add this just fine without copying everything, as annotations are not included in method signature
Copying is required only after modifying types/value params of methods
k
This is the IR transformer I got so far:
Copy code
class JSExportTransformer(  private val pluginContext: IrPluginContext
) : IrElementTransformerVoidWithContext() {
  private val jsExportFqName = FqName("kotlin.js.JsExport")
  private val jsExperimentalExportFqName = FqName("kotlin.js.ExperimentalJsExport")
  private val jsExportConstructor = pluginContext.referenceClass(jsExportFqName)!!.constructors.first()
  private val jsExperimentalExportConstructor = pluginContext.referenceClass(jsExperimentalExportFqName)!!.constructors.first()

  private fun createExportAnnotations(): List<IrConstructorCall> {
    val jsExportCall = IrConstructorCallImpl.fromSymbolOwner(
      jsExportConstructor.owner.returnType,
      jsExportConstructor,
    )
    val jsExperimentalExportCall = IrConstructorCallImpl.fromSymbolOwner(
      jsExperimentalExportConstructor.owner.returnType,
      jsExperimentalExportConstructor
    )
    return listOf(jsExperimentalExportCall, jsExportCall)
  }

  override fun visitClassNew(declaration: IrClass): IrStatement {
    val isPublic = declaration.visibility.isPublicAPI
    val isExportable = declaration.kind != ClassKind.ENUM_ENTRY && declaration.isInline
    if (!isPublic || !isExportable || declaration.isExpect || declaration.isInner || declaration.isCompanion || declaration.isAnnotationClass || declaration.isAnnotatedWithDeprecated) {
      return super.visitClassNew(declaration)
    }
    val alreadyAnnotated = declaration.annotations.any { it.type.classFqName?.toString() == jsExportFqName.toString() }
    if (!alreadyAnnotated) {
      declaration.annotations += createExportAnnotations()
    }
    return super.visitClassNew(declaration)
  }
}
I checked in debugger that the code gets called (during the klib phase) and also checked the IR deserialization in the JS phase and the annotations are gone. Of course, the symbols are not exported to JS. I invoke this transformer in the generate() method of the IR extension plugin:
Copy code
override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
  // Checks for JS platform etc. omitted for brevity
  val transformer = JSExportTransformer(pluginContext)
  moduleFragment.transform(transformer, null)
}
Sorry for bothering you with this, I’ve been trying to figure out why the annotations disappear but so far I’m pretty lost as to where to look for further hints.
s
It is honestly hard to say without looking up how Kotlin checks for JsExport things, as it might be done somewhere before IR even created From the top of my head, it might be caused by the fact that compiler doesn't know you have annotations added there and doesn't deserialize them. You can check by adding some annotation to the method in code, it should deserialize both after. Another thing that might be possible is that JsExport logic reads descriptors and not IR. Descriptors are created in a frontend phase and as you are not modifying them in your plugin, the compiler ignores then
It might be the case that JsExport is just converted to a flag in descriptor/function, so I recommend looking up how it is used inside compiler code
k
I looked it up and it checks for the presence of the annotation in the JS phase so all should be good there. Good hint with the deserialization, will try to check that.