Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,8 @@ class BspTestHarness(workspaceRoot: Path, projectConfigs: Option[List[BspTestHar
buildCache = new BuildCache(bleep.model.BspServerConfig.default.maxCachedWorkspacesFor(Runtime.getRuntime.maxMemory()), harnessAnalysisCache),
analysisCache = harnessAnalysisCache,
daemonInfo = DaemonInfo.inProcess(bleep.model.BspServerConfig.default),
connId = 1
connId = 1,
configOverride = None
)

val buildPayload: Option[BspBuildData.Payload] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,4 +214,80 @@ class BuildStateReducerTest extends AnyFunSuite with Matchers {
state.runningSuites shouldBe empty
state.runningTests shouldBe empty
}

// ==========================================================================
// noOp — "did anything actually recompile"
// ==========================================================================

private def compiled(project: String, reason: bleep.bsp.protocol.CompileReason): Seq[BuildEvent] =
Seq(
BuildEvent.CompileStarted(cpn(project), ts),
BuildEvent.CompilationReason(cpn(project), reason, totalFiles = 1, invalidatedFiles = Nil, changedDependencies = Nil, ts + 1),
BuildEvent.CompileFinished(
cpn(project),
bleep.bsp.protocol.CompileStatus.Success,
durationMs = 5,
timestamp = ts + 2,
diagnostics = Nil,
skippedBecause = None
)
)

private def summaryOf(events: Seq[BuildEvent]): BuildSummary =
events.foldLeft(BuildState.empty)(BuildStateReducer.reduce).toSummary(durationMs = 0, wasCancelled = false)

test("noOp: every compile up to date") {
val summary = summaryOf(compiled("a", bleep.bsp.protocol.CompileReason.UpToDate) ++ compiled("b", bleep.bsp.protocol.CompileReason.UpToDate))
summary.upToDateProjects shouldBe List(cpn("a"), cpn("b"))
summary.noOp shouldBe true
}

test("noOp: one project recompiled is enough to make the run not a no-op") {
// The asymmetry is the point. A caller uses this to decide whether it may skip a deploy, so a single project that really compiled has to outweigh any
// number that did not.
val summary = summaryOf(compiled("a", bleep.bsp.protocol.CompileReason.UpToDate) ++ compiled("b", bleep.bsp.protocol.CompileReason.Incremental))
summary.upToDateProjects shouldBe List(cpn("a"))
summary.noOp shouldBe false
}

test("noOp: a run in which nothing compiled is not a no-op") {
// There was no compile to be a no-op about. Answering true here would tell a deploy script it may skip on the strength of a run that never looked.
summaryOf(Nil).noOp shouldBe false
}
// ==========================================================================
// linkedOutputs — where the link put things
// ==========================================================================

test("linkedOutputs: the linker's own file list is kept, main artifact first") {
// Kept rather than recomputed, because the directory layout under `link-output/` is bleep's and has been renamed before. A caller reconstructing it is
// guessing; this is the linker's own answer.
val summary = summaryOf(
Seq(
BuildEvent.LinkStarted(cpn("frontend"), bleep.bsp.protocol.LinkPlatformName.ScalaJs, ts),
BuildEvent.LinkSucceeded(
cpn("frontend"),
bleep.bsp.protocol.LinkPlatformName.ScalaJs,
durationMs = 12,
generatedFiles = List("/out/js/main.js", "/out/js/main.js.map"),
ts + 1
)
)
)

summary.linkedOutputs.map(_.project) shouldBe List(cpn("frontend"))
summary.linkedOutputs.head.mainArtifact shouldBe java.nio.file.Path.of("/out/js/main.js")
summary.linkedOutputs.head.files should have size 2
}

test("linkedOutputs: a failed link contributes nothing to report") {
val summary = summaryOf(
Seq(
BuildEvent.LinkStarted(cpn("frontend"), bleep.bsp.protocol.LinkPlatformName.ScalaJs, ts),
BuildEvent.LinkFailed(cpn("frontend"), bleep.bsp.protocol.LinkPlatformName.ScalaJs, durationMs = 3, error = "boom", ts + 1)
)
)

summary.linkedOutputs shouldBe empty
summary.linkFailures should have size 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -192,12 +192,9 @@ class CrossPlatformIntegrationTest extends AnyFunSuite with Matchers {
KotlinJs(
moduleKind = Some(KotlinJsModuleKind.ESModule),
moduleName = Some("mymodule"),
outputMode = Some(KotlinJsOutputMode.JsExecutable),
sourceMap = Some(true),
sourceMapPrefix = None,
sourceMapEmbedSources = None,
target = Some(KotlinJsTarget.Node),
developmentMode = Some(false),
generateDts = Some(true)
)
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ class KotlinJsIntegrationTest extends AnyFunSuite with Matchers {
val config = KotlinJsCompilerConfig.ForTests
config.kotlinVersion shouldBe bleep.model.Versions.Kotlin23
config.moduleKind shouldBe KotlinJsCompilerConfig.ModuleKind.CommonJS
config.outputMode shouldBe KotlinJsCompilerConfig.OutputMode.JsExecutable
config.sourceMap shouldBe true
config.developmentMode shouldBe true
config.generateDts shouldBe false
Expand All @@ -79,22 +78,12 @@ class KotlinJsIntegrationTest extends AnyFunSuite with Matchers {
KotlinJsCompilerConfig.ModuleKind.ESModule.name shouldBe "es"
}

test("KotlinJsCompilerConfig: output modes") {
KotlinJsCompilerConfig.OutputMode.Klib.name shouldBe "klib"
KotlinJsCompilerConfig.OutputMode.JsExecutable.name shouldBe "js"
}

test("KotlinJsCompilerConfig: source map embed sources") {
KotlinJsCompilerConfig.SourceMapEmbedSources.Never.name shouldBe "never"
KotlinJsCompilerConfig.SourceMapEmbedSources.Always.name shouldBe "always"
KotlinJsCompilerConfig.SourceMapEmbedSources.Inlining.name shouldBe "inlining"
}

test("KotlinJsCompilerConfig: targets") {
KotlinJsCompilerConfig.Target.Browser.name shouldBe "browser"
KotlinJsCompilerConfig.Target.Node.name shouldBe "nodejs"
}

// ============================================================================
// KotlinJsCompileResult Tests
// ============================================================================
Expand Down Expand Up @@ -176,8 +165,7 @@ class KotlinJsAdvancedIntegrationTest extends AnyFunSuite with Matchers with Pla
)

val config = KotlinJsCompilerConfig.ForTests.copy(
moduleName = "hello",
outputMode = KotlinJsCompilerConfig.OutputMode.JsExecutable
moduleName = "hello"
)

val errors = scala.collection.mutable.ListBuffer[CompilerError]()
Expand Down Expand Up @@ -258,8 +246,7 @@ class KotlinJsAdvancedIntegrationTest extends AnyFunSuite with Matchers with Pla
)

val config = KotlinJsCompilerConfig.ForTests.copy(
moduleName = "mylib",
outputMode = KotlinJsCompilerConfig.OutputMode.Klib
moduleName = "mylib"
)

val listener = new DiagnosticListener {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,10 @@ class LinkDagIntegrationTest extends AnyFunSuite with Matchers {
LinkPlatform.Jvm shouldBe a[LinkPlatform]
LinkPlatform.ScalaJs("1.16.0", "3.3.3", ScalaJsLinkConfig.Debug) shouldBe a[LinkPlatform.ScalaJs]
LinkPlatform.ScalaNative("0.5.6", "3.3.3", ScalaNativeLinkConfig.Debug) shouldBe a[LinkPlatform.ScalaNative]
LinkPlatform.KotlinJs("2.0.0", TaskDag.KotlinJsConfig(bleep.model.KotlinJsModuleKind.CommonJS, true, false, java.nio.file.Path.of("."))) shouldBe a[
LinkPlatform.KotlinJs(
"2.0.0",
TaskDag.KotlinJsConfig(bleep.model.KotlinJsModuleKind.CommonJS, None, true, None, bleep.model.KotlinJsSourceMapEmbedSources.Never, false, false)
) shouldBe a[
LinkPlatform.KotlinJs
]
LinkPlatform.KotlinNative("2.0.0", TaskDag.KotlinNativeConfig("linux-x64", true, false, false)) shouldBe a[LinkPlatform.KotlinNative]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,9 @@ class PlatformCancellationTest extends AnyFunSuite with Matchers {
kotlinVersion = "2.3.0",
moduleName = "test-module",
moduleKind = KotlinJsCompilerConfig.ModuleKind.CommonJS,
outputMode = KotlinJsCompilerConfig.OutputMode.JsExecutable,
sourceMap = false,
sourceMapPrefix = None,
sourceMapEmbedSources = KotlinJsCompilerConfig.SourceMapEmbedSources.Never,
target = KotlinJsCompilerConfig.Target.Node,
developmentMode = true,
generateDts = false,
additionalOptions = Seq.empty
Expand Down Expand Up @@ -111,11 +109,9 @@ class PlatformCancellationTest extends AnyFunSuite with Matchers {
kotlinVersion = "2.3.0",
moduleName = "test-module",
moduleKind = KotlinJsCompilerConfig.ModuleKind.CommonJS,
outputMode = KotlinJsCompilerConfig.OutputMode.JsExecutable,
sourceMap = false,
sourceMapPrefix = None,
sourceMapEmbedSources = KotlinJsCompilerConfig.SourceMapEmbedSources.Never,
target = KotlinJsCompilerConfig.Target.Node,
developmentMode = true,
generateDts = false,
additionalOptions = Seq.empty
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ class BleepStatusEndpointTest extends AnyFunSuite with Matchers {
buildCache = new BuildCache(4, analysisCache),
analysisCache = analysisCache,
daemonInfo = daemonInfo,
connId = 17
connId = 17,
configOverride = None
)

private val thread = new Thread(() => server.run(), "status-endpoint-test-server")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ class CopyStateEndpointTest extends AnyFunSuite with Matchers {
buildCache = new BuildCache(4, analysisCache),
analysisCache = analysisCache,
daemonInfo = daemonInfo,
connId = 17
connId = 17,
configOverride = None
)

private val thread = new Thread(() => server.run(), "copy-state-endpoint-test-server")
Expand Down
23 changes: 16 additions & 7 deletions bleep-bsp/src/scala/bleep/analysis/KotlinJsCompiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -413,17 +413,19 @@ object KotlinJsLinker {
klibs: Seq[Path],
outputDir: Path,
config: KotlinJsCompilerConfig,
projectKlibName: String,
diagnosticListener: DiagnosticListener,
cancellation: CancellationToken
): IO[bleep.bsp.Outcome.ThreadOutcome[KotlinJsLinkResult]] =
bleep.bsp.Outcome.runInFreshThread[KotlinJsLinkResult](name = "kotlin-js-linker", contextClassLoader = None, cancellation = cancellation) {
linkBlocking(klibs, outputDir, config, diagnosticListener, cancellation)
linkBlocking(klibs, outputDir, config, projectKlibName, diagnosticListener, cancellation)
}

private def linkBlocking(
klibs: Seq[Path],
outputDir: Path,
config: KotlinJsCompilerConfig,
projectKlibName: String,
diagnosticListener: DiagnosticListener,
cancellation: CancellationToken
): KotlinJsLinkResult = {
Expand All @@ -448,10 +450,14 @@ object KotlinJsLinker {

// For linking, we pass KLIBs via includes field (the main KLIB to link) and libraries (dependencies)
// The first KLIB is the main one to link, others are dependencies
// Which of these is the project's own KLIB, matched on the *project's* name rather than on `config.moduleName`.
//
// Those were the same string until `kotlin.js.moduleName` became a setting a build can change, and then they were not: the KLIB on disk is named after
// the project, so a project naming its module something else matched nothing, `includes` was never set, and the link failed with no diagnostic. One
// string was doing two jobs — naming the output, and identifying a file — and only one of them is the user's to choose.
val (mainKlib, depKlibs) = klibs.partition { p =>
// The main KLIB is typically the one matching the module name
p.getFileName.toString.contains(config.moduleName.replace("_", "-")) ||
p.getFileName.toString.contains(config.moduleName)
p.getFileName.toString.contains(projectKlibName.replace("_", "-")) ||
p.getFileName.toString.contains(projectKlibName)
}

// Set main KLIB via includes field (not freeArgs which is interpreted as sources)
Expand Down Expand Up @@ -550,9 +556,12 @@ object KotlinJsLinker {
return KotlinJsLinkResult(outputDir, None, exitCode)
}

// Find the JS output file
val jsFile = outputDir.resolve(s"${config.moduleName}.js")
val jsFileOpt = if (Files.exists(jsFile)) Some(jsFile) else None
// Find the JS output file.
//
// `.mjs` as well as `.js`, because Kotlin names its output after the module kind: `moduleKind = es` emits `<moduleName>.mjs`, every other kind emits
// `<moduleName>.js`. Looking only for `.js` meant an ES link that the compiler had completed successfully came back with no file, and the caller reported
// that as "Kotlin/JS linking failed" with no diagnostics — the compiler had nothing to complain about.
val jsFileOpt = List(".js", ".mjs").map(ext => outputDir.resolve(config.moduleName + ext)).find(Files.exists(_))

KotlinJsLinkResult(outputDir, jsFileOpt, exitCode)
} catch {
Expand Down
22 changes: 0 additions & 22 deletions bleep-bsp/src/scala/bleep/analysis/KotlinJsCompilerConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@ case class KotlinJsCompilerConfig(
kotlinVersion: String,
moduleKind: KotlinJsCompilerConfig.ModuleKind,
moduleName: String,
outputMode: KotlinJsCompilerConfig.OutputMode,
sourceMap: Boolean,
sourceMapPrefix: Option[String],
sourceMapEmbedSources: KotlinJsCompilerConfig.SourceMapEmbedSources,
target: KotlinJsCompilerConfig.Target,
developmentMode: Boolean,
generateDts: Boolean,
additionalOptions: Seq[String]
Expand All @@ -34,15 +32,6 @@ object KotlinJsCompilerConfig {
case object ESModule extends ModuleKind { val name = "es" }
}

/** Output mode. */
sealed trait OutputMode {
def name: String
}
object OutputMode {
case object Klib extends OutputMode { val name = "klib" }
case object JsExecutable extends OutputMode { val name = "js" }
}

/** Source map embedding. */
sealed trait SourceMapEmbedSources {
def name: String
Expand All @@ -53,15 +42,6 @@ object KotlinJsCompilerConfig {
case object Inlining extends SourceMapEmbedSources { val name = "inlining" }
}

/** Target environment. */
sealed trait Target {
def name: String
}
object Target {
case object Browser extends Target { val name = "browser" }
case object Node extends Target { val name = "nodejs" }
}

/** A ready-made config for tests, not a default anything reads at build time. Production builds this explicitly in `ProjectCompiler` from the project's own
* `kotlin.version`; nothing here is consulted. It was called `Default` and sat next to real defaults, which reads as "what a project gets when it says
* nothing" — it is not, and that misreading is easy to act on.
Expand All @@ -70,11 +50,9 @@ object KotlinJsCompilerConfig {
kotlinVersion = bleep.model.Versions.Kotlin23,
moduleKind = ModuleKind.CommonJS,
moduleName = "main",
outputMode = OutputMode.JsExecutable,
sourceMap = true,
sourceMapPrefix = None,
sourceMapEmbedSources = SourceMapEmbedSources.Never,
target = Target.Node,
developmentMode = true,
generateDts = false,
additionalOptions = Seq.empty
Expand Down
4 changes: 2 additions & 2 deletions bleep-bsp/src/scala/bleep/analysis/ProjectCompiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -571,13 +571,13 @@ object KotlinJsProjectCompiler extends ProjectCompiler {

val jsConfig = KotlinJsCompilerConfig(
kotlinVersion = kt.kotlinVersion,
// The compile phase produces a KLIB, not a program: module kind, source-map shape and `.d.ts` generation are decided at link time, where the project's
// `kotlin.js` settings are read. Nothing here is a user-facing choice.
moduleKind = KotlinJsCompilerConfig.ModuleKind.CommonJS,
moduleName = config.name.replace("-", "_"),
outputMode = KotlinJsCompilerConfig.OutputMode.JsExecutable,
sourceMap = true,
sourceMapPrefix = None,
sourceMapEmbedSources = KotlinJsCompilerConfig.SourceMapEmbedSources.Never,
target = KotlinJsCompilerConfig.Target.Node,
developmentMode = true,
generateDts = false,
additionalOptions = kt.kotlinOptions
Expand Down
4 changes: 3 additions & 1 deletion bleep-bsp/src/scala/bleep/bsp/BspServerDaemon.scala
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,9 @@ object BspServerDaemon {
buildCache = buildCache,
analysisCache = analysisCache,
daemonInfo = daemonInfo,
connId = connId
connId = connId,
// The real daemon re-reads the user config per request on purpose; see `configOverride`.
configOverride = None
)

// Run server message loop
Expand Down
Loading