[PL][tests] K/JS: Run compiler through CLI interface

This commit is contained in:
Dmitriy Dolovov
2023-07-11 16:58:12 +02:00
committed by Space Team
parent 19c6208cc0
commit 715aba3d18
2 changed files with 128 additions and 279 deletions
@@ -21,7 +21,7 @@ object PartialLinkageTestUtils {
val testModeConstructorParameters: Map<String, String>
// Customize the source code of a module before compiling it to a KLIB.
fun customizeModuleSources(moduleName: String, moduleSourceDir: File) = Unit
fun customizeModuleSources(moduleName: String, moduleSourceDir: File)
// Build a KLIB from a module.
fun buildKlib(moduleName: String, buildDirs: ModuleBuildDirs, dependencies: Dependencies, klibFile: File)
@@ -93,6 +93,9 @@ object PartialLinkageTestUtils {
// Populate the source dir with *.kt files.
copySources(from = moduleTestDir, to = moduleBuildDirs.sourceDir)
// Customize the source dir if necessary.
customizeModuleSources(moduleName, moduleBuildDirs.sourceDir)
// Include PL utils into the main module.
if (moduleName == MAIN_MODULE_NAME) {
val utilsDir = testDir.parentFile.resolve(PL_UTILS_DIR)
@@ -110,8 +113,6 @@ object PartialLinkageTestUtils {
}
}
customizeModuleSources(moduleName, moduleBuildDirs.sourceDir)
moduleBuildDirs.outputDir.apply { mkdirs() }
this[moduleName] = ModuleUnderTest(
@@ -5,54 +5,20 @@
package org.jetbrains.kotlin.js.test.ir
import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.vfs.local.CoreLocalFileSystem
import com.intellij.psi.PsiManager
import com.intellij.psi.SingleRootFileViewProvider
import com.intellij.testFramework.TestDataFile
import org.jetbrains.kotlin.backend.common.phaser.PhaseConfig
import org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport
import org.jetbrains.kotlin.cli.common.messages.MessageRenderer
import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector
import org.jetbrains.kotlin.cli.common.ExitCode
import org.jetbrains.kotlin.cli.js.K2JsIrCompiler
import org.jetbrains.kotlin.cli.js.klib.*
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.config.CommonConfigurationKeys
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.diagnostics.DiagnosticReporterFactory
import org.jetbrains.kotlin.ir.backend.js.*
import org.jetbrains.kotlin.ir.backend.js.ic.CacheUpdater
import org.jetbrains.kotlin.ir.backend.js.ic.JsExecutableProducer
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.CompilationOutputs
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.IrModuleToJsTransformer
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.JsGenerationGranularity
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.TranslationMode
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
import org.jetbrains.kotlin.ir.declarations.impl.IrFactoryImpl
import org.jetbrains.kotlin.ir.declarations.impl.IrFactoryImplForJsIC
import org.jetbrains.kotlin.ir.linkage.partial.PartialLinkageConfig
import org.jetbrains.kotlin.ir.linkage.partial.PartialLinkageLogLevel
import org.jetbrains.kotlin.ir.linkage.partial.PartialLinkageMode
import org.jetbrains.kotlin.ir.linkage.partial.setupPartialLinkageConfig
import org.jetbrains.kotlin.ir.symbols.IrSymbol
import org.jetbrains.kotlin.js.config.JSConfigurationKeys
import org.jetbrains.kotlin.js.testOld.V8IrJsTestChecker
import org.jetbrains.kotlin.klib.PartialLinkageTestUtils
import org.jetbrains.kotlin.klib.PartialLinkageTestUtils.Dependencies
import org.jetbrains.kotlin.klib.PartialLinkageTestUtils.MAIN_MODULE_NAME
import org.jetbrains.kotlin.klib.PartialLinkageTestUtils.ModuleBuildDirs
import org.jetbrains.kotlin.konan.file.ZipFileSystemCacheableAccessor
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.serialization.js.ModuleKind
import org.jetbrains.kotlin.test.utils.TestDisposable
import org.junit.jupiter.api.AfterEach
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.PrintStream
import java.nio.charset.Charset
import kotlin.io.path.createTempDirectory
abstract class AbstractJsPartialLinkageNoICTestCase : AbstractJsPartialLinkageTestCase(CompilerType.K1_NO_IC)
@@ -60,56 +26,44 @@ abstract class AbstractJsPartialLinkageNoICES6TestCase : AbstractJsPartialLinkag
abstract class AbstractJsPartialLinkageWithICTestCase : AbstractJsPartialLinkageTestCase(CompilerType.K1_WITH_IC)
abstract class AbstractFirJsPartialLinkageNoICTestCase : AbstractJsPartialLinkageTestCase(CompilerType.K2_NO_IC)
abstract class AbstractJsPartialLinkageTestCase(val compilerType: CompilerType) {
enum class CompilerType(val es6Mode: Boolean) {
K1_NO_IC(false),
K1_NO_IC_WITH_ES6(true),
K1_WITH_IC(false),
K2_NO_IC(false)
abstract class AbstractJsPartialLinkageTestCase(private val compilerType: CompilerType) {
enum class CompilerType(val useFir: Boolean, val es6Mode: Boolean, val useIc: Boolean) {
K1_NO_IC(useFir = false, es6Mode = false, useIc = false),
K1_NO_IC_WITH_ES6(useFir = false, es6Mode = true, useIc = false),
K1_WITH_IC(useFir = false, es6Mode = false, useIc = true),
K2_NO_IC(useFir = true, es6Mode = false, useIc = false)
}
private val zipAccessor = ZipFileSystemCacheableAccessor(2)
private val buildDir = createTempDirectory().toFile().also { it.mkdirs() }
private val environment =
KotlinCoreEnvironment.createForParallelTests(TestDisposable(), CompilerConfiguration(), EnvironmentConfigFiles.JS_CONFIG_FILES)
private val buildDir: File = createTempDirectory().toFile().also { it.mkdirs() }
@AfterEach
fun clearArtifacts() {
zipAccessor.reset()
buildDir.deleteRecursively()
}
private fun createConfig(moduleName: String): CompilerConfiguration {
val config = environment.configuration.copy()
config.put(CommonConfigurationKeys.MODULE_NAME, moduleName)
config.put(JSConfigurationKeys.MODULE_KIND, ModuleKind.PLAIN)
config.put(JSConfigurationKeys.PROPERTY_LAZY_INITIALIZATION, true)
zipAccessor.reset()
config.put(JSConfigurationKeys.ZIP_FILE_SYSTEM_ACCESSOR, zipAccessor)
return config
}
private inner class JsTestConfiguration(testPath: String) : PartialLinkageTestUtils.TestConfiguration {
override val testDir: File = File(testPath).absoluteFile
override val buildDir: File get() = this@AbstractJsPartialLinkageTestCase.buildDir
override val stdlibFile: File get() = File("libraries/stdlib/js-ir/build/classes/kotlin/js/main").absoluteFile
override val testModeConstructorParameters = mapOf("isJs" to "true")
override fun customizeModuleSources(moduleName: String, moduleSourceDir: File) {
if (moduleName == MAIN_MODULE_NAME)
customizeMainModuleSources(moduleSourceDir)
}
override fun buildKlib(
moduleName: String,
buildDirs: ModuleBuildDirs,
dependencies: Dependencies,
klibFile: File
klibFile: File,
) = this@AbstractJsPartialLinkageTestCase.buildKlib(moduleName, buildDirs, dependencies, klibFile)
override fun buildBinaryAndRun(mainModuleKlibFile: File, dependencies: Dependencies) =
this@AbstractJsPartialLinkageTestCase.buildBinaryAndRun(mainModuleKlibFile, dependencies)
override fun onNonEmptyBuildDirectory(directory: File) {
zipAccessor.reset()
directory.listFiles()?.forEach(File::deleteRecursively)
}
@@ -121,260 +75,154 @@ abstract class AbstractJsPartialLinkageTestCase(val compilerType: CompilerType)
// The entry point to generated test classes.
fun runTest(@TestDataFile testPath: String) = PartialLinkageTestUtils.runTest(JsTestConfiguration(testPath))
private fun buildKlib(moduleName: String, buildDirs: ModuleBuildDirs, dependencies: Dependencies, klibFile: File) {
buildDirs.sourceDir.walkTopDown()
.filter { file -> file.isFile && file.extension == "js" }
.forEach { file -> file.copyTo(buildDirs.outputDir.resolve(file.relativeTo(buildDirs.sourceDir)), overwrite = true) }
when (compilerType) {
CompilerType.K1_NO_IC, CompilerType.K1_NO_IC_WITH_ES6, CompilerType.K1_WITH_IC ->
buildKlibWithK1(moduleName, buildDirs.sourceDir, dependencies, klibFile)
CompilerType.K2_NO_IC -> buildKlibWithK2(moduleName, buildDirs.sourceDir, dependencies, klibFile)
private fun customizeMainModuleSources(moduleSourceDir: File) {
// Add @JsExport annotation to make box function be visible for Node.
moduleSourceDir.walkTopDown().forEach { file ->
if (file.extension == "kt") {
var modified = false
val lines = file.readLines().map { line ->
if (line.startsWith("fun $BOX_FUN_FQN()")) {
modified = true
"@OptIn(ExperimentalJsExport::class) @JsExport $line"
} else
line
}
if (modified) file.writeText(lines.joinToString("\n"))
}
}
}
private fun buildKlibWithK1(moduleName: String, moduleSourceDir: File, dependencies: Dependencies, klibFile: File) {
val config = createConfig(moduleName)
val ktFiles = environment.createPsiFiles(moduleSourceDir)
fun buildKlib(moduleName: String, buildDirs: ModuleBuildDirs, dependencies: Dependencies, klibFile: File) {
val kotlinSourceFilePaths = mutableListOf<String>()
val regularDependencies = dependencies.regularDependencies.map { it.libraryFile.absolutePath }
val friendDependencies = dependencies.friendDependencies.map { it.libraryFile.absolutePath }
val moduleStructure = prepareAnalyzedSourceModule(
environment.project,
ktFiles,
config,
regularDependencies,
friendDependencies,
AnalyzerWithCompilerReport(config)
)
val moduleSourceFiles = (moduleStructure.mainModule as MainModule.SourceFiles).files
val icData = moduleStructure.compilerConfiguration.incrementalDataProvider?.getSerializedData(moduleSourceFiles) ?: emptyList()
val expectDescriptorToSymbol = mutableMapOf<DeclarationDescriptor, IrSymbol>()
val (moduleFragment, _) = generateIrForKlibSerialization(
environment.project,
moduleSourceFiles,
config,
moduleStructure.jsFrontEndResult.jsAnalysisResult,
sortDependencies(moduleStructure.moduleDependencies),
icData,
expectDescriptorToSymbol,
IrFactoryImpl,
verifySignatures = true
) {
moduleStructure.getModuleDescriptor(it)
buildDirs.sourceDir.walkTopDown().forEach { sourceFile ->
if (sourceFile.isFile) when (sourceFile.extension) {
"kt" -> kotlinSourceFilePaths += sourceFile.absolutePath
"js" -> {
// This is needed to preserve *.js files from test data which are required for tests with `external` declarations:
sourceFile.copyTo(buildDirs.outputDir.resolve(sourceFile.relativeTo(buildDirs.sourceDir)), overwrite = true)
}
}
}
val metadataSerializer =
KlibMetadataIncrementalSerializer(config, moduleStructure.project, moduleStructure.jsFrontEndResult.hasErrors)
generateKLib(
moduleStructure,
klibFile.path,
nopack = false,
jsOutputName = moduleName,
icData = icData,
expectDescriptorToSymbol = expectDescriptorToSymbol,
moduleFragment = moduleFragment
) { file ->
metadataSerializer.serializeScope(file, moduleStructure.jsFrontEndResult.bindingContext, moduleFragment.descriptor)
}
}
private fun buildKlibWithK2(moduleName: String, moduleSourceDir: File, dependencies: Dependencies, klibFile: File) {
val config = createConfig(moduleName)
val ktFiles = environment.createPsiFiles(moduleSourceDir)
val regularDependencies = dependencies.regularDependencies.map { it.libraryFile.absolutePath }
val friendDependencies = dependencies.friendDependencies.map { it.libraryFile.absolutePath }
val diagnosticsReporter = DiagnosticReporterFactory.createPendingReporter()
val moduleStructure = ModulesStructure(
project = environment.project,
mainModule = MainModule.SourceFiles(ktFiles),
compilerConfiguration = config,
dependencies = regularDependencies,
friendDependenciesPaths = friendDependencies
// Build KLIB:
runCompilerViaCLI(
listOf(
"-Xir-produce-klib-file",
"-ir-output-dir", klibFile.parentFile.absolutePath,
"-ir-output-name", moduleName
),
dependencies.toCompilerArgs(),
listOf("-language-version", "2.0").takeIf { compilerType.useFir },
kotlinSourceFilePaths
)
val outputStream = ByteArrayOutputStream()
val messageCollector = PrintingMessageCollector(PrintStream(outputStream), MessageRenderer.PLAIN_FULL_PATHS, true)
val analyzedOutput = compileModuleToAnalyzedFirWithPsi(
moduleStructure = moduleStructure,
ktFiles = ktFiles,
libraries = regularDependencies,
friendLibraries = friendDependencies,
diagnosticsReporter = diagnosticsReporter,
incrementalDataProvider = null,
lookupTracker = null
)
val fir2IrActualizedResult = transformFirToIr(moduleStructure, analyzedOutput.output, diagnosticsReporter)
if (analyzedOutput.reportCompilationErrors(moduleStructure, diagnosticsReporter, messageCollector)) {
val messages = outputStream.toByteArray().toString(Charset.forName("UTF-8"))
throw AssertionError("The following errors occurred compiling test:\n$messages")
}
serializeFirKlib(
moduleStructure = moduleStructure,
firOutputs = analyzedOutput.output,
fir2IrActualizedResult = fir2IrActualizedResult,
outputKlibPath = klibFile.absolutePath,
messageCollector = messageCollector,
diagnosticsReporter = diagnosticsReporter,
jsOutputName = moduleName
)
if (messageCollector.hasErrors()) {
val messages = outputStream.toByteArray().toString(Charset.forName("UTF-8"))
throw AssertionError("The following errors occurred serializing test klib:\n$messages")
}
}
private fun buildBinaryAndRun(mainModuleKlibFile: File, allDependencies: Dependencies) {
val configuration = createConfig(MAIN_MODULE_NAME)
configuration.setupPartialLinkageConfig(PartialLinkageConfig(PartialLinkageMode.ENABLE, PartialLinkageLogLevel.WARNING))
val compilationOutputs = when (compilerType) {
CompilerType.K1_NO_IC, CompilerType.K1_NO_IC_WITH_ES6, CompilerType.K2_NO_IC ->
buildBinaryNoIC(configuration, mainModuleKlibFile, allDependencies, compilerType.es6Mode)
CompilerType.K1_WITH_IC -> buildBinaryWithIC(configuration, mainModuleKlibFile, allDependencies)
// The modules in `Dependencies.regularDependencies` are already in topological order.
// It is important to pass the provided and the produced JS files to Node in exactly the same order.
val knownModulesInTopologicalOrder: List<ModuleDetails> = allDependencies.regularDependencies.map { dependency ->
ModuleDetails(name = dependency.moduleName, outputDir = dependency.libraryFile.parentFile)
}
val knownModuleNames: Set<ModuleName> = knownModulesInTopologicalOrder.mapTo(hashSetOf(), ModuleDetails::name)
val binariesDir = File(buildDir, BIN_DIR_NAME).also { it.mkdirs() }
val binariesDir: File = File(buildDir, BIN_DIR_NAME).also { it.mkdirs() }
// key = module name, value = list of produced JS files
val producedBinaries: Map<String, List<File>> = compilationOutputs
.writeAll(binariesDir, MAIN_MODULE_NAME, false, MAIN_MODULE_NAME, ModuleKind.PLAIN)
.filter { file -> file.extension == "js" }
.groupByTo(linkedMapOf()) { file -> file.nameWithoutExtension.toInnerName() }
runCompilerViaCLI(
listOf(
"-Xir-produce-js",
"-Xir-per-module",
"-module-kind", "plain",
"-Xinclude=${mainModuleKlibFile.absolutePath}",
"-ir-output-dir", binariesDir.absolutePath,
"-ir-output-name", MAIN_MODULE_NAME
),
listOf(
"-Xcache-directory",
buildDir.resolve("libs-cache").absolutePath
).takeIf { compilerType.useIc },
allDependencies.toCompilerArgs(),
listOf("-Xes-classes").takeIf { compilerType.es6Mode },
listOf(
"-Xpartial-linkage=enable",
"-Xpartial-linkage-loglevel=WARNING"
)
)
// key = module name, value = list of JS files out of test data
val providedBinaries: Map<String, List<File>> =
(allDependencies.regularDependencies.asSequence().map { it.libraryFile } + mainModuleKlibFile)
.mapNotNull { klibFile ->
val moduleName = if (klibFile.extension == "klib") klibFile.nameWithoutExtension else return@mapNotNull null
val outputDir = klibFile.parentFile
// All JS files produced during the compiler call.
val producedBinaries: Map<ModuleName, File> = binariesDir.walkTopDown()
.filter { binaryFile -> binaryFile.extension == "js" }
.associateBy { binaryFile -> binaryFile.guessModuleName() }
val providedJsFiles = outputDir.listFiles()?.filter { it.isFile && it.extension == "js" }.orEmpty()
if (providedJsFiles.isEmpty()) return@mapNotNull null
moduleName to providedJsFiles
}.toMap()
val unexpectedModuleNames = providedBinaries.keys - producedBinaries.keys
val unexpectedModuleNames = producedBinaries.keys - knownModuleNames
check(unexpectedModuleNames.isEmpty()) { "Unexpected module names: $unexpectedModuleNames" }
val allBinaries: List<File> = buildList {
producedBinaries.forEach { (moduleName, producedJsFiles) ->
this += providedBinaries[moduleName].orEmpty()
this += producedJsFiles
knownModulesInTopologicalOrder.forEach { moduleDetails ->
// A set of JS files directly out of test data (aka "external" JS files) to be used immediately in the application:
val providedBinaries: List<File> = moduleDetails.outputDir.listFiles()?.filter { it.extension == "js" }.orEmpty()
addAll(providedBinaries)
// A JS file produced by the compiler for the given module:
val producedBinary: File? = producedBinaries[moduleDetails.name]
producedBinary?.let(::add)
}
}
executeAndCheckBinaries(MAIN_MODULE_NAME, allBinaries)
}
private fun buildBinaryNoIC(
configuration: CompilerConfiguration,
mainModuleKlibFile: File,
allDependencies: Dependencies,
es6mode: Boolean
): CompilationOutputs {
val klib = MainModule.Klib(mainModuleKlibFile.path)
val moduleStructure = ModulesStructure(
environment.project,
klib,
configuration,
allDependencies.regularDependencies.map { it.libraryFile.path },
allDependencies.friendDependencies.map { it.libraryFile.path }
)
val ir = compile(
moduleStructure,
PhaseConfig(jsPhases),
IrFactoryImplForJsIC(WholeWorldStageController()),
exportedDeclarations = setOf(BOX_FUN_FQN),
granularity = JsGenerationGranularity.PER_MODULE,
es6mode = es6mode
)
val transformer = IrModuleToJsTransformer(
backendContext = ir.context,
mainArguments = emptyList()
)
val compiledResult = transformer.generateModule(
modules = ir.allModules,
modes = setOf(TranslationMode.PER_MODULE_DEV),
relativeRequirePath = false
)
return compiledResult.outputs[TranslationMode.PER_MODULE_DEV] ?: error("No compiler output")
private fun File.guessModuleName(): String {
return when {
extension != "js" -> error("Not a JS file: $this")
name.startsWith("kotlin-") && "stdlib" in name -> "stdlib"
else -> nameWithoutExtension.removePrefix("kotlin_")
}
}
private fun buildBinaryWithIC(
configuration: CompilerConfiguration,
mainModuleKlibFile: File,
allDependencies: Dependencies
): CompilationOutputs {
// TODO: what about friend dependencies?
val cacheUpdater = CacheUpdater(
mainModule = mainModuleKlibFile.absolutePath,
allModules = allDependencies.regularDependencies.map { it.libraryFile.path },
mainModuleFriends = emptyList(),
cacheDir = buildDir.resolve("libs-cache").absolutePath,
compilerConfiguration = configuration,
irFactory = { IrFactoryImplForJsIC(WholeWorldStageController()) },
mainArguments = null,
compilerInterfaceFactory = { mainModule, cfg ->
JsIrCompilerWithIC(mainModule, cfg, JsGenerationGranularity.PER_MODULE, PhaseConfig(jsPhases), setOf(BOX_FUN_FQN))
}
)
val icCaches = cacheUpdater.actualizeCaches()
val mainModuleName = icCaches.last().moduleExternalName
val jsExecutableProducer = JsExecutableProducer(
mainModuleName = mainModuleName,
moduleKind = configuration[JSConfigurationKeys.MODULE_KIND]!!,
sourceMapsInfo = SourceMapsInfo.from(configuration),
caches = icCaches,
relativeRequirePath = true
)
return jsExecutableProducer.buildExecutable(granularity = JsGenerationGranularity.PER_MODULE, outJsProgram = true).compilationOut
private fun Dependencies.toCompilerArgs(): List<String> = buildList {
if (regularDependencies.isNotEmpty()) {
this += "-libraries"
this += regularDependencies.joinToString(File.pathSeparator) { it.libraryFile.absolutePath }
}
if (friendDependencies.isNotEmpty()) {
this += "-Xfriend-modules=${friendDependencies.joinToString(File.pathSeparator) { it.libraryFile.absolutePath }}"
}
}
private val IrModuleFragment.exportName get() = "kotlin_${name.asStringStripSpecialMarkers()}"
private fun String.toInnerName() = if (startsWith("kotlin_")) substringAfter("kotlin_") else this
private fun runCompilerViaCLI(vararg compilerArgs: List<String?>?) {
val allCompilerArgs = compilerArgs.flatMap { args -> args.orEmpty().filterNotNull() }.toTypedArray()
private fun KotlinCoreEnvironment.createPsiFiles(sourceDir: File): List<KtFile> {
val psiManager = PsiManager.getInstance(project)
val fileSystem = VirtualFileManager.getInstance().getFileSystem(StandardFileSystems.FILE_PROTOCOL) as CoreLocalFileSystem
val compilerXmlOutput = ByteArrayOutputStream()
val exitCode = PrintStream(compilerXmlOutput).use { printStream ->
K2JsIrCompiler().execFullPathsInMessages(printStream, allCompilerArgs)
}
return sourceDir.walkTopDown().filter { file ->
file.isFile && file.extension == "kt"
}.flatMap { file ->
val virtualFile = fileSystem.findFileByIoFile(file) ?: error("VirtualFile for $file not found")
SingleRootFileViewProvider(psiManager, virtualFile).allFiles
}.filterIsInstance<KtFile>().toList()
if (exitCode != ExitCode.OK)
throw AssertionError(
buildString {
appendLine("Compiler failure.")
appendLine("Exit code = $exitCode.")
appendLine("Compiler messages:")
appendLine("==========")
appendLine(compilerXmlOutput.toString(Charsets.UTF_8.name()))
appendLine("==========")
}
)
}
private fun File.binJsFile(name: String): File = File(this, "$name.js")
private fun executeAndCheckBinaries(mainModuleName: String, dependencies: Collection<File>) {
val checker = V8IrJsTestChecker
val filePaths = dependencies.map { it.canonicalPath }
checker.check(filePaths, mainModuleName, null, BOX_FUN_FQN.asString(), "OK", withModuleSystem = false)
checker.check(filePaths, mainModuleName, null, BOX_FUN_FQN, "OK", withModuleSystem = false)
}
companion object {
private const val BIN_DIR_NAME = "_bins_js"
private val BOX_FUN_FQN = FqName("box")
private const val BOX_FUN_FQN = "box"
}
}
private typealias ModuleName = String
private class ModuleDetails(val name: ModuleName, val outputDir: File)