feature: Add minimal sbt 2.x crossproject plugin (sbt-uni-crossproject)#623
Conversation
portable-scala/sbt-crossproject has not been ported to sbt 2.x, which blocks uni's eventual sbt 2.x migration. Uni only uses the CrossType.Pure layout and a small slice of the crossProject API, so re-implement just that surface as a uni-owned plugin rather than wait for an upstream port. Single plugin (not upstream's three artifacts) depending on sbt-scalajs and sbt-scala-native, defining all three platforms. The crossProject macro replicates sbt 2.x's own KeyMacro.definingValName to derive ids from the enclosing val name. Validated by a scripted test that cross-builds JVM/JS/Native with shared, per-platform, and scala-3 cross-version sources plus a `core % Test` dependency. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces sbt-uni-crossproject, a minimal, uni-owned sbt 2.x re-implementation of portable-scala/sbt-crossproject supporting the CrossType.Pure layout. It includes the core platform definitions, a macro to derive project IDs from enclosing val names, and builder conversions, along with a scripted test suite for validation. The review feedback suggests minor code simplifications in CrossProject.scala, specifically simplifying the ClasspathDependency reference and removing an unnecessary vararg splice when applying settings.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| platform -> | ||
| (ClasspathDep | ||
| .ClasspathDependency(LocalProject(project.id), dep.configuration): ClasspathDep[ | ||
| ProjectReference | ||
| ]) |
There was a problem hiding this comment.
The qualification ClasspathDep.ClasspathDependency is redundant and can be simplified. Since ClasspathDependency is a standard type alias and constructor helper in the sbt package (which is imported via import sbt.*), you can reference it directly. This also removes the need for the explicit type annotation : ClasspathDep[ProjectReference] as the compiler can infer it automatically.
platform -> ClasspathDependency(LocalProject(project.id), dep.configuration)| val projectID = id + platform.sbtSuffix | ||
| val project = platform.enable( | ||
| Project(projectID, _crossType.platformDir(base, platform)) | ||
| .settings((name := id) +: shared*) |
There was a problem hiding this comment.
…roject 2.0.1 is the latest final release; RC10 was carried over from the sibling plugin builds. publishLocal + scripted verified on 2.0.1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add sbt-scalafmt (2.6.1) to the build, format the sources to the mirrored .scalafmt.conf style, and run scalafmtCheckAll in CI before scripted — keeping this build consistent with sbt-uni and sbt-uni-playwright. The crossProject macro still expands and the scripted test still passes after formatting. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Why The `sbt-uni` and `sbt-uni-playwright` sbt 2.x plugin builds were pinned to `2.0.0-RC10`. **sbt 2.0.1** (final) is now available, so move them to it. (Follow-up to #623, which uses 2.0.1 for the new `sbt-uni-crossproject` plugin.) ## What - Bump `sbt.version` to `2.0.1` in all seven `build.properties` across both builds (plugin metabuilds + their `sbt-test` projects). - Refresh the now-stale `sbt-scalafmt` note in `sbt-uni-playwright/project/plugin.sbt`: its "needs RC11+" blocker no longer applies on 2.0.1, so re-adding scalafmt is now possible (left as a follow-up to keep this change focused). ## Validation Both builds compile on 2.0.1: - `sbt-uni-playwright` (`uni-jsenv-playwright` + plugin) — clean. - `sbt-uni` — compiles against a locally published `uni`; only the two pre-existing `thisProjectRef`-excluded-from-cache lint warnings remain (not introduced by this bump). The full scripted suites for both run in CI (`test_sbt_plugin`, `test_sbt_uni_playwright`). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## What Adds a **Build Tooling** reference page for `sbt-uni-crossproject` (merged in #623), mirroring the `sbt-uni-playwright` page, and links it in both duplicated sidebars in `config.mts`. Covers: installation (with `__UNI_VERSION__`), the `crossProject(...)` usage and val-name → `coreJVM`/`coreJS`/`coreNative` derivation, the `CrossType.Pure` directory layout, the `%%`-on-sbt-2.x note, and a supported-API table — all verified against the merged source. ## Validation `pnpm docs:build` succeeds (no dead links). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Why The main build ran on **sbt 1.x**. The blockers are now gone: - `sbt-scalajs-crossproject` + `sbt-scala-native-crossproject` → **`sbt-uni-crossproject`** (#623) - third-party `scala-js-env-playwright` → **`uni-jsenv-playwright`** (#613) - `sbt-revolver` (unused in the build, no sbt 2.x release) → **`sbt-uni`**'s `uniRestart`/`uniStop`/`uniStatus` ## What - `project/build.properties` → **sbt 2.0.1**; metabuild plugins swapped (see table in the ADR). Dropped unused `sbt-buildinfo` and the now-built-in `addDependencyTreePlugin`; bumped `sbt-pack` 0.23 → 1.0.0. - Because `sbt-uni-crossproject` reproduces the `crossProject` / `CrossType.Pure` API, the project definitions are **unchanged**. Only sbt-2.x source adjustments: - **root → `uni-root`** — sbt 2.x derives output dirs from the project *name*, which collided with the `uni` library project (*"Overlapping output directories"*). - **`%%%` → `%%`** everywhere; the platform-less `scalajs-test-interface_2.13` named directly with a single `%` (in a JS project, any cross-version injects the unwanted `_sjs1`). - **`Test / jsEnv` wrapped in `Def.uncached(...)`** (JSEnv isn't serializable; sbt 2.x caches settings). - `import scala.language.implicitConversions` for `core.jvm` used as `ProjectReference`; `xs: _*` → `xs*`; infix `classifier` → `.classifier(...)`. - Docs: CLAUDE.md + the book's cross-build install snippet updated to the sbt 2.x / `sbt-uni-crossproject` approach. New ADR `adr/2026-06-30-sbt2-main-build-migration.md`. - **CI needs no change** — the repo's `./sbt` runner reads `build.properties` (→ 2.0.1) and `code_format` uses the kept `sbt-scalafmt`. ## Validation (all on sbt 2.0.1, run locally) | Platform | Result | |---|---| | JVM (`projectJVM/test`) | 1655 passed, 4 pending · +39 netty/book | | JS / NodeJSEnv | uniJS ~1420 · testJS 71 passed | | JS / Playwright Chromium (`domTest`) | 374 passed | | Native (`projectNative/test`) | uniNative 1417 (+9 pending) · testNative 71 passed | `scalafmtCheckAll` and `pnpm docs:build` pass. (`uni-core` reports 0 tests on every platform — it has no `src/test`, not a regression.) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Why
portable-scala/sbt-crossproject(and itssbt-scalajs-crossproject/sbt-scala-native-crossprojectcompanions) have not been ported to sbt 2.x, which blocks uni's eventual sbt 2.x migration. Uni only ever uses theCrossType.Purelayout and a small slice of thecrossProjectAPI, so this re-implements just that surface as a uni-owned plugin rather than waiting for an upstream port.What
A new standalone sbt 2.x build
sbt-uni-crossproject/, modelled onsbt-uni/sbt-uni-playwright, publishing one pluginorg.wvlet.uni:sbt-uni-crossproject:crossProject(JVMPlatform, JSPlatform, NativePlatform)with.crossType(CrossType.Pure),.in,.settings,.jvmSettings/.jsSettings/.nativeSettings,.configurevariants,.enablePlugins,.dependsOn(incl.% Test),.aggregate, and.jvm/.js/.nativeaccessors.sbt-scalajs1.22.0 andsbt-scala-native0.5.12 (both published for sbt 2.x as_sbt2_3), defining all three platforms.crossProjectmacro derives the id from the enclosingvalname by replicating sbt 2.x's ownKeyMacro.definingValName, soval core = ...yieldscoreJVM/coreJS/coreNative.See
adr/2026-06-30-sbt-uni-crossproject.mdfor the design and the non-obvious traps (the val-name macro, whynew CrossProject(...)is required internally, thegiven Conversion[Builder, CrossProject]build trigger, load-timebaseDirectoryresolution for shared sources).Validation
scripted crossproject/basiccross-builds aCrossType.Purecore+ dependentappacross all three platforms: compiles JVM/JS/Native (Native to NIR only — no clang/Node needed), compiles sharedTestsources, assertsjvmSettings/jsSettings/nativeSettingsroute only to their own platform, and runs the JVM app whose runtime assertion proves the shared, per-platform, andscala-3cross-version sources all resolve. Wired into CI astest_sbt_uni_crossproject; released byrelease-sbt-uni-crossproject.yml.Note: the main uni build stays on sbt 1.x with upstream sbt-crossproject for now — this plugin unblocks the migration but does not perform it.
🤖 Generated with Claude Code