`:commons` is on the CLI classpath, yet it declared Compose UI, Coil, Compose resources, markdown and desktop Compose as dependencies, dragging ~40 MB of UI/Skiko jars into every `amy` distribution. This moves every Compose-dependent file into a new KMP module, `:commonsUI`, that `api`-depends on `:commons`; `:commons` keeps only the Compose runtime (stability annotations + snapshot state) and lifecycle-viewmodel. Files keep their `com.vitorpamplona.amethyst.commons.*` packages, so the split is a build-graph boundary and no consumer import changed. 236 files were `git mv`'d (composables, icons, robohash, theme, Coil fetchers, the `@Composable` relay-client entry points, `composeResources`, and the tests that exercise them). Two headless files needed surgery instead of a move: `GalleryParser` lost a vestigial foundation `@OptIn`, and the `LocalPrivacyLockState`/`lockStateFor` CompositionLocal accessor moved out of `PrivacyLockState` into its own commonsUI file. The feed DAL under `ui/feeds` and `ui/note/ParentNote`+`ReplyContext` stay in `commons` because ViewModels depend on them. `amethyst`, `desktopApp`, `nappletHost` (NappletWebContract serves the shell from composeResources) and `benchmark` now depend on `:commonsUI`; `cli`, `geode` and `marmotBench` do not. commons' androidMain gains an explicit androidx.core KTX dep it previously got transitively through Compose UI. CI, crowdin, the icon-font tools and the escaping hook point at the new composeResources location; CLAUDE.md, commons/ARCHITECTURE.md, a new commonsUI/ARCHITECTURE.md, CONTRIBUTING, BUILDING and the affected skills document the boundary. A plan doc under commons/plans records the classification method and follow-ups. Verified: JVM compiles for commons, commonsUI, cli, desktopApp; Android debug compiles for nappletHost and amethyst; commons/commonsUI/cli JVM test suites; both verifyKmpPurity gates; the cli runtime classpath no longer resolves Compose UI, material3, Skiko or Coil. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N56KzPSYiN5edMRamvKEgD
4.5 KiB
Split commons into commons (headless) + commonsUI (Compose)
Date: 2026-09-12 Status: shipped on this branch
Why
cli (amy) depends on :commons. Until now :commons declared Compose
UI, Coil, Compose resources, markdown and (on JVM) compose.desktop.currentOs
as dependencies, so every CLI distribution dragged ~40 MB of Compose + Skiko
jars it never loads (create-release.yml had a 200 MB budget "until commons
is split into core + ui modules"; BUILDING.md flagged the same for the
Homebrew bundle). The UI / non-UI boundary documented in
commons/ARCHITECTURE.md §1 was a convention enforced by nothing.
What
A new KMP module :commonsUI (same targets and source-set layout as
:commons, plus skikoMain) that api-depends on :commons and owns every
Compose-dependent file. :commons keeps only the Compose runtime
(@Stable/@Immutable, snapshot state) and lifecycle-viewmodel; it no
longer applies the org.jetbrains.compose plugin, has no composeResources,
no Coil, no markdown, no highlights, no desktop Compose.
Files keep their packages. The split is a build-graph boundary, not a
package rename: 231 files were git mv'd from commons/src/… to
commonsUI/src/… and not a single import changed in amethyst,
desktopApp, nappletHost or cli. The generated Res class stays at
com.vitorpamplona.amethyst.commons.resources for the same reason.
Consumers: amethyst, desktopApp, nappletHost (for
NappletWebContract, which reads the shell/shim from composeResources) and
benchmark (robohash) gained project(":commonsUI"). cli, geode,
marmotBench are untouched and must stay that way.
Method (reproducible)
- Classify every
.ktundercommons/srcas UI if it importsandroidx.compose.{ui,foundation,material3,animation},org.jetbrains.compose.*,coil3,org.jetbrains.skia,androidx.lifecycle.compose,…commons.resources, or declares@Composable. - Build the intra-module reference graph (imports + same-package simple-name
hits) and verify no headless file references a UI file. Two real hits
surfaced and were fixed by surgery rather than by moving logic into the UI
module:
richtext/GalleryParsercarried a vestigial@OptIn(ExperimentalLayoutApi::class)— dropped; it is pure logic.privacylock/PrivacyLockStatedeclared theLocalPrivacyLockStateCompositionLocal +lockStateFor()next to the state machine — the two Compose members moved tocommonsUI/…/privacylock/LocalPrivacyLockState.kt(same package).
- Additionally move the rest of the UI-only packages (
ui/**,icons,hashtags,robohash,<feature>/ui) unless a headless file depends on the file. That rule keeps the feed DAL (ui/feeds/FeedFilter,AdditiveFeedFilter,ChangesFlowFilter,FeedContentState,RepostRenderability,AdditiveComplexFeedFilter…) andui/note/ParentNote+ReplyContextincommons(ViewModels use them). - Tests follow their subject; test fixtures shared with headless tests
(
ui/note/StubCache) stay incommons. - Check
expect/actualpairs never straddle the boundary (none did) and that nointernaldeclaration is used across it (none was).
Verified
:commons:compileKotlinJvm,:commonsUI:compileKotlinJvm,:cli:compileKotlin,:desktopApp:compileKotlin,:nappletHost:compileDebugKotlin,:amethyst:compileFdroidDebugKotlin.:commons:jvmTest,:commonsUI:jvmTest,:cli:test,:commons:verifyKmpPurity,:commonsUI:verifyKmpPurity.:cliruntime classpath no longer containsorg.jetbrains.compose.ui,foundation,material3,skikoorcoil.
iOS targets could not be linked in the Linux CI container; the workflow runs
:commonsUI:iosSimulatorArm64Test + :commonsUI:compileTestKotlinIosArm64
next to the :commons ones on macOS.
Follow-ups
- Tighten the CLI size budget in
create-release.ymlonce a release confirms the newamytarball size (target < 80 MB percli/plans/2026-04-21-cli-distribution.md). - Rename the feed DAL out of
ui.feeds(it is the oneui.*package that still lives incommons); a package rename that touches app imports. nip64Chess→nip64Chess/uifor the composables now incommonsUI(module split done, package rename pending).commonsstill applies the Compose compiler plugin on purpose (stability inference for its model classes as seen from the apps' composables). Revisit if aruntime-annotation-only setup proves sufficient.