From 45a0024fe4fd322fabcb06f89b65be6469c4cc5c Mon Sep 17 00:00:00 2001 From: Richie Caputo Date: Thu, 27 Aug 2026 12:15:23 -0400 Subject: [PATCH 1/3] Let an explicit specifier win over an unversioned latest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dependencyPairs compared specifiers by string equality, and a missing version resolves to "latest" — so one unversioned dep in any published library hard-conflicted with every consumer that pins a version, reachable through transitive classpath manifests rather than the consumer's own build file, escapable only via npmOverrides. "latest" is only the placeholder for an unversioned declaration, so any explicit specifier now wins over it. Two different explicit specifiers (including semantically overlapping ones like ^19 and ^19.0.0) still fail deterministically, and the conflict message no longer lists the irrelevant "latest". Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 4 +++- README.md | 2 +- docs/MIGRATING-0.3.md | 4 +++- millbun/src/mill/bun/BunToolchainModule.scala | 11 ++++++++-- .../test/src/mill/bun/BunToolchainTests.scala | 21 +++++++++++++++++++ 5 files changed, 37 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96fcc66..40c801f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Scala.js linking delegates to Mill's standard linker hooks; applications now choose `scalaJSVersion` explicitly. - Development dependencies are local tooling inputs and are no longer published transitively. - `bunPackageJsonExtras` rejects dependency fields now represented by typed settings. -- Missing dependency versions are represented as `latest` instead of an empty package.json value. +- Missing dependency versions are represented as `latest` instead of an empty package.json value, + and an explicit specifier wins over `latest` when the same package is declared both ways — + two different explicit specifiers still fail deterministically. - TypeScript `bunBundleFormat` is `Option[String]`, matching Scala.js; `None` lets `bun build` infer. - `unmanagedDeps` entries are staged into `vendor/` and declared as `file:./vendor/` dependencies, so local packages install under frozen lockfiles and locks stay portable. diff --git a/README.md b/README.md index f2ddb4e..aaa881f 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ The dependency model is shared across Scala.js and TypeScript: | `npmOverrides` | Explicit resolution for otherwise conflicting declarations | | `bunPackageJsonExtras` | Unmodeled fields such as `scripts`; typed dependency fields are rejected here | -The `bun"pkg@specifier"` interpolator is an optional compile-time validator for dependency strings. Unversioned dependencies resolve explicitly to `latest`; contradictory requirements fail unless selected by `npmOverrides`. +The `bun"pkg@specifier"` interpolator is an optional compile-time validator for dependency strings. Unversioned dependencies resolve explicitly to `latest`, and an explicit specifier wins over `latest` for the same package; contradictory explicit requirements fail unless selected by `npmOverrides`. ## Managed Bun diff --git a/docs/MIGRATING-0.3.md b/docs/MIGRATING-0.3.md index b6cb44d..ed9fac6 100644 --- a/docs/MIGRATING-0.3.md +++ b/docs/MIGRATING-0.3.md @@ -46,7 +46,9 @@ override def bunBundleFormat = Task { Some("esm") } 0.2 resolved the same package declared with different specifiers last-wins. 0.3 fails deterministically for **all** generated package.json files — direct declarations, module-graph -aggregation, and classpath manifests alike. Resolve with `npmOverrides`: +aggregation, and classpath manifests alike. Unversioned declarations are the one exception: they +resolve to `latest`, and an explicit specifier for the same package wins over `latest` instead +of conflicting. Resolve genuine conflicts with `npmOverrides`: ```scala override def npmOverrides = Task { Map("react" -> "^19.0.0") } diff --git a/millbun/src/mill/bun/BunToolchainModule.scala b/millbun/src/mill/bun/BunToolchainModule.scala index 6728a70..3ad83f4 100644 --- a/millbun/src/mill/bun/BunToolchainModule.scala +++ b/millbun/src/mill/bun/BunToolchainModule.scala @@ -315,12 +315,19 @@ object BunToolchainModule { )) parsed.groupBy(_.name).toSeq.sortBy(_._1).map { case (name, entries) => val specifiers = entries.map(_.specifier).distinct - val resolved = overrides.get(name).orElse(specifiers match { + // "latest" is only the placeholder for an unversioned declaration, so any explicit + // specifier wins over it: without this, one unversioned dep in a published library would + // hard-conflict with every consumer that pins a version. Two DIFFERENT explicit + // specifiers (even semantically overlapping ones like ^19 and ^19.0.0) still fail + // deterministically rather than resolving by declaration order. + val explicit = specifiers.filterNot(_ == "latest") + val candidates = if (explicit.nonEmpty) explicit else specifiers + val resolved = overrides.get(name).orElse(candidates match { case Seq(specifier) => Some(specifier) case _ => None }).getOrElse( throw new IllegalArgumentException( - s"Conflicting npm dependency '$name': ${specifiers.sorted.mkString(", ")}. " + + s"Conflicting npm dependency '$name': ${explicit.sorted.mkString(", ")}. " + "Declare npmOverrides to select one specifier." ) ) diff --git a/millbun/test/src/mill/bun/BunToolchainTests.scala b/millbun/test/src/mill/bun/BunToolchainTests.scala index df23cb5..3393669 100644 --- a/millbun/test/src/mill/bun/BunToolchainTests.scala +++ b/millbun/test/src/mill/bun/BunToolchainTests.scala @@ -261,6 +261,27 @@ object BunToolchainTests extends TestSuite: ) assert(pairs.map((name, version) => name -> version.str) == Seq("react" -> "19.1.1")) + test("an explicit specifier wins over an unversioned declaration"): + // "latest" is only the placeholder for a missing version. Without this yield, one + // unversioned dep in a published library conflicts with every consumer that pins. + val pairs = BunToolchainModule.dependencyPairs(Seq("react", "react@^19.0.0")) + assert(pairs.map((name, version) => name -> version.str) == Seq("react" -> "^19.0.0")) + + // Order-independent, and multiple latests collapse into the one explicit winner. + val reversed = BunToolchainModule.dependencyPairs(Seq("react@^19.0.0", "react", "react")) + assert(reversed.map((name, version) => name -> version.str) == Seq("react" -> "^19.0.0")) + + // Only-unversioned still resolves to latest. + val bare = BunToolchainModule.dependencyPairs(Seq("react", "react")) + assert(bare.map((name, version) => name -> version.str) == Seq("react" -> "latest")) + + // Two different explicit specifiers still conflict, with latest absent from the message. + val error = intercept[IllegalArgumentException]( + BunToolchainModule.dependencyPairs(Seq("react", "react@^18", "react@^19")) + ) + assert(error.getMessage.contains("Conflicting npm dependency 'react'")) + assert(!error.getMessage.contains("latest")) + test("malformed dependency declarations fail clearly"): Seq("", "react@", "@types", "@types/bun@").foreach: input => intercept[IllegalArgumentException](BunToolchainModule.splitDep(input)) From 6a24b661cd99b1017e4a4c441219913e781b5238 Mon Sep 17 00:00:00 2001 From: Richie Caputo Date: Thu, 27 Aug 2026 12:19:51 -0400 Subject: [PATCH 2/3] Add scalafmt with a conservative config and a CI format check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deliberately conservative: Scala 3 dialect, maxColumn 120, newlines.source = keep, and no brace/indentation conversion — the tree legitimately mixes both styles, and this config normalizes spacing and wrapping without re-styling it. The one-time reformat touched 10 files (+49/-26). CI checks formatting ahead of compilation on every platform so it cannot drift. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 3 +++ .scalafmt.conf | 10 ++++++++++ ...BunDependencyManifestIntegrationTests.scala | 6 ++++-- .../bun/BunTypeScriptIntegrationTests.scala | 3 ++- .../bun/BunWorkspaceIntegrationTests.scala | 4 +++- millbun/src/mill/bun/BunDep.scala | 2 +- millbun/src/mill/bun/BunToolchainModule.scala | 18 ++++++++++-------- millbun/src/mill/bun/BunWebSupport.scala | 5 ++++- millbun/src/mill/bun/BunWorkspaceModule.scala | 3 ++- .../bun/BunTypeScriptModule.scala | 14 +++++++++----- .../mill/scalajslib/bun/BunScalaJSModule.scala | 18 +++++++++++++----- .../mill/bun/BunVendoredNodeModulesTests.scala | 2 +- 12 files changed, 62 insertions(+), 26 deletions(-) create mode 100644 .scalafmt.conf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 950aa79..a5149ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,9 @@ jobs: with: bun-version: 1.4.0 + - name: Check formatting + run: $MILL --no-server mill.scalalib.scalafmt.ScalafmtModule/checkFormatAll __.sources + - name: Compile run: $MILL --no-server millbun.compile diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..212de3d --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,10 @@ +version = "3.8.3" +runner.dialect = scala3 + +# Deliberately conservative: the tree mixes brace and significant-indentation styles, and this +# config normalizes spacing and wrapping without converting between them. +maxColumn = 120 +rewrite.scala3.removeOptionalBraces = false +rewrite.scala3.insertEndMarkerMinLines = 0 +docstrings.style = keep +newlines.source = keep diff --git a/millbun/integration/src/mill/bun/BunDependencyManifestIntegrationTests.scala b/millbun/integration/src/mill/bun/BunDependencyManifestIntegrationTests.scala index b966b50..75a7645 100644 --- a/millbun/integration/src/mill/bun/BunDependencyManifestIntegrationTests.scala +++ b/millbun/integration/src/mill/bun/BunDependencyManifestIntegrationTests.scala @@ -60,7 +60,8 @@ object BunDependencyManifestIntegrationTests extends BunIntegrationSuite { val res = tester.eval("appLocal.bunInstall") assert(res.isSuccess) - val packageJson = ujson.read(os.read(tester.workspacePath / "out" / "appLocal" / "bunInstall.dest" / "package.json")) + val packageJson = + ujson.read(os.read(tester.workspacePath / "out" / "appLocal" / "bunInstall.dest" / "package.json")) assert(packageJson("optionalDependencies").obj("optional-local").str == "^1.0.0") } @@ -69,7 +70,8 @@ object BunDependencyManifestIntegrationTests extends BunIntegrationSuite { val res = tester.eval("appPublished.bunInstall") assert(res.isSuccess) - val packageJson = ujson.read(os.read(tester.workspacePath / "out" / "appPublished" / "bunInstall.dest" / "package.json")) + val packageJson = + ujson.read(os.read(tester.workspacePath / "out" / "appPublished" / "bunInstall.dest" / "package.json")) assert(!packageJson("devDependencies").obj.contains("dev-only")) assert(packageJson("optionalDependencies").obj("optional-published").str == "^3.0.0") assert(packageJson("peerDependencies").obj("peer-published").str == "^4.0.0") diff --git a/millbun/integration/src/mill/bun/BunTypeScriptIntegrationTests.scala b/millbun/integration/src/mill/bun/BunTypeScriptIntegrationTests.scala index a06b2ad..7407015 100644 --- a/millbun/integration/src/mill/bun/BunTypeScriptIntegrationTests.scala +++ b/millbun/integration/src/mill/bun/BunTypeScriptIntegrationTests.scala @@ -224,7 +224,8 @@ object BunTypeScriptIntegrationTests extends BunIntegrationSuite { // Test module should have is-odd in devDependencies (not dependencies) val testRes = tester.eval("app.test.bunInstall") assert(testRes.isSuccess) - val testPkg = ujson.read(os.read(tester.workspacePath / "out" / "app" / "test" / "bunInstall.dest" / "package.json")) + val testPkg = + ujson.read(os.read(tester.workspacePath / "out" / "app" / "test" / "bunInstall.dest" / "package.json")) assert(testPkg("devDependencies").obj.contains("is-odd")) assert(!testPkg("dependencies").obj.contains("is-odd")) assert(!testPkg("devDependencies").obj.contains("is-even")) diff --git a/millbun/integration/src/mill/bun/BunWorkspaceIntegrationTests.scala b/millbun/integration/src/mill/bun/BunWorkspaceIntegrationTests.scala index 023cce6..887b097 100644 --- a/millbun/integration/src/mill/bun/BunWorkspaceIntegrationTests.scala +++ b/millbun/integration/src/mill/bun/BunWorkspaceIntegrationTests.scala @@ -33,7 +33,9 @@ object BunWorkspaceIntegrationTests extends BunIntegrationSuite: // otherwise a rename satisfies Mill's duplicate guard while bun still sees collisions. assert(scalaJson("name").str == "scala-app-renamed") assert(scalaJson("dependencies").obj("shared-local").str == "file:./vendor/shared-local") - assert(os.exists(workspaceInstall / "packages" / "scala-app-renamed" / "vendor" / "shared-local" / "package.json")) + assert( + os.exists(workspaceInstall / "packages" / "scala-app-renamed" / "vendor" / "shared-local" / "package.json") + ) assert(!os.read(workspaceInstall / ".workspace-installed").contains("shared-local")) val scalaResult = tester.eval("scalaApp.bunInstall") diff --git a/millbun/src/mill/bun/BunDep.scala b/millbun/src/mill/bun/BunDep.scala index 450e194..587c4bf 100644 --- a/millbun/src/mill/bun/BunDep.scala +++ b/millbun/src/mill/bun/BunDep.scala @@ -29,7 +29,7 @@ object BunDep: */ def validate(dep: String): String = BunToolchainModule.parseDependency(dep) match - case Right(_) => dep + case Right(_) => dep case Left(message) => throw new IllegalArgumentException(s"Invalid bun dependency: $message") private object BunDepMacro: diff --git a/millbun/src/mill/bun/BunToolchainModule.scala b/millbun/src/mill/bun/BunToolchainModule.scala index 3ad83f4..b9f68e0 100644 --- a/millbun/src/mill/bun/BunToolchainModule.scala +++ b/millbun/src/mill/bun/BunToolchainModule.scala @@ -119,7 +119,7 @@ object BunToolchainModule { case name if name.contains("mac") || name.contains("darwin") => Right("darwin") case name if name.contains("linux") => Right("linux") case name if name.contains("windows") => Right("windows") - case other => Left(s"Unsupported operating system '$other'") + case other => Left(s"Unsupported operating system '$other'") } val archPart = architecture.toLowerCase match { case "aarch64" | "arm64" => Right("aarch64") @@ -233,7 +233,7 @@ object BunToolchainModule { ) cached catch - case _: java.nio.file.AtomicMoveNotSupportedException => publishViaCopy(staged, cached) + case _: java.nio.file.AtomicMoveNotSupportedException => publishViaCopy(staged, cached) case scala.util.control.NonFatal(_) if os.exists(cached) => // Lost the publish race. The path is keyed by the verified checksum, so the winner's // bytes are the right bytes. Windows reports this as a sharing violation rather than @@ -300,7 +300,7 @@ object BunToolchainModule { /** Parse a dependency for compatibility with the existing public helper. */ def splitDep(input: String): (String, ujson.Str) = parseDependency(input) match { - case Right(dep) => dep.name -> ujson.Str(dep.specifier) + case Right(dep) => dep.name -> ujson.Str(dep.specifier) case Left(message) => throw new IllegalArgumentException(message) } @@ -309,10 +309,12 @@ object BunToolchainModule { inputs: Seq[String], overrides: Map[String, String] = Map.empty ): Seq[(String, ujson.Str)] = { - val parsed = inputs.map(input => parseDependency(input).fold( - message => throw new IllegalArgumentException(message), - identity - )) + val parsed = inputs.map(input => + parseDependency(input).fold( + message => throw new IllegalArgumentException(message), + identity + ) + ) parsed.groupBy(_.name).toSeq.sortBy(_._1).map { case (name, entries) => val specifiers = entries.map(_.specifier).distinct // "latest" is only the placeholder for an unversioned declaration, so any explicit @@ -324,7 +326,7 @@ object BunToolchainModule { val candidates = if (explicit.nonEmpty) explicit else specifiers val resolved = overrides.get(name).orElse(candidates match { case Seq(specifier) => Some(specifier) - case _ => None + case _ => None }).getOrElse( throw new IllegalArgumentException( s"Conflicting npm dependency '$name': ${explicit.sorted.mkString(", ")}. " + diff --git a/millbun/src/mill/bun/BunWebSupport.scala b/millbun/src/mill/bun/BunWebSupport.scala index 0b779c1..4f90856 100644 --- a/millbun/src/mill/bun/BunWebSupport.scala +++ b/millbun/src/mill/bun/BunWebSupport.scala @@ -129,7 +129,10 @@ private[mill] object BunWebSupport: os.walk(source).foreach { path => val destination = target / path.relativeTo(source) if os.isDir(path) then os.makeDir.all(destination) - else if !os.exists(destination) || os.mtime(path) != os.mtime(destination) || os.size(path) != os.size(destination) then + else if !os.exists(destination) || os.mtime(path) != os.mtime(destination) || os.size(path) != os.size( + destination + ) + then os.copy.over(path, destination, createFolders = true) } else if !os.exists(target) || os.mtime(source) != os.mtime(target) || os.size(source) != os.size(target) then diff --git a/millbun/src/mill/bun/BunWorkspaceModule.scala b/millbun/src/mill/bun/BunWorkspaceModule.scala index f9eb152..6da8ecd 100644 --- a/millbun/src/mill/bun/BunWorkspaceModule.scala +++ b/millbun/src/mill/bun/BunWorkspaceModule.scala @@ -56,7 +56,8 @@ trait BunWorkspaceModule extends BunToolchainModule: /** Generated root and member package.json files before installation. */ def bunWorkspaceLayout: T[PathRef] = Task { val packages = resolvedPackages() - val duplicateNames = packages.groupBy(_._1).collect { case (name, entries) if entries.size > 1 => name }.toSeq.sorted + val duplicateNames = + packages.groupBy(_._1).collect { case (name, entries) if entries.size > 1 => name }.toSeq.sorted if duplicateNames.nonEmpty then Task.fail(s"Duplicate Bun workspace package names: ${duplicateNames.mkString(", ")}") diff --git a/millbun/src/mill/javascriptlib/bun/BunTypeScriptModule.scala b/millbun/src/mill/javascriptlib/bun/BunTypeScriptModule.scala index 2e33a8e..fbafe14 100644 --- a/millbun/src/mill/javascriptlib/bun/BunTypeScriptModule.scala +++ b/millbun/src/mill/javascriptlib/bun/BunTypeScriptModule.scala @@ -73,9 +73,9 @@ trait BunTypeScriptModule extends TypeScriptModule with BunToolchainModule with /** Ambient runtime types aligned to the configured Bun target. */ protected def ambientTypeDeps: T[Seq[String]] = Task { bunBundleTarget() match { - case "bun" => Seq(s"@types/bun@${bunTypesVersion()}") + case "bun" => Seq(s"@types/bun@${bunTypesVersion()}") case "node" => Seq(s"@types/node@${nodeTypesVersion()}") - case _ => Seq.empty + case _ => Seq.empty } } @@ -99,7 +99,8 @@ trait BunTypeScriptModule extends TypeScriptModule with BunToolchainModule with BunToolchainModule.dependencyPairs(transitiveNpmDeps(), overrides), transitiveUnmanagedDeps() )), - devDependencies = ujson.Obj.from(BunToolchainModule.dependencyPairs(transitiveNpmDevDeps() ++ tsDeps(), overrides)) + devDependencies = + ujson.Obj.from(BunToolchainModule.dependencyPairs(transitiveNpmDevDeps() ++ tsDeps(), overrides)) ).cleanJson.obj.toSeq ) @@ -108,7 +109,8 @@ trait BunTypeScriptModule extends TypeScriptModule with BunToolchainModule with if optional.nonEmpty then resolved("optionalDependencies") = ujson.Obj.from(optional) if peers.nonEmpty then resolved("peerDependencies") = ujson.Obj.from(peers) if overrides.nonEmpty then - resolved("overrides") = ujson.Obj.from(overrides.toSeq.sortBy(_._1).map((name, value) => name -> ujson.Str(value))) + resolved("overrides") = + ujson.Obj.from(overrides.toSeq.sortBy(_._1).map((name, value) => name -> ujson.Str(value))) BunToolchainModule.mergePackageJson(resolved, bunPackageJsonExtras()) } @@ -217,6 +219,7 @@ trait BunTypeScriptModule extends TypeScriptModule with BunToolchainModule with os.copy.over(generated, sourceLock, createFolders = true) PathRef(sourceLock) } + /** * Preserve Mill's compile sandbox preparation, but invoke TypeScript through * Bun instead of a Node-shebang script. @@ -369,7 +372,8 @@ trait BunTypeScriptModule extends TypeScriptModule with BunToolchainModule with */ def compileExecutables: T[Map[String, PathRef]] = Task { val targets = bunCompileTargets() - if (targets.isEmpty) Task.fail("bunCompileTargets is empty. Set targets like Seq(\"bun-linux-x64\", \"bun-darwin-arm64\").") + if (targets.isEmpty) + Task.fail("bunCompileTargets is empty. Set targets like Seq(\"bun-linux-x64\", \"bun-darwin-arm64\").") val compileDir = compile().path val buildDir = Task.dest / "workspace" diff --git a/millbun/src/mill/scalajslib/bun/BunScalaJSModule.scala b/millbun/src/mill/scalajslib/bun/BunScalaJSModule.scala index 0c03165..2add628 100644 --- a/millbun/src/mill/scalajslib/bun/BunScalaJSModule.scala +++ b/millbun/src/mill/scalajslib/bun/BunScalaJSModule.scala @@ -183,9 +183,11 @@ trait BunScalaJSModule extends ScalaJSModule with BunToolchainModule with BunPac @deprecated("Use transitiveNpmOptionalDeps", "0.3.0") def transitiveBunOptionalDeps: T[Seq[String]] = Task { - val moduleOptional = Task.traverse(recursiveInstallBunModuleDeps)(module => Task.Anon { - module.npmOptionalDeps() ++ module.bunOptionalDeps() - })().flatten + val moduleOptional = Task.traverse(recursiveInstallBunModuleDeps)(module => + Task.Anon { + module.npmOptionalDeps() ++ module.bunOptionalDeps() + } + )().flatten moduleOptional ++ classpathBunOptionalDeps() ++ npmOptionalDeps() ++ bunOptionalDeps() } @@ -349,7 +351,12 @@ trait BunScalaJSModule extends ScalaJSModule with BunToolchainModule with BunPac bunfigFiles() } - private def ensureLinkedWorkspace(report: Report, installDir: os.Path, lockfiles: Seq[String], bunConfigs: Seq[PathRef]): Unit = { + private def ensureLinkedWorkspace( + report: Report, + installDir: os.Path, + lockfiles: Seq[String], + bunConfigs: Seq[PathRef] + ): Unit = { val linkedDir = report.dest.path os.copy.over(installDir / "package.json", linkedDir / "package.json", createFolders = true) @@ -521,7 +528,8 @@ trait BunScalaJSModule extends ScalaJSModule with BunToolchainModule with BunPac */ def compileExecutables: T[Map[String, PathRef]] = Task { val targets = bunCompileTargets() - if (targets.isEmpty) Task.fail("bunCompileTargets is empty. Set targets like Seq(\"bun-linux-x64\", \"bun-darwin-arm64\").") + if (targets.isEmpty) + Task.fail("bunCompileTargets is empty. Set targets like Seq(\"bun-linux-x64\", \"bun-darwin-arm64\").") val linked = fullLinkJS() bunInstall() diff --git a/millbun/test/src/mill/bun/BunVendoredNodeModulesTests.scala b/millbun/test/src/mill/bun/BunVendoredNodeModulesTests.scala index 949afd3..f7cf028 100644 --- a/millbun/test/src/mill/bun/BunVendoredNodeModulesTests.scala +++ b/millbun/test/src/mill/bun/BunVendoredNodeModulesTests.scala @@ -169,7 +169,7 @@ object BunVendoredNodeModulesTests extends TestSuite { private def parentDirectories(path: String): Seq[String] = path.split('/').dropRight(1).scanLeft("") { - case ("", segment) => s"$segment/" + case ("", segment) => s"$segment/" case (acc, segment) => s"$acc$segment/" }.drop(1) } From 28331827e8f9c7355604807f18678e09bc6f6da7 Mon Sep 17 00:00:00 2001 From: Richie Caputo Date: Thu, 27 Aug 2026 12:25:46 -0400 Subject: [PATCH 3/3] Pin checkout line endings so the format check is platform-independent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub's Windows runners default to autocrlf=true, which rewrote every Scala source to CRLF at checkout — scalafmt then reported all 28 files misformatted. .gitattributes now checks sources out as LF on every platform; batch files stay CRLF because cmd.exe mis-parses some constructs without it (their blobs are still stored normalized, which is the one renormalization in this commit). Co-Authored-By: Claude Fable 5 --- .gitattributes | 7 +++++++ .../resources/typescript-env/bun-proxy.cmd | 18 +++++++++--------- 2 files changed, 16 insertions(+), 9 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..db500b7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +# Deterministic working trees on every platform: sources check out as LF everywhere, so the +# scalafmt CI check compares identical bytes on Windows runners (whose git defaults to +# autocrlf=true and would otherwise rewrite all 28 Scala sources to CRLF at checkout). +* text=auto eol=lf +# cmd.exe mis-parses some batch constructs without CRLF, and these only ever run on Windows. +*.bat text eol=crlf +*.cmd text eol=crlf diff --git a/millbun/integration/resources/typescript-env/bun-proxy.cmd b/millbun/integration/resources/typescript-env/bun-proxy.cmd index 3e105e3..0f70b59 100644 --- a/millbun/integration/resources/typescript-env/bun-proxy.cmd +++ b/millbun/integration/resources/typescript-env/bun-proxy.cmd @@ -1,9 +1,9 @@ -@echo off -setlocal -set "FIRST=%~1" -if "%FIRST%"=="" set "FIRST=unknown" -set "MARKER=%BUN_PROXY_MARKER%" -if "%MARKER%"=="" set "MARKER=missing" ->> "%CD%\.bun-env-log" echo %FIRST%:%MARKER% -bun %* -exit /b %ERRORLEVEL% +@echo off +setlocal +set "FIRST=%~1" +if "%FIRST%"=="" set "FIRST=unknown" +set "MARKER=%BUN_PROXY_MARKER%" +if "%MARKER%"=="" set "MARKER=missing" +>> "%CD%\.bun-env-log" echo %FIRST%:%MARKER% +bun %* +exit /b %ERRORLEVEL%