Repository: cashapp/redwood
Pull request #759 (2023-01-30 07:23:38)

Update to Kotlin 1.8.
Updates Redwood to Kotlin 1.8. Resolves: https://github.com/cashapp/redwood/issues/701

This works around https://github.com/Kotlin/kotlinx.serialization/issues/1454 by only running either the redwood or zipline plugin in each module (not both).


Patch:
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index e6960b6a0..1e21ea914 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -22,14 +22,9 @@ jobs:
           distribution: 'zulu'
           java-version: 19
 
-      # For cinterop issues. Remove with Kotlin 1.8.
-      - uses: maxim-lobanov/setup-xcode@v1
-        with:
-          xcode-version: '13.4.1'
-
       - uses: gradle/gradle-build-action@v2
 
-      - run: ./gradlew build dokkaHtmlMultiModule --parallel -PredwoodNoSamples
+      - run: ./gradlew kotlinUpgradeYarnLock build dokkaHtmlMultiModule --parallel -PredwoodNoSamples
 
       # If we're on the integration branch, save the site to deploy from the publish job.
       - uses: actions/upload-artifact@v3
@@ -62,11 +57,6 @@ jobs:
           distribution: 'zulu'
           java-version: 19
 
-      # For cinterop issues. Remove with Kotlin 1.8.
-      - uses: maxim-lobanov/setup-xcode@v1
-        with:
-          xcode-version: '13.4.1'
-
       - uses: gradle/gradle-build-action@v2
 
       - run: ./gradlew -p samples/counter build
@@ -91,11 +81,6 @@ jobs:
           distribution: 'zulu'
           java-version: 19
 
-      # For cinterop issues. Remove with Kotlin 1.8.
-      - uses: maxim-lobanov/setup-xcode@v1
-        with:
-          xcode-version: '13.4.1'
-
       - uses: gradle/gradle-build-action@v2
 
       - run: ./gradlew -p samples/emoji-search build
@@ -127,11 +112,6 @@ jobs:
           distribution: 'zulu'
           java-version: 19
 
-      # For cinterop issues. Remove with Kotlin 1.8.
-      - uses: maxim-lobanov/setup-xcode@v1
-        with:
-          xcode-version: '13.4.1'
-
       - uses: gradle/gradle-build-action@v2
 
       - run: ./gradlew publish
diff --git a/build-support/src/main/kotlin/app/cash/redwood/buildsupport/ComposePlugin.kt b/build-support/src/main/kotlin/app/cash/redwood/buildsupport/ComposePlugin.kt
index 90b5cc3d6..f8f4b88cd 100644
--- a/build-support/src/main/kotlin/app/cash/redwood/buildsupport/ComposePlugin.kt
+++ b/build-support/src/main/kotlin/app/cash/redwood/buildsupport/ComposePlugin.kt
@@ -33,9 +33,11 @@ class ComposePlugin : KotlinCompilerPluginSupportPlugin {
 
   override fun getCompilerPluginId() = "app.cash.redwood.tools.compose"
 
-  override fun getPluginArtifact(): SubpluginArtifact {
-    return SubpluginArtifact("org.jetbrains.compose.compiler", "compiler", jbComposeCompiler)
-  }
+  override fun getPluginArtifact(): SubpluginArtifact = SubpluginArtifact(
+    "org.jetbrains.compose.compiler",
+    "compiler",
+    jbComposeCompiler,
+  )
 
   override fun applyToCompilation(
     kotlinCompilation: KotlinCompilation<*>
diff --git a/build.gradle b/build.gradle
index 6618ea3ca..26e1cbb17 100644
--- a/build.gradle
+++ b/build.gradle
@@ -126,6 +126,9 @@ allprojects {
             it.linkTask.enabled = false
           }
         }
+        binaries.withType(org.jetbrains.kotlin.gradle.plugin.mpp.Framework) {
+          linkerOpts += "-lsqlite3"
+        }
       }
     }
     tasks.withType(org.jetbrains.kotlin.gradle.tasks.FatFrameworkTask).configureEach {
diff --git a/gradle.properties b/gradle.properties
index 7d231a448..b1e6a5d69 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,5 @@
 kotlin.js.compiler=ir
+kotlin.mpp.androidSourceSetLayoutVersion=2
 kotlin.native.ignoreDisabledTargets=true
 kotlin.mpp.androidGradlePluginCompatibility.nowarn=true
 kotlin.mpp.stability.nowarn=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 0af9c4592..fdb077abf 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,13 +1,13 @@
 [versions]
-kotlin = "1.7.20"
+kotlin = "1.8.0"
 kotlinx-coroutines = "1.6.4"
 kotlinx-serialization = "1.4.1"
-androidxComposeCompiler = "1.3.2"
+androidxComposeCompiler = "1.4.0"
 androidx-activity = "1.3.1"
-jbCompose = "1.2.2"
-jbComposeCompiler = "1.3.2.2"
+jbCompose = "1.3.0"
+jbComposeCompiler = "1.4.0"
 lint = "30.4.1"
-zipline = "0.9.15"
+zipline = "0.9.16"
 coil = "2.2.2"
 
 [libraries]
@@ -57,7 +57,7 @@ junit = { module = "junit:junit", version = "4.13.2" }
 testParameterInjector = { module = "com.google.testparameterinjector:test-parameter-injector", version = "1.10" }
 truth = { module = "com.google.truth:truth", version = "1.1.3" }
 robolectric = { module = "org.robolectric:robolectric", version = "4.9.2" }
-okio = { module = "com.squareup.okio:okio", version = "3.2.0" }
+okio = { module = "com.squareup.okio:okio", version = "3.3.0" }
 okHttp = { module = "com.squareup.okhttp3:okhttp", version = "4.10.0" }
 zipline = { module = "app.cash.zipline:zipline", version.ref = "zipline" }
 zipline-gradlePlugin = { module = "app.cash.zipline:zipline-gradle-plugin", version.ref = "zipline" }
diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock
index 2457a24f2..fc5238628 100644
--- a/kotlin-js-store/yarn.lock
+++ b/kotlin-js-store/yarn.lock
@@ -98,9 +98,11 @@
   integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==
 
 "@types/cors@^2.8.12":
-  version "2.8.12"
-  resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080"
-  integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==
+  version "2.8.13"
+  resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.13.tgz#b8ade22ba455a1b8cb3b5d3f35910fd204f84f94"
+  integrity sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==
+  dependencies:
+    "@types/node" "*"
 
 "@types/eslint-scope@^3.7.3":
   version "3.7.4"
@@ -128,22 +130,22 @@
   resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40"
   integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==
 
-"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18":
-  version "4.17.31"
-  resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz#a1139efeab4e7323834bb0226e62ac019f474b2f"
-  integrity sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==
+"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.31":
+  version "4.17.33"
+  resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz#de35d30a9d637dc1450ad18dd583d75d5733d543"
+  integrity sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==
   dependencies:
     "@types/node" "*"
     "@types/qs" "*"
     "@types/range-parser" "*"
 
 "@types/express@*", "@types/express@^4.17.13":
-  version "4.17.14"
-  resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.14.tgz#143ea0557249bc1b3b54f15db4c81c3d4eb3569c"
-  integrity sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==
+  version "4.17.16"
+  resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.16.tgz#986caf0b4b850611254505355daa24e1b8323de8"
+  integrity sha512-LkKpqRZ7zqXJuvoELakaFYuETHjZkSol8EV6cNnyishutDBCCdv6+dsKPbKkCcIk57qRphOLY5sEgClw1bO3gA==
   dependencies:
     "@types/body-parser" "*"
-    "@types/express-serve-static-core" "^4.17.18"
+    "@types/express-serve-static-core" "^4.17.31"
     "@types/qs" "*"
     "@types/serve-static" "*"
 
@@ -165,9 +167,9 @@
   integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==
 
 "@types/node@*", "@types/node@>=10.0.0":
-  version "18.11.9"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4"
-  integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==
+  version "18.11.18"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f"
+  integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==
 
 "@types/qs@*":
   version "6.9.7"
@@ -207,9 +209,9 @@
     "@types/node" "*"
 
 "@types/ws@^8.5.1":
-  version "8.5.3"
-  resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d"
-  integrity sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==
+  version "8.5.4"
+  resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.4.tgz#bb10e36116d6e570dd943735f86c933c1587b8a5"
+  integrity sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==
   dependencies:
     "@types/node" "*"
 
@@ -384,10 +386,10 @@ acorn-import-assertions@^1.7.6:
   resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9"
   integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==
 
-acorn@^8.4.1, acorn@^8.5.0:
-  version "8.8.1"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73"
-  integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==
+acorn@^8.5.0, acorn@^8.7.1:
+  version "8.8.2"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
+  integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
 
 ajv-formats@^2.1.1:
   version "2.1.1"
@@ -419,9 +421,9 @@ ajv@^6.12.5:
     uri-js "^4.2.2"
 
 ajv@^8.0.0, ajv@^8.8.0:
-  version "8.11.2"
-  resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.2.tgz#aecb20b50607acf2569b6382167b65a96008bb78"
-  integrity sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==
+  version "8.12.0"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1"
+  integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==
   dependencies:
     fast-deep-equal "^3.1.1"
     json-schema-traverse "^1.0.0"
@@ -512,9 +514,9 @@ body-parser@1.20.1, body-parser@^1.19.0:
     unpipe "1.0.0"
 
 bonjour-service@^1.0.11:
-  version "1.0.14"
-  resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.0.14.tgz#c346f5bc84e87802d08f8d5a60b93f758e514ee7"
-  integrity sha512-HIMbgLnk1Vqvs6B4Wq5ep7mxvj9sGz5d1JJyDNSGNIdA/w2MCz6GTjWTdjqOJV1bEPj+6IkxDvWNFKEBxNt4kQ==
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.1.0.tgz#424170268d68af26ff83a5c640b95def01803a13"
+  integrity sha512-LVRinRB3k1/K0XzZ2p58COnWvkQknIY6sf0zF2rpErvcJXpMBttEPQSxK+HEXSS9VmpZlDoDnQWv8ftJT20B0Q==
   dependencies:
     array-flatten "^2.1.2"
     dns-equal "^1.0.0"
@@ -587,9 +589,9 @@ camelcase@^6.0.0:
   integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
 
 caniuse-lite@^1.0.30001400:
-  version "1.0.30001435"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001435.tgz#502c93dbd2f493bee73a408fe98e98fb1dad10b2"
-  integrity sha512-kdCkUTjR+v4YAJelyiDTqiu82BDr4W4CP5sgTA0ZBmqn30XfS2ZghPLMowik9TPhS+psWJiUNxsqLyurDbmutA==
+  version "1.0.30001447"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001447.tgz#ef1f39ae38d839d7176713735a8e467a0a2523bd"
+  integrity sha512-bdKU1BQDPeEXe9A39xJnGtY0uRq/z5osrnXUw0TcK+EYno45Y+U7QU9HhHEyzvMDffpYadFXi3idnSNkcwLkTw==
 
 chalk@^4.1.0:
   version "4.1.2"
@@ -689,10 +691,10 @@ concat-map@0.0.1:
   resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
   integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
 
-connect-history-api-fallback@^1.6.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc"
-  integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==
+connect-history-api-fallback@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8"
+  integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==
 
 connect@^3.7.0:
   version "3.7.0"
@@ -867,9 +869,9 @@ encodeurl@~1.0.2:
   integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
 
 engine.io-parser@~5.0.3:
-  version "5.0.4"
-  resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.4.tgz#0b13f704fa9271b3ec4f33112410d8f3f41d0fc0"
-  integrity sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==
+  version "5.0.6"
+  resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.6.tgz#7811244af173e157295dec9b2718dfe42a64ef45"
+  integrity sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==
 
 engine.io@~6.2.1:
   version "6.2.1"
@@ -887,7 +889,7 @@ engine.io@~6.2.1:
     engine.io-parser "~5.0.3"
     ws "~8.2.3"
 
-enhanced-resolve@^5.9.3:
+enhanced-resolve@^5.10.0:
   version "5.12.0"
   resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634"
   integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==
@@ -1158,9 +1160,9 @@ get-caller-file@^2.0.5:
   integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
 
 get-intrinsic@^1.0.2:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385"
-  integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f"
+  integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==
   dependencies:
     function-bind "^1.1.1"
     has "^1.0.3"
@@ -1612,9 +1614,9 @@ media-typer@0.3.0:
   integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
 
 memfs@^3.4.3:
-  version "3.4.12"
-  resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.4.12.tgz#d00f8ad8dab132dc277c659dc85bfd14b07d03bd"
-  integrity sha512-BcjuQn6vfqP+k100e0E9m61Hyqa//Brp+I3f0OBmN0ATHlFA8vx3Lt8z57R3u2bPqe3WGDBC+nF72fTH7isyEw==
+  version "3.4.13"
+  resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.4.13.tgz#248a8bd239b3c240175cd5ec548de5227fc4f345"
+  integrity sha512-omTM41g3Skpvx5dSYeZIbXKcXoAVc/AoMNwn9TKx++L/gaen/+4TTttmu8ZSch5vfVJ8uJvGbroTsIlslRg6lg==
   dependencies:
     fs-monkey "^1.0.3"
 
@@ -1771,9 +1773,9 @@ node-forge@^1:
   integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==
 
 node-releases@^2.0.6:
-  version "2.0.6"
-  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503"
-  integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==
+  version "2.0.8"
+  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.8.tgz#0f349cdc8fcfa39a92ac0be9bc48b7706292b9ae"
+  integrity sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==
 
 normalize-path@^3.0.0, normalize-path@~3.0.0:
   version "3.0.0"
@@ -1793,9 +1795,9 @@ object-assign@^4:
   integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
 
 object-inspect@^1.9.0:
-  version "1.12.2"
-  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea"
-  integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==
+  version "1.12.3"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9"
+  integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==
 
 obuf@^1.0.0, obuf@^1.1.2:
   version "1.1.2"
@@ -1946,9 +1948,9 @@ proxy-addr@~2.0.7:
     ipaddr.js "1.9.1"
 
 punycode@^2.1.0:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
-  integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
+  integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
 
 qjobs@^1.2.0:
   version "1.2.0"
@@ -2138,13 +2140,20 @@ send@0.18.0:
     range-parser "~1.2.1"
     statuses "2.0.1"
 
-serialize-javascript@6.0.0, serialize-javascript@^6.0.0:
+serialize-javascript@6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8"
   integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==
   dependencies:
     randombytes "^2.1.0"
 
+serialize-javascript@^6.0.0:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c"
+  integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==
+  dependencies:
+    randombytes "^2.1.0"
+
 serve-index@^1.9.1:
   version "1.9.1"
   resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239"
@@ -2217,9 +2226,9 @@ socket.io-adapter@~2.4.0:
   integrity sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==
 
 socket.io-parser@~4.2.1:
-  version "4.2.1"
-  resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.1.tgz#01c96efa11ded938dcb21cbe590c26af5eff65e5"
-  integrity sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.2.tgz#1dd384019e25b7a3d374877f492ab34f2ad0d206"
+  integrity sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==
   dependencies:
     "@socket.io/component-emitter" "~3.1.0"
     debug "~4.3.1"
@@ -2306,9 +2315,9 @@ statuses@2.0.1:
   integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==
 
 streamroller@^3.1.3:
-  version "3.1.3"
-  resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.3.tgz#d95689a8c29b30d093525d0baffe6616fd62ca7e"
-  integrity sha512-CphIJyFx2SALGHeINanjFRKQ4l7x2c+rXYJ4BMq0gd+ZK0gi4VT8b+eHe2wi58x4UayBAKx4xtHpXT/ea1cz8w==
+  version "3.1.4"
+  resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.4.tgz#844a18e795d39c1089a8216e66a1cf1151271df0"
+  integrity sha512-Ha1Ccw2/N5C/IF8Do6zgNe8F3jQo8MPBnMBGvX0QjNv/I97BcNRzK6/mzOpZHHK7DjMLTI3c7Xw7Y1KvdChkvw==
   dependencies:
     date-format "^4.0.14"
     debug "^4.3.4"
@@ -2390,9 +2399,9 @@ terser-webpack-plugin@^5.1.3:
     terser "^5.14.1"
 
 terser@^5.14.1:
-  version "5.16.0"
-  resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.0.tgz#29362c6f5506e71545c73b069ccd199bb28f7f54"
-  integrity sha512-KjTV81QKStSfwbNiwlBXfcgMcOloyuRdb62/iLFPGBcVNF4EXjhdYBhYHmbJpiBrVxZhDvltE11j+LBQUxEEJg==
+  version "5.16.1"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.1.tgz#5af3bc3d0f24241c7fb2024199d5c461a1075880"
+  integrity sha512-xvQfyfA1ayT0qdK47zskQgRZeWLoOQ8JQ6mIgRGVNwZKdQMU+5FkCBjmv4QjcrTzyZquRw2FVtlJSRUmMKQslw==
   dependencies:
     "@jridgewell/source-map" "^0.3.2"
     acorn "^8.5.0"
@@ -2431,10 +2440,15 @@ type-is@~1.6.18:
     media-typer "0.3.0"
     mime-types "~2.1.24"
 
+typescript@4.7.4:
+  version "4.7.4"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
+  integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
+
 ua-parser-js@^0.7.30:
-  version "0.7.32"
-  resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.32.tgz#cd8c639cdca949e30fa68c44b7813ef13e36d211"
-  integrity sha512-f9BESNVhzlhEFf2CHMSj40NWOjYPl1YKYbrvIr/hFTDEmLq7SRbWvm7FcdcpCYT95zrOhC7gZSxjdnnTpBcwVw==
+  version "0.7.33"
+  resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532"
+  integrity sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw==
 
 universalify@^0.1.0:
   version "0.1.2"
@@ -2486,7 +2500,7 @@ void-elements@^2.0.0:
   resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
   integrity sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==
 
-watchpack@^2.3.1:
+watchpack@^2.4.0:
   version "2.4.0"
   resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
   integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==
@@ -2530,10 +2544,10 @@ webpack-dev-middleware@^5.3.1:
     range-parser "^1.2.1"
     schema-utils "^4.0.0"
 
-webpack-dev-server@4.9.2:
-  version "4.9.2"
-  resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.9.2.tgz#c188db28c7bff12f87deda2a5595679ebbc3c9bc"
-  integrity sha512-H95Ns95dP24ZsEzO6G9iT+PNw4Q7ltll1GfJHV4fKphuHWgKFzGHWi4alTlTnpk1SPPk41X+l2RB7rLfIhnB9Q==
+webpack-dev-server@4.9.3:
+  version "4.9.3"
+  resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.9.3.tgz#2360a5d6d532acb5410a668417ad549ee3b8a3c9"
+  integrity sha512-3qp/eoboZG5/6QgiZ3llN8TUzkSpYg1Ko9khWX1h40MIEUNS2mDoIa8aXsPfskER+GbTvs/IJZ1QTBBhhuetSw==
   dependencies:
     "@types/bonjour" "^3.5.9"
     "@types/connect-history-api-fallback" "^1.3.5"
@@ -2547,7 +2561,7 @@ webpack-dev-server@4.9.2:
     chokidar "^3.5.3"
     colorette "^2.0.10"
     compression "^1.7.4"
-    connect-history-api-fallback "^1.6.0"
+    connect-history-api-fallback "^2.0.0"
     default-gateway "^6.0.3"
     express "^4.17.3"
     graceful-fs "^4.2.6"
@@ -2585,21 +2599,21 @@ webpack-sources@^3.2.3:
   resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
   integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
 
-webpack@5.73.0:
-  version "5.73.0"
-  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.73.0.tgz#bbd17738f8a53ee5760ea2f59dce7f3431d35d38"
-  integrity sha512-svjudQRPPa0YiOYa2lM/Gacw0r6PvxptHj4FuEKQ2kX05ZLkjbVc5MnPs6its5j7IZljnIqSVo/OsY2X0IpHGA==
+webpack@5.74.0:
+  version "5.74.0"
+  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.74.0.tgz#02a5dac19a17e0bb47093f2be67c695102a55980"
+  integrity sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==
   dependencies:
     "@types/eslint-scope" "^3.7.3"
     "@types/estree" "^0.0.51"
     "@webassemblyjs/ast" "1.11.1"
     "@webassemblyjs/wasm-edit" "1.11.1"
     "@webassemblyjs/wasm-parser" "1.11.1"
-    acorn "^8.4.1"
+    acorn "^8.7.1"
     acorn-import-assertions "^1.7.6"
     browserslist "^4.14.5"
     chrome-trace-event "^1.0.2"
-    enhanced-resolve "^5.9.3"
+    enhanced-resolve "^5.10.0"
     es-module-lexer "^0.9.0"
     eslint-scope "5.1.1"
     events "^3.2.0"
@@ -2612,7 +2626,7 @@ webpack@5.73.0:
     schema-utils "^3.1.0"
     tapable "^2.1.1"
     terser-webpack-plugin "^5.1.3"
-    watchpack "^2.3.1"
+    watchpack "^2.4.0"
     webpack-sources "^3.2.3"
 
 websocket-driver@>=0.5.1, websocket-driver@^0.7.4:
@@ -2668,9 +2682,9 @@ wrappy@1:
   integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
 
 ws@^8.4.2:
-  version "8.11.0"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143"
-  integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==
+  version "8.12.0"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-8.12.0.tgz#485074cc392689da78e1828a9ff23585e06cddd8"
+  integrity sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==
 
 ws@~8.2.3:
   version "8.2.3"
diff --git a/redwood-cli/src/main/kotlin/app/cash/redwood/cli/GenerateCommand.kt b/redwood-cli/src/main/kotlin/app/cash/redwood/cli/GenerateCommand.kt
index 5c119ff33..d37a93f53 100644
--- a/redwood-cli/src/main/kotlin/app/cash/redwood/cli/GenerateCommand.kt
+++ b/redwood-cli/src/main/kotlin/app/cash/redwood/cli/GenerateCommand.kt
@@ -62,12 +62,12 @@ internal class GenerateCommand : CliktCommand(name = "generate") {
 
     when (val type = type) {
       is CodegenType -> {
-        val schema = parseSchema(schemaType)
-        schema.generate(type, out)
+        val schemaSet = parseSchema(schemaType)
+        schemaSet.generate(type, out)
       }
       is ProtocolCodegenType -> {
-        val schema = parseProtocolSchema(schemaType)
-        schema.generate(type, out)
+        val schemaSet = parseProtocolSchema(schemaType)
+        schemaSet.generate(type, out)
       }
       else -> throw AssertionError()
     }
diff --git a/redwood-layout-composeui/build.gradle b/redwood-layout-composeui/build.gradle
index 91fafd673..04dc6b412 100644
--- a/redwood-layout-composeui/build.gradle
+++ b/redwood-layout-composeui/build.gradle
@@ -33,7 +33,7 @@ kotlin {
       }
     }
 
-    androidTest {
+    androidInstrumentedTest {
       dependencies {
         implementation projects.redwoodLayoutSharedTest
       }
diff --git a/redwood-layout-composeui/src/androidTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiFlexContainerTest.kt b/redwood-layout-composeui/src/androidInstrumentedTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiFlexContainerTest.kt
similarity index 100%
rename from redwood-layout-composeui/src/androidTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiFlexContainerTest.kt
rename to redwood-layout-composeui/src/androidInstrumentedTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiFlexContainerTest.kt
diff --git a/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolRedwoodComposition.kt b/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolRedwoodComposition.kt
index 384a26ace..49833e5b5 100644
--- a/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolRedwoodComposition.kt
+++ b/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolRedwoodComposition.kt
@@ -16,9 +16,8 @@
 package app.cash.redwood.protocol.compose
 
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.InternalComposeApi
+import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.MonotonicFrameClock
-import androidx.compose.runtime.currentComposer
 import app.cash.redwood.compose.LocalWidgetVersion
 import app.cash.redwood.compose.RedwoodComposition
 import app.cash.redwood.compose.WidgetApplier
@@ -46,24 +45,11 @@ private class ProtocolRedwoodComposition(
   private val composition: RedwoodComposition,
   private val widgetVersion: UInt,
 ) : RedwoodComposition by composition {
-  @OptIn(InternalComposeApi::class) // See internal function comment below.
   override fun setContent(content: @Composable () -> Unit) {
-    // TODO using CompositionLocalProvider fails to link in release mode with:
-    //  inlinable function call in a function with debug info must have a !dbg location
-    //    %16 = call i32 @"kfun:kotlin.Array#<get-size>(){}kotlin.Int"(%struct.ObjHeader* %15)
-    //  inlinable function call in a function with debug info must have a !dbg location
-    //    call void @"kfun:kotlin.Array#<init>(kotlin.Int){}"(%struct.ObjHeader* %18, i32 %17)
-    //  inlinable function call in a function with debug info must have a !dbg location
-    //    %20 = call i32 @"kfun:kotlin.Array#<get-size>(){}kotlin.Int"(%struct.ObjHeader* %19)
-    //  inlinable function call in a function with debug info must have a !dbg location
-    //    %24 = call %struct.ObjHeader* @"kfun:kotlin.collections#copyInto__at__kotlin.Array<out|0:0>(kotlin.Array<0:0>;kotlin.Int;kotlin.Int;kotlin.Int){0\C2\A7<kotlin.Any?>}kotlin.Array<0:0>"(%struct.ObjHeader* %21, %struct.ObjHeader* %22, i32 %23, i32 0, i32 %20, %struct.ObjHeader** %13)
-    //  inlinable function call in a function with debug info must have a !dbg location
-    //    call void @"kfun:androidx.compose.runtime#CompositionLocalProvider(kotlin.Array<out|androidx.compose.runtime.ProvidedValue<*>>...;kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int){}"(%struct.ObjHeader* %27, %struct.ObjHeader* %1, %struct.ObjHeader* %3, i32 %28)
-    val providers = arrayOf(LocalWidgetVersion provides widgetVersion)
     composition.setContent {
-      currentComposer.startProviders(providers)
-      content()
-      currentComposer.endProviders()
+      CompositionLocalProvider(LocalWidgetVersion provides widgetVersion) {
+        content()
+      }
     }
   }
 }
diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/codegen.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/codegen.kt
index fa212bdd7..cb9d02aa0 100644
--- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/codegen.kt
+++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/codegen.kt
@@ -19,7 +19,7 @@ import app.cash.redwood.tooling.codegen.CodegenType.Compose
 import app.cash.redwood.tooling.codegen.CodegenType.LayoutModifiers
 import app.cash.redwood.tooling.codegen.CodegenType.Testing
 import app.cash.redwood.tooling.codegen.CodegenType.Widget
-import app.cash.redwood.tooling.schema.Schema
+import app.cash.redwood.tooling.schema.SchemaSet
 import java.nio.file.Path
 
 public enum class CodegenType {
@@ -29,35 +29,35 @@ public enum class CodegenType {
   Widget,
 }
 
-public fun Schema.generate(type: CodegenType, destination: Path) {
+public fun SchemaSet.generate(type: CodegenType, destination: Path) {
   when (type) {
     Compose -> {
-      generateLayoutModifierImpls(this)?.writeTo(destination)
-      for (scope in scopes) {
-        generateScope(this, scope).writeTo(destination)
+      generateLayoutModifierImpls(schema)?.writeTo(destination)
+      for (scope in schema.scopes) {
+        generateScope(schema, scope).writeTo(destination)
       }
-      for (widget in widgets) {
-        generateComposable(this, widget).writeTo(destination)
+      for (widget in schema.widgets) {
+        generateComposable(schema, widget).writeTo(destination)
       }
     }
     LayoutModifiers -> {
-      for (layoutModifier in layoutModifiers) {
-        generateLayoutModifierInterface(this, layoutModifier).writeTo(destination)
+      for (layoutModifier in schema.layoutModifiers) {
+        generateLayoutModifierInterface(schema, layoutModifier).writeTo(destination)
       }
     }
     Testing -> {
       generateTester(this).writeTo(destination)
-      generateMutableWidgetFactory(this).writeTo(destination)
-      for (widget in widgets) {
-        generateMutableWidget(this, widget).writeTo(destination)
-        generateWidgetValue(this, widget).writeTo(destination)
+      generateMutableWidgetFactory(schema).writeTo(destination)
+      for (widget in schema.widgets) {
+        generateMutableWidget(schema, widget).writeTo(destination)
+        generateWidgetValue(schema, widget).writeTo(destination)
       }
     }
     Widget -> {
       generateWidgetFactories(this).writeTo(destination)
-      generateWidgetFactory(this).writeTo(destination)
-      for (widget in widgets) {
-        generateWidget(this, widget).writeTo(destination)
+      generateWidgetFactory(schema).writeTo(destination)
+      for (widget in schema.widgets) {
+        generateWidget(schema, widget).writeTo(destination)
       }
     }
   }
diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/composeProtocolGeneration.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/composeProtocolGeneration.kt
index 7ad886a97..155485397 100644
--- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/composeProtocolGeneration.kt
+++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/composeProtocolGeneration.kt
@@ -16,6 +16,7 @@
 package app.cash.redwood.tooling.codegen
 
 import app.cash.redwood.tooling.schema.ProtocolSchema
+import app.cash.redwood.tooling.schema.ProtocolSchemaSet
 import app.cash.redwood.tooling.schema.ProtocolWidget
 import app.cash.redwood.tooling.schema.ProtocolWidget.ProtocolChildren
 import app.cash.redwood.tooling.schema.ProtocolWidget.ProtocolEvent
@@ -80,8 +81,9 @@ class ExampleProtocolBridge(
 }
 */
 internal fun generateProtocolBridge(
-  schema: ProtocolSchema,
+  schemaSet: ProtocolSchemaSet,
 ): FileSpec {
+  val schema = schemaSet.schema
   val type = schema.protocolBridgeType()
   val providerType = schema.getWidgetFactoryProviderType().parameterizedBy(NOTHING)
   return FileSpec.builder(type.packageName, type.simpleName)
@@ -149,7 +151,7 @@ internal fun generateProtocolBridge(
                 .addStatement("val root = bridge.widgetChildren(%T.Root, %T.Root)", Protocol.Id, Protocol.ChildrenTag)
                 .apply {
                   val arguments = buildList {
-                    for (dependency in schema.allSchemas) {
+                    for (dependency in schemaSet.all) {
                       add(CodeBlock.of("%N = %T(bridge, json, mismatchHandler)", dependency.type.flatName, dependency.protocolWidgetFactoryType(schema)))
                     }
                   }
@@ -666,8 +668,9 @@ internal fun generateProtocolLayoutModifierSerializers(
 }
 
 internal fun generateProtocolLayoutModifierSerialization(
-  schema: ProtocolSchema,
+  schemaSet: ProtocolSchemaSet,
 ): FileSpec {
+  val schema = schemaSet.schema
   val name = schema.layoutModifierToProtocol.simpleName
   return FileSpec.builder(schema.composePackage(), "layoutModifierSerialization")
     .addFunction(
@@ -693,7 +696,7 @@ internal fun generateProtocolLayoutModifierSerialization(
         .returns(Protocol.LayoutModifierElement)
         .beginControlFlow("return when (this)")
         .apply {
-          val layoutModifiers = schema.allLayoutModifiers()
+          val layoutModifiers = schemaSet.allLayoutModifiers()
           if (layoutModifiers.isEmpty()) {
             addAnnotation(
               AnnotationSpec.builder(Suppress::class)
diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/diffConsumingGeneration.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/diffConsumingGeneration.kt
index 5da4d463f..6d96f20fe 100644
--- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/diffConsumingGeneration.kt
+++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/diffConsumingGeneration.kt
@@ -16,6 +16,7 @@
 package app.cash.redwood.tooling.codegen
 
 import app.cash.redwood.tooling.schema.ProtocolSchema
+import app.cash.redwood.tooling.schema.ProtocolSchemaSet
 import app.cash.redwood.tooling.schema.ProtocolWidget
 import app.cash.redwood.tooling.schema.ProtocolWidget.ProtocolChildren
 import app.cash.redwood.tooling.schema.ProtocolWidget.ProtocolEvent
@@ -57,8 +58,9 @@ public class SunspotDiffConsumingNodeFactory<W : Any>(
 }
 */
 internal fun generateDiffConsumingNodeFactory(
-  schema: ProtocolSchema,
+  schemaSet: ProtocolSchemaSet,
 ): FileSpec {
+  val schema = schemaSet.schema
   val provider = schema.getWidgetFactoryProviderType().parameterizedBy(typeVariableW)
   val type = schema.diffConsumingNodeFactoryType()
   return FileSpec.builder(type.packageName, type.simpleName)
@@ -108,7 +110,7 @@ internal fun generateDiffConsumingNodeFactory(
             )
             .beginControlFlow("return when (tag.value)")
             .apply {
-              for (dependency in schema.allSchemas.sortedBy { it.widgets.firstOrNull()?.tag ?: 0 }) {
+              for (dependency in schemaSet.all.sortedBy { it.widgets.firstOrNull()?.tag ?: 0 }) {
                 for (widget in dependency.widgets.sortedBy { it.tag }) {
                   addStatement(
                     "%L -> %T(parentId, parentChildren, provider.%N.%N(), json, mismatchHandler)",
@@ -338,11 +340,11 @@ internal fun generateDiffConsumingWidget(
 }
 
 internal fun generateDiffConsumingLayoutModifierSerialization(
-  schema: ProtocolSchema,
+  schemaSet: ProtocolSchemaSet,
 ): FileSpec {
-  return FileSpec.builder(schema.widgetPackage(), "layoutModifierSerialization")
-    .addFunction(generateJsonArrayToLayoutModifier(schema))
-    .addFunction(generateJsonElementToLayoutModifier(schema))
+  return FileSpec.builder(schemaSet.schema.widgetPackage(), "layoutModifierSerialization")
+    .addFunction(generateJsonArrayToLayoutModifier(schemaSet.schema))
+    .addFunction(generateJsonElementToLayoutModifier(schemaSet))
     .build()
 }
 
@@ -417,7 +419,7 @@ private fun generateJsonArrayToLayoutModifier(schema: ProtocolSchema): FunSpec {
     .build()
 }
 
-private fun generateJsonElementToLayoutModifier(schema: ProtocolSchema): FunSpec {
+private fun generateJsonElementToLayoutModifier(schemaSet: ProtocolSchemaSet): FunSpec {
   return FunSpec.builder("toLayoutModifier")
     .addModifiers(PRIVATE)
     .receiver(Protocol.LayoutModifierElement)
@@ -426,7 +428,7 @@ private fun generateJsonElementToLayoutModifier(schema: ProtocolSchema): FunSpec
     .returns(Redwood.LayoutModifier)
     .beginControlFlow("val serializer = when (tag.value)")
     .apply {
-      val layoutModifiers = schema.allLayoutModifiers()
+      val layoutModifiers = schemaSet.allLayoutModifiers()
       if (layoutModifiers.isEmpty()) {
         addAnnotation(
           AnnotationSpec.builder(Suppress::class)
@@ -434,8 +436,9 @@ private fun generateJsonElementToLayoutModifier(schema: ProtocolSchema): FunSpec
             .build(),
         )
       } else {
+        val host = schemaSet.schema
         for ((localSchema, layoutModifier) in layoutModifiers) {
-          val typeName = ClassName(localSchema.widgetPackage(schema), layoutModifier.type.flatName + "Impl")
+          val typeName = ClassName(localSchema.widgetPackage(host), layoutModifier.type.flatName + "Impl")
           if (layoutModifier.properties.isEmpty()) {
             addStatement("%L -> return %T", layoutModifier.tag, typeName)
           } else {
diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolCodegen.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolCodegen.kt
index 9d4d7bdc4..c5137dfc3 100644
--- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolCodegen.kt
+++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolCodegen.kt
@@ -18,7 +18,7 @@ package app.cash.redwood.tooling.codegen
 import app.cash.redwood.tooling.codegen.ProtocolCodegenType.Compose
 import app.cash.redwood.tooling.codegen.ProtocolCodegenType.Json
 import app.cash.redwood.tooling.codegen.ProtocolCodegenType.Widget
-import app.cash.redwood.tooling.schema.ProtocolSchema
+import app.cash.redwood.tooling.schema.ProtocolSchemaSet
 import app.cash.redwood.tooling.schema.toEmbeddedSchema
 import java.nio.file.Path
 import kotlin.io.path.createDirectories
@@ -30,21 +30,21 @@ public enum class ProtocolCodegenType {
   Widget,
 }
 
-public fun ProtocolSchema.generate(type: ProtocolCodegenType, destination: Path) {
+public fun ProtocolSchemaSet.generate(type: ProtocolCodegenType, destination: Path) {
   when (type) {
     Compose -> {
       generateProtocolBridge(this).writeTo(destination)
       generateProtocolLayoutModifierSerialization(this).writeTo(destination)
-      for (dependency in allSchemas) {
-        generateProtocolWidgetFactory(dependency, host = this).writeTo(destination)
-        generateProtocolLayoutModifierSerializers(dependency, host = this)?.writeTo(destination)
+      for (dependency in all) {
+        generateProtocolWidgetFactory(dependency, host = schema).writeTo(destination)
+        generateProtocolLayoutModifierSerializers(dependency, host = schema)?.writeTo(destination)
         for (widget in dependency.widgets) {
-          generateProtocolWidget(dependency, widget, host = this).writeTo(destination)
+          generateProtocolWidget(dependency, widget, host = schema).writeTo(destination)
         }
       }
     }
     Json -> {
-      val embeddedSchema = toEmbeddedSchema()
+      val embeddedSchema = schema.toEmbeddedSchema()
       val path = destination.resolve(embeddedSchema.path)
       path.parent.createDirectories()
       path.writeText(embeddedSchema.json)
@@ -52,10 +52,10 @@ public fun ProtocolSchema.generate(type: ProtocolCodegenType, destination: Path)
     Widget -> {
       generateDiffConsumingNodeFactory(this).writeTo(destination)
       generateDiffConsumingLayoutModifierSerialization(this).writeTo(destination)
-      for (dependency in allSchemas) {
-        generateDiffConsumingLayoutModifierImpls(dependency, host = this).writeTo(destination)
+      for (dependency in all) {
+        generateDiffConsumingLayoutModifierImpls(dependency, host = schema).writeTo(destination)
         for (widget in dependency.widgets) {
-          generateDiffConsumingWidget(dependency, widget, host = this).writeTo(destination)
+          generateDiffConsumingWidget(dependency, widget, host = schema).writeTo(destination)
         }
       }
     }
diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/sharedHelpers.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/sharedHelpers.kt
index 391b9bfd7..7569f1730 100644
--- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/sharedHelpers.kt
+++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/sharedHelpers.kt
@@ -19,6 +19,7 @@ import app.cash.redwood.tooling.schema.FqType
 import app.cash.redwood.tooling.schema.LayoutModifier
 import app.cash.redwood.tooling.schema.ProtocolLayoutModifier
 import app.cash.redwood.tooling.schema.ProtocolSchema
+import app.cash.redwood.tooling.schema.ProtocolSchemaSet
 import app.cash.redwood.tooling.schema.Schema
 import app.cash.redwood.tooling.schema.Widget
 import app.cash.redwood.tooling.schema.Widget.Event
@@ -144,8 +145,8 @@ internal val Schema.toLayoutModifier: MemberName get() =
 internal val Schema.layoutModifierToProtocol: MemberName get() =
   MemberName(composePackage(), "toProtocol")
 
-internal fun ProtocolSchema.allLayoutModifiers(): List<Pair<ProtocolSchema, ProtocolLayoutModifier>> {
-  return allSchemas.flatMap { schema ->
+internal fun ProtocolSchemaSet.allLayoutModifiers(): List<Pair<ProtocolSchema, ProtocolLayoutModifier>> {
+  return all.flatMap { schema ->
     schema.layoutModifiers.map { schema to it }
   }
 }
diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/widgetGeneration.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/widgetGeneration.kt
index 4965ccb00..f4343ad2d 100644
--- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/widgetGeneration.kt
+++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/widgetGeneration.kt
@@ -18,6 +18,7 @@ package app.cash.redwood.tooling.codegen
 import app.cash.redwood.tooling.schema.ProtocolWidget
 import app.cash.redwood.tooling.schema.ProtocolWidget.ProtocolTrait
 import app.cash.redwood.tooling.schema.Schema
+import app.cash.redwood.tooling.schema.SchemaSet
 import app.cash.redwood.tooling.schema.Widget
 import app.cash.redwood.tooling.schema.Widget.Children
 import app.cash.redwood.tooling.schema.Widget.Event
@@ -41,7 +42,8 @@ interface SunspotWidgetFactoryProvider<W : Any> : RedwoodLayoutWidgetFactoryProv
   val Sunspot: SunspotWidgetFactory<W>
 }
  */
-internal fun generateWidgetFactories(schema: Schema): FileSpec {
+internal fun generateWidgetFactories(schemaSet: SchemaSet): FileSpec {
+  val schema = schemaSet.schema
   val widgetFactoriesType = schema.getWidgetFactoriesType()
   return FileSpec.builder(widgetFactoriesType.packageName, widgetFactoriesType.simpleName)
     .addType(
@@ -51,7 +53,7 @@ internal fun generateWidgetFactories(schema: Schema): FileSpec {
         .apply {
           val constructorBuilder = FunSpec.constructorBuilder()
 
-          for (dependency in schema.allSchemas) {
+          for (dependency in schemaSet.all) {
             val dependencyType = dependency.getWidgetFactoryType().parameterizedBy(typeVariableW)
             addProperty(
               PropertySpec.builder(dependency.type.flatName, dependencyType, OVERRIDE)
@@ -71,7 +73,7 @@ internal fun generateWidgetFactories(schema: Schema): FileSpec {
         .addSuperinterface(RedwoodWidget.WidgetProvider.parameterizedBy(typeVariableW))
         .addProperty(schema.type.flatName, schema.getWidgetFactoryType().parameterizedBy(typeVariableW))
         .apply {
-          for (dependency in schema.dependencies) {
+          for (dependency in schemaSet.dependencies.values) {
             addSuperinterface(dependency.getWidgetFactoryProviderType().parameterizedBy(typeVariableW))
           }
         }
diff --git a/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schema.kt b/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schema.kt
index 9f3f133a5..3eb750394 100644
--- a/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schema.kt
+++ b/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schema.kt
@@ -20,18 +20,23 @@ import app.cash.redwood.tooling.schema.Widget.Event
 import app.cash.redwood.tooling.schema.Widget.Property
 import app.cash.redwood.tooling.schema.Widget.Trait
 
+/** A [Schema] and its dependencies. */
+public interface SchemaSet {
+  public val schema: Schema
+  public val dependencies: Map<FqType, Schema>
+
+  public val all: List<Schema> get() = buildList {
+    add(schema)
+    addAll(dependencies.values)
+  }
+}
+
 public interface Schema {
   public val type: FqType
   public val scopes: List<FqType>
   public val widgets: List<Widget>
   public val layoutModifiers: List<LayoutModifier>
-  public val dependencies: List<Schema>
-
-  /** This schema and all its [dependencies]. */
-  public val allSchemas: List<Schema> get() = buildList {
-    add(this@Schema)
-    addAll(dependencies)
-  }
+  public val dependencies: List<FqType>
 }
 
 public interface Widget {
@@ -76,15 +81,20 @@ public interface LayoutModifier {
   )
 }
 
+/** A [ProtocolSchema] and its dependencies. */
+public interface ProtocolSchemaSet : SchemaSet {
+  override val schema: ProtocolSchema
+  override val dependencies: Map<FqType, ProtocolSchema>
+
+  override val all: List<ProtocolSchema> get() = buildList {
+    add(schema)
+    addAll(dependencies.values)
+  }
+}
+
 public interface ProtocolSchema : Schema {
   override val widgets: List<ProtocolWidget>
   override val layoutModifiers: List<ProtocolLayoutModifier>
-  override val dependencies: List<ProtocolSchema>
-
-  override val allSchemas: List<ProtocolSchema> get() = buildList {
-    add(this@ProtocolSchema)
-    addAll(dependencies)
-  }
 }
 
 public interface ProtocolWidget : Widget {
diff --git a/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaClasses.kt b/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaClasses.kt
index e642b35b2..60c0ddf34 100644
--- a/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaClasses.kt
+++ b/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaClasses.kt
@@ -21,12 +21,17 @@ import app.cash.redwood.tooling.schema.ProtocolWidget.ProtocolEvent
 import app.cash.redwood.tooling.schema.ProtocolWidget.ProtocolProperty
 import app.cash.redwood.tooling.schema.ProtocolWidget.ProtocolTrait
 
+internal data class ParsedProtocolSchemaSet(
+  override val schema: ProtocolSchema,
+  override val dependencies: Map<FqType, ProtocolSchema>,
+) : ProtocolSchemaSet
+
 internal data class ParsedProtocolSchema(
   override val type: FqType,
   override val scopes: List<FqType>,
   override val widgets: List<ProtocolWidget>,
   override val layoutModifiers: List<ProtocolLayoutModifier>,
-  override val dependencies: List<ProtocolSchema>,
+  override val dependencies: List<FqType>,
 ) : ProtocolSchema
 
 internal data class ParsedProtocolWidget(
diff --git a/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaJson.kt b/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaJson.kt
index bdc11e907..cb07f031e 100644
--- a/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaJson.kt
+++ b/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaJson.kt
@@ -86,7 +86,7 @@ public fun ProtocolSchema.toEmbeddedSchema(): EmbeddedSchema {
         },
       )
     },
-    dependencies = dependencies.map { it.type },
+    dependencies = dependencies,
   )
   val json = json.encodeToString(SchemaJsonV1.serializer(), schemaJson)
 
diff --git a/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaParser.kt b/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaParser.kt
index b5c309d28..d29c366ae 100644
--- a/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaParser.kt
+++ b/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaParser.kt
@@ -41,11 +41,11 @@ private val KClass<*>.schemaAnnotation: SchemaAnnotation get() {
   return requireNotNull(findAnnotation()) { "Schema $qualifiedName missing @Schema annotation" }
 }
 
-public fun parseSchema(schemaType: KClass<*>): Schema {
+public fun parseSchema(schemaType: KClass<*>): SchemaSet {
   return parseProtocolSchema(schemaType)
 }
 
-public fun parseProtocolSchema(schemaType: KClass<*>, tag: Int = 0): ProtocolSchema {
+public fun parseProtocolSchema(schemaType: KClass<*>, tag: Int = 0): ProtocolSchemaSet {
   val schemaAnnotation = schemaType.schemaAnnotation
   require(tag in 0..maxSchemaTag) { "Schema tag must be in range [0, $maxSchemaTag]: $tag" }
 
@@ -154,7 +154,7 @@ public fun parseProtocolSchema(schemaType: KClass<*>, tag: Int = 0): ProtocolSch
       require(it.tag in 1..maxSchemaTag) {
         "Dependency ${it.schema.qualifiedName} tag must be in range (0, $maxSchemaTag]: ${it.tag}"
       }
-      val schema = parseProtocolSchema(it.schema, it.tag)
+      val schema = parseProtocolSchema(it.schema, it.tag).schema
       require(schema.dependencies.isEmpty()) {
         "Schema dependency ${it.schema.qualifiedName} also has its own dependencies. " +
           "For now, only a single level of dependencies is supported."
@@ -167,10 +167,14 @@ public fun parseProtocolSchema(schemaType: KClass<*>, tag: Int = 0): ProtocolSch
     scopes.toList(),
     widgets,
     layoutModifiers,
-    dependencies,
+    dependencies.map { it.type },
+  )
+  val schemaSet = ParsedProtocolSchemaSet(
+    schema,
+    dependencies.associateBy { it.type },
   )
 
-  val duplicatedWidgets = (listOf(schema) + dependencies)
+  val duplicatedWidgets = schemaSet.all
     .flatMap { it.widgets.map { widget -> widget to it } }
     .groupBy { it.first.type }
     .filterValues { it.size > 1 }
@@ -187,7 +191,7 @@ public fun parseProtocolSchema(schemaType: KClass<*>, tag: Int = 0): ProtocolSch
     )
   }
 
-  return schema
+  return schemaSet
 }
 
 private fun parseWidget(
diff --git a/redwood-treehouse-configuration/build.gradle b/redwood-treehouse-configuration/build.gradle
new file mode 100644
index 000000000..1b6a8e588
--- /dev/null
+++ b/redwood-treehouse-configuration/build.gradle
@@ -0,0 +1,18 @@
+import app.cash.redwood.buildsupport.KmpTargets
+
+apply plugin: 'org.jetbrains.kotlin.multiplatform'
+apply plugin: 'org.jetbrains.kotlin.plugin.serialization'
+apply plugin: 'com.vanniktech.maven.publish'
+apply plugin: 'org.jetbrains.dokka' // Must be applied here for publish plugin.
+
+kotlin {
+  KmpTargets.addAllTargets(project)
+
+  sourceSets {
+    commonMain {
+      dependencies {
+        api libs.kotlinx.serialization.core
+      }
+    }
+  }
+}
diff --git a/redwood-treehouse-configuration/gradle.properties b/redwood-treehouse-configuration/gradle.properties
new file mode 100644
index 000000000..9175b104b
--- /dev/null
+++ b/redwood-treehouse-configuration/gradle.properties
@@ -0,0 +1,2 @@
+POM_ARTIFACT_ID=redwood-treehouse-configuration
+POM_NAME=Redwood Treehouse Configuration
diff --git a/redwood-treehouse/src/commonMain/kotlin/app/cash/redwood/treehouse/HostConfiguration.kt b/redwood-treehouse-configuration/src/commonMain/kotlin/app/cash/redwood/treehouse/HostConfiguration.kt
similarity index 100%
rename from redwood-treehouse/src/commonMain/kotlin/app/cash/redwood/treehouse/HostConfiguration.kt
rename to redwood-treehouse-configuration/src/commonMain/kotlin/app/cash/redwood/treehouse/HostConfiguration.kt
diff --git a/redwood-treehouse-guest-compose/build.gradle b/redwood-treehouse-guest-compose/build.gradle
new file mode 100644
index 000000000..33ba793c8
--- /dev/null
+++ b/redwood-treehouse-guest-compose/build.gradle
@@ -0,0 +1,20 @@
+import app.cash.redwood.buildsupport.KmpTargets
+
+apply plugin: 'org.jetbrains.kotlin.multiplatform'
+apply plugin: 'com.vanniktech.maven.publish'
+apply plugin: 'org.jetbrains.dokka' // Must be applied here for publish plugin.
+apply plugin: 'app.cash.redwood.build.compose'
+
+kotlin {
+  KmpTargets.addAllTargets(project)
+
+  sourceSets {
+    commonMain {
+      dependencies {
+        api projects.redwoodProtocolCompose
+        api projects.redwoodTreehouseConfiguration
+        api libs.jetbrains.compose.runtime
+      }
+    }
+  }
+}
diff --git a/redwood-treehouse-guest-compose/gradle.properties b/redwood-treehouse-guest-compose/gradle.properties
new file mode 100644
index 000000000..b2380c703
--- /dev/null
+++ b/redwood-treehouse-guest-compose/gradle.properties
@@ -0,0 +1,2 @@
+POM_ARTIFACT_ID=redwood-treehouse-compose
+POM_NAME=Redwood Treehouse Compose
diff --git a/redwood-treehouse-guest/src/commonMain/kotlin/app/cash/redwood/treehouse/HostConfigurationGuest.kt b/redwood-treehouse-guest-compose/src/commonMain/kotlin/app/cash/redwood/treehouse/HostConfigurationGuest.kt
similarity index 100%
rename from redwood-treehouse-guest/src/commonMain/kotlin/app/cash/redwood/treehouse/HostConfigurationGuest.kt
rename to redwood-treehouse-guest-compose/src/commonMain/kotlin/app/cash/redwood/treehouse/HostConfigurationGuest.kt
diff --git a/redwood-treehouse-guest/src/commonMain/kotlin/app/cash/redwood/treehouse/TreehouseUi.kt b/redwood-treehouse-guest-compose/src/commonMain/kotlin/app/cash/redwood/treehouse/TreehouseUi.kt
similarity index 100%
rename from redwood-treehouse-guest/src/commonMain/kotlin/app/cash/redwood/treehouse/TreehouseUi.kt
rename to redwood-treehouse-guest-compose/src/commonMain/kotlin/app/cash/redwood/treehouse/TreehouseUi.kt
diff --git a/redwood-treehouse-guest-compose/src/commonMain/kotlin/app/cash/redwood/treehouse/composition.kt b/redwood-treehouse-guest-compose/src/commonMain/kotlin/app/cash/redwood/treehouse/composition.kt
new file mode 100644
index 000000000..dc749ae80
--- /dev/null
+++ b/redwood-treehouse-guest-compose/src/commonMain/kotlin/app/cash/redwood/treehouse/composition.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2023 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package app.cash.redwood.treehouse
+
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import app.cash.redwood.compose.RedwoodComposition
+import kotlinx.coroutines.flow.Flow
+
+// Inline at callsite once https://github.com/Kotlin/kotlinx.serialization/issues/1454 is fixed.
+public fun RedwoodComposition.bind(
+  treehouseUi: TreehouseUi,
+  initialHostConfiguration: HostConfiguration,
+  hostConfigurationFlow: Flow<HostConfiguration>,
+) {
+  setContent {
+    val hostConfiguration by hostConfigurationFlow.collectAsState(initialHostConfiguration)
+    CompositionLocalProvider(LocalHostConfiguration provides hostConfiguration) {
+      treehouseUi.Show()
+    }
+  }
+}
diff --git a/redwood-treehouse-guest/build.gradle b/redwood-treehouse-guest/build.gradle
index 49f27b306..1a9321bd5 100644
--- a/redwood-treehouse-guest/build.gradle
+++ b/redwood-treehouse-guest/build.gradle
@@ -6,7 +6,6 @@ apply plugin: 'com.android.library'
 apply plugin: 'com.vanniktech.maven.publish'
 apply plugin: 'org.jetbrains.dokka' // Must be applied here for publish plugin.
 apply plugin: 'app.cash.zipline'
-apply plugin: 'app.cash.redwood.build.compose'
 
 kotlin {
   KmpTargets.addAllTargets(project)
@@ -20,6 +19,7 @@ kotlin {
         api projects.redwoodProtocol
         api projects.redwoodProtocolCompose
         api projects.redwoodTreehouse
+        api projects.redwoodTreehouseGuestCompose
       }
     }
   }
diff --git a/redwood-treehouse-guest/src/commonMain/kotlin/app/cash/redwood/treehouse/treehouseCompose.kt b/redwood-treehouse-guest/src/commonMain/kotlin/app/cash/redwood/treehouse/treehouseCompose.kt
index f4669d932..b3cfdbf2e 100644
--- a/redwood-treehouse-guest/src/commonMain/kotlin/app/cash/redwood/treehouse/treehouseCompose.kt
+++ b/redwood-treehouse-guest/src/commonMain/kotlin/app/cash/redwood/treehouse/treehouseCompose.kt
@@ -15,9 +15,6 @@
  */
 package app.cash.redwood.treehouse
 
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
 import app.cash.redwood.compose.RedwoodComposition
 import app.cash.redwood.protocol.EventSink
 import app.cash.redwood.protocol.compose.ProtocolBridge
@@ -66,12 +63,7 @@ private class RedwoodZiplineTreehouseUi(
     this.composition = composition
 
     val (initialHostConfiguration, hostConfigurationFlow) = hostConfigurations
-    composition.setContent {
-      val hostConfiguration by hostConfigurationFlow.collectAsState(initialHostConfiguration)
-      CompositionLocalProvider(LocalHostConfiguration provides hostConfiguration) {
-        treehouseUi.Show()
-      }
-    }
+    composition.bind(treehouseUi, initialHostConfiguration, hostConfigurationFlow)
   }
 
   override fun close() {
diff --git a/redwood-treehouse-host/build.gradle b/redwood-treehouse-host/build.gradle
index db0404106..985485d00 100644
--- a/redwood-treehouse-host/build.gradle
+++ b/redwood-treehouse-host/build.gradle
@@ -43,7 +43,7 @@ kotlin {
         implementation libs.turbine
       }
     }
-    androidTest {
+    androidInstrumentedTest {
       dependsOn(commonTest)
       dependencies {
         implementation libs.robolectric
diff --git a/redwood-treehouse-host/src/androidTest/kotlin/app/cash/redwood/treehouse/TreehouseWidgetViewTest.kt b/redwood-treehouse-host/src/androidInstrumentedTest/kotlin/app/cash/redwood/treehouse/TreehouseWidgetViewTest.kt
similarity index 100%
rename from redwood-treehouse-host/src/androidTest/kotlin/app/cash/redwood/treehouse/TreehouseWidgetViewTest.kt
rename to redwood-treehouse-host/src/androidInstrumentedTest/kotlin/app/cash/redwood/treehouse/TreehouseWidgetViewTest.kt
diff --git a/redwood-treehouse/build.gradle b/redwood-treehouse/build.gradle
index efe007a23..892ef0f11 100644
--- a/redwood-treehouse/build.gradle
+++ b/redwood-treehouse/build.gradle
@@ -13,7 +13,9 @@ kotlin {
   sourceSets {
     commonMain {
       dependencies {
+        api projects.redwoodCompose
         api projects.redwoodProtocol
+        api projects.redwoodTreehouseConfiguration
         api libs.zipline
       }
     }
diff --git a/redwood-widget/build.gradle b/redwood-widget/build.gradle
index 4bfa7b07d..76e9719aa 100644
--- a/redwood-widget/build.gradle
+++ b/redwood-widget/build.gradle
@@ -22,7 +22,7 @@ kotlin {
         implementation libs.kotlin.test
       }
     }
-    androidTest {
+    androidUnitTest {
       dependencies {
         implementation libs.robolectric
       }
diff --git a/redwood-widget/src/test/kotlin/app/cash/redwood/widget/ViewGroupChildrenTest.kt b/redwood-widget/src/androidUnitTest/kotlin/app/cash/redwood/widget/ViewGroupChildrenTest.kt
similarity index 100%
rename from redwood-widget/src/test/kotlin/app/cash/redwood/widget/ViewGroupChildrenTest.kt
rename to redwood-widget/src/androidUnitTest/kotlin/app/cash/redwood/widget/ViewGroupChildrenTest.kt
diff --git a/samples/counter/ios-shared/build.gradle b/samples/counter/ios-shared/build.gradle
index 771a5d4d3..07ace9d30 100644
--- a/samples/counter/ios-shared/build.gradle
+++ b/samples/counter/ios-shared/build.gradle
@@ -25,7 +25,9 @@ kotlin {
 
   cocoapods {
     noPodspec()
-    frameworkName = 'CounterKt'
+    framework {
+      baseName = 'CounterKt'
+    }
   }
 
   sourceSets {
diff --git a/samples/emoji-search/android-views/src/main/java/com/example/redwood/emojisearch/android/views/EmojiSearchActivity.kt b/samples/emoji-search/android-views/src/main/java/com/example/redwood/emojisearch/android/views/EmojiSearchActivity.kt
index 2306f300a..ce86d7e3a 100644
--- a/samples/emoji-search/android-views/src/main/java/com/example/redwood/emojisearch/android/views/EmojiSearchActivity.kt
+++ b/samples/emoji-search/android-views/src/main/java/com/example/redwood/emojisearch/android/views/EmojiSearchActivity.kt
@@ -19,7 +19,6 @@ import android.os.Bundle
 import androidx.activity.ComponentActivity
 import app.cash.redwood.compose.AndroidUiDispatcher.Companion.Main
 import app.cash.redwood.layout.view.ViewRedwoodLayoutWidgetFactory
-import app.cash.redwood.protocol.widget.DiffConsumingNode
 import app.cash.redwood.protocol.widget.ProtocolMismatchHandler
 import app.cash.redwood.treehouse.TreehouseApp
 import app.cash.redwood.treehouse.TreehouseAppFactory
@@ -42,6 +41,7 @@ class EmojiSearchActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
     super.onCreate(savedInstanceState)
 
+    val context = this
     val treehouseApp = createTreehouseApp()
     val treehouseContent = TreehouseView.Content(EmojiSearchPresenter::launch)
 
@@ -50,20 +50,18 @@ class EmojiSearchActivity : ComponentActivity() {
         app: TreehouseApp<EmojiSearchPresenter>,
         json: Json,
         protocolMismatchHandler: ProtocolMismatchHandler,
-      ): DiffConsumingNode.Factory<*> {
-        return EmojiSearchDiffConsumingNodeFactory(
-          provider = EmojiSearchWidgetFactories(
-            EmojiSearch = AndroidEmojiSearchWidgetFactory(
-              context = this@EmojiSearchActivity,
-              treehouseApp = treehouseApp,
-            ),
-            RedwoodLayout = ViewRedwoodLayoutWidgetFactory(this@EmojiSearchActivity),
-            RedwoodTreehouseLazyLayout = ViewRedwoodTreehouseLazyLayoutWidgetFactory(this@EmojiSearchActivity, treehouseApp, this),
+      ) = EmojiSearchDiffConsumingNodeFactory(
+        provider = EmojiSearchWidgetFactories(
+          EmojiSearch = AndroidEmojiSearchWidgetFactory(
+            context = context,
+            treehouseApp = treehouseApp,
           ),
-          json = json,
-          mismatchHandler = protocolMismatchHandler,
-        )
-      }
+          RedwoodLayout = ViewRedwoodLayoutWidgetFactory(context),
+          RedwoodTreehouseLazyLayout = ViewRedwoodTreehouseLazyLayoutWidgetFactory(context, treehouseApp, this),
+        ),
+        json = json,
+        mismatchHandler = protocolMismatchHandler,
+      )
     }
 
     setContentView(
diff --git a/samples/emoji-search/ios-shared/build.gradle b/samples/emoji-search/ios-shared/build.gradle
index f642ea751..fd7741db8 100644
--- a/samples/emoji-search/ios-shared/build.gradle
+++ b/samples/emoji-search/ios-shared/build.gradle
@@ -8,7 +8,9 @@ kotlin {
 
   cocoapods {
     noPodspec()
-    frameworkName = "EmojiSearchKt"
+    framework {
+      baseName = 'EmojiSearchKt'
+    }
   }
 
   sourceSets {
diff --git a/samples/emoji-search/ios-shared/src/commonMain/kotlin/com/example/redwood/emojisearch/ios/exposed.kt b/samples/emoji-search/ios-shared/src/commonMain/kotlin/com/example/redwood/emojisearch/ios/exposed.kt
index 4d3bb54af..e4904dd1d 100644
--- a/samples/emoji-search/ios-shared/src/commonMain/kotlin/com/example/redwood/emojisearch/ios/exposed.kt
+++ b/samples/emoji-search/ios-shared/src/commonMain/kotlin/com/example/redwood/emojisearch/ios/exposed.kt
@@ -26,7 +26,7 @@ import com.example.redwood.emojisearch.widget.EmojiSearchDiffConsumingNodeFactor
 import com.example.redwood.emojisearch.widget.EmojiSearchWidgetFactories
 import com.example.redwood.emojisearch.widget.EmojiSearchWidgetFactory
 import okio.ByteString
-import okio.toByteString
+import okio.ByteString.Companion.toByteString
 import platform.Foundation.NSData
 
 // Used to export types to Objective-C / Swift.
diff --git a/samples/emoji-search/presenter-treehouse/build.gradle b/samples/emoji-search/presenter-treehouse/build.gradle
index 2272ba3cc..e5cb58dd9 100644
--- a/samples/emoji-search/presenter-treehouse/build.gradle
+++ b/samples/emoji-search/presenter-treehouse/build.gradle
@@ -1,7 +1,6 @@
 apply plugin: 'org.jetbrains.kotlin.multiplatform'
 apply plugin: 'org.jetbrains.kotlin.plugin.serialization'
 apply plugin: 'app.cash.zipline'
-apply plugin: 'app.cash.redwood'
 
 kotlin {
   iosArm64()
diff --git a/samples/emoji-search/presenter-treehouse/src/jsMain/kotlin/com/example/redwood/emojisearch/treehouse/RealEmojiSearchPresenter.kt b/samples/emoji-search/presenter-treehouse/src/jsMain/kotlin/com/example/redwood/emojisearch/treehouse/RealEmojiSearchPresenter.kt
index d17aeca1c..d0ad859d4 100644
--- a/samples/emoji-search/presenter-treehouse/src/jsMain/kotlin/com/example/redwood/emojisearch/treehouse/RealEmojiSearchPresenter.kt
+++ b/samples/emoji-search/presenter-treehouse/src/jsMain/kotlin/com/example/redwood/emojisearch/treehouse/RealEmojiSearchPresenter.kt
@@ -15,18 +15,11 @@
  */
 package com.example.redwood.emojisearch.treehouse
 
-import androidx.compose.runtime.Composable
-import app.cash.redwood.protocol.compose.ProtocolBridge
 import app.cash.redwood.treehouse.StandardFrameClockService
-import app.cash.redwood.treehouse.TreehouseUi
 import app.cash.redwood.treehouse.ZiplineTreehouseUi
 import app.cash.redwood.treehouse.asZiplineTreehouseUi
-import app.cash.redwood.treehouse.lazylayout.compose.LazyColumn
-import app.cash.redwood.treehouse.lazylayout.compose.items
 import com.example.redwood.emojisearch.compose.EmojiSearchProtocolBridge
-import com.example.redwood.emojisearch.presenter.ColumnProvider
-import com.example.redwood.emojisearch.presenter.EmojiSearch
-import com.example.redwood.emojisearch.presenter.HttpClient
+import com.example.redwood.emojisearch.presenter.EmojiSearchTreehouseUi
 import kotlinx.serialization.json.Json
 
 class RealEmojiSearchPresenter(
@@ -37,41 +30,10 @@ class RealEmojiSearchPresenter(
 
   override fun launch(): ZiplineTreehouseUi {
     val bridge = EmojiSearchProtocolBridge.create(json)
-    val httpClient = HostHttpClient(hostApi)
-    val lazyColumnProvider = LazyColumnProvider(bridge)
-    val treehouseUi = object : TreehouseUi {
-      @Composable
-      override fun Show() {
-        EmojiSearch(httpClient, lazyColumnProvider)
-      }
-    }
+    val treehouseUi = EmojiSearchTreehouseUi(hostApi::httpCall, bridge)
     return treehouseUi.asZiplineTreehouseUi(
       bridge = bridge,
       widgetVersion = 0U,
     )
   }
 }
-
-private class HostHttpClient(
-  private val hostApi: HostApi,
-) : HttpClient {
-  override suspend fun call(url: String, headers: Map<String, String>): String {
-    return hostApi.httpCall(url, headers)
-  }
-}
-
-private class LazyColumnProvider(
-  private val bridge: ProtocolBridge,
-) : ColumnProvider {
-  @Composable
-  override fun <T> create(
-    items: List<T>,
-    itemContent: @Composable (item: T) -> Unit,
-  ) {
-    bridge.LazyColumn {
-      items(items) { item ->
-        itemContent(item)
-      }
-    }
-  }
-}
diff --git a/samples/emoji-search/presenter/build.gradle b/samples/emoji-search/presenter/build.gradle
index 03c56790b..2cfa2c636 100644
--- a/samples/emoji-search/presenter/build.gradle
+++ b/samples/emoji-search/presenter/build.gradle
@@ -12,6 +12,8 @@ kotlin {
     commonMain {
       dependencies {
         api projects.samples.emojiSearch.schema.compose
+        api projects.redwoodTreehouse
+        api projects.redwoodTreehouseLazylayoutApi
       }
     }
     commonTest {
diff --git a/samples/emoji-search/presenter/src/commonMain/kotlin/com/example/redwood/emojisearch/presenter/EmojiSearch.kt b/samples/emoji-search/presenter/src/commonMain/kotlin/com/example/redwood/emojisearch/presenter/EmojiSearch.kt
index 2cd7e99a4..c2d59f49c 100644
--- a/samples/emoji-search/presenter/src/commonMain/kotlin/com/example/redwood/emojisearch/presenter/EmojiSearch.kt
+++ b/samples/emoji-search/presenter/src/commonMain/kotlin/com/example/redwood/emojisearch/presenter/EmojiSearch.kt
@@ -42,7 +42,7 @@ private data class EmojiImage(
 )
 
 // TODO Switch to https://github.com/cashapp/zipline/issues/490 once available.
-interface HttpClient {
+fun interface HttpClient {
   suspend fun call(url: String, headers: Map<String, String>): String
 }
 
diff --git a/samples/emoji-search/presenter/src/jsMain/kotlin/com/example/redwood/emojisearch/presenter/EmojiSearchTreehouseUi.kt b/samples/emoji-search/presenter/src/jsMain/kotlin/com/example/redwood/emojisearch/presenter/EmojiSearchTreehouseUi.kt
new file mode 100644
index 000000000..c52824d96
--- /dev/null
+++ b/samples/emoji-search/presenter/src/jsMain/kotlin/com/example/redwood/emojisearch/presenter/EmojiSearchTreehouseUi.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2023 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.redwood.emojisearch.presenter
+
+import androidx.compose.runtime.Composable
+import app.cash.redwood.protocol.compose.ProtocolBridge
+import app.cash.redwood.treehouse.TreehouseUi
+import app.cash.redwood.treehouse.lazylayout.compose.LazyColumn
+import app.cash.redwood.treehouse.lazylayout.compose.items
+
+class EmojiSearchTreehouseUi(
+  private val httpClient: HttpClient,
+  bridge: ProtocolBridge,
+) : TreehouseUi {
+  private val lazyColumnProvider = LazyColumnProvider(bridge)
+
+  @Composable
+  override fun Show() {
+    EmojiSearch(httpClient, lazyColumnProvider)
+  }
+}
+
+private class LazyColumnProvider(
+  private val bridge: ProtocolBridge,
+) : ColumnProvider {
+  @Composable
+  override fun <T> create(
+    items: List<T>,
+    itemContent: @Composable (item: T) -> Unit,
+  ) = with(bridge) {
+    LazyColumn {
+      items(items, itemContent)
+    }
+  }
+}
diff --git a/settings.gradle b/settings.gradle
index 86e2c5700..4f7bf55eb 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -26,7 +26,9 @@ include ':redwood-tooling-lint'
 include ':redwood-tooling-schema'
 include ':redwood-treehouse'
 include ':redwood-treehouse-composeui'
+include ':redwood-treehouse-configuration'
 include ':redwood-treehouse-guest'
+include ':redwood-treehouse-guest-compose'
 include ':redwood-treehouse-host'
 include ':redwood-treehouse-lazylayout-api'
 include ':redwood-treehouse-lazylayout-compose'


Test patch:
diff --git a/redwood-gradle-plugin/src/test/fixture/compose-ui/gradle.properties b/redwood-gradle-plugin/src/test/fixture/compose-ui/gradle.properties
index fa5642925..7853c51dc 100644
--- a/redwood-gradle-plugin/src/test/fixture/compose-ui/gradle.properties
+++ b/redwood-gradle-plugin/src/test/fixture/compose-ui/gradle.properties
@@ -6,4 +6,5 @@ android.defaults.buildfeatures.renderscript=false
 android.defaults.buildfeatures.resvalues=false
 android.defaults.buildfeatures.shaders=false
 
+kotlin.js.compiler=ir
 kotlin.mpp.stability.nowarn=true
diff --git a/redwood-gradle-plugin/src/test/fixture/lint-android-no-kotlin/gradle.properties b/redwood-gradle-plugin/src/test/fixture/lint-android-no-kotlin/gradle.properties
new file mode 100644
index 000000000..0b4e3e129
--- /dev/null
+++ b/redwood-gradle-plugin/src/test/fixture/lint-android-no-kotlin/gradle.properties
@@ -0,0 +1,2 @@
+kotlin.js.compiler=ir
+kotlin.mpp.stability.nowarn=true
diff --git a/redwood-gradle-plugin/src/test/fixture/lint-android/gradle.properties b/redwood-gradle-plugin/src/test/fixture/lint-android/gradle.properties
new file mode 100644
index 000000000..0b4e3e129
--- /dev/null
+++ b/redwood-gradle-plugin/src/test/fixture/lint-android/gradle.properties
@@ -0,0 +1,2 @@
+kotlin.js.compiler=ir
+kotlin.mpp.stability.nowarn=true
diff --git a/redwood-gradle-plugin/src/test/fixture/lint-js/gradle.properties b/redwood-gradle-plugin/src/test/fixture/lint-js/gradle.properties
new file mode 100644
index 000000000..0b4e3e129
--- /dev/null
+++ b/redwood-gradle-plugin/src/test/fixture/lint-js/gradle.properties
@@ -0,0 +1,2 @@
+kotlin.js.compiler=ir
+kotlin.mpp.stability.nowarn=true
diff --git a/redwood-gradle-plugin/src/test/fixture/lint-jvm/gradle.properties b/redwood-gradle-plugin/src/test/fixture/lint-jvm/gradle.properties
new file mode 100644
index 000000000..0b4e3e129
--- /dev/null
+++ b/redwood-gradle-plugin/src/test/fixture/lint-jvm/gradle.properties
@@ -0,0 +1,2 @@
+kotlin.js.compiler=ir
+kotlin.mpp.stability.nowarn=true
diff --git a/redwood-gradle-plugin/src/test/fixture/lint-mpp-android/gradle.properties b/redwood-gradle-plugin/src/test/fixture/lint-mpp-android/gradle.properties
new file mode 100644
index 000000000..0b4e3e129
--- /dev/null
+++ b/redwood-gradle-plugin/src/test/fixture/lint-mpp-android/gradle.properties
@@ -0,0 +1,2 @@
+kotlin.js.compiler=ir
+kotlin.mpp.stability.nowarn=true
diff --git a/redwood-gradle-plugin/src/test/fixture/lint-mpp-no-android/gradle.properties b/redwood-gradle-plugin/src/test/fixture/lint-mpp-no-android/gradle.properties
new file mode 100644
index 000000000..0b4e3e129
--- /dev/null
+++ b/redwood-gradle-plugin/src/test/fixture/lint-mpp-no-android/gradle.properties
@@ -0,0 +1,2 @@
+kotlin.js.compiler=ir
+kotlin.mpp.stability.nowarn=true
diff --git a/redwood-gradle-plugin/src/test/fixture/protocol-no-layout-modifiers/gradle.properties b/redwood-gradle-plugin/src/test/fixture/protocol-no-layout-modifiers/gradle.properties
index 2dcadc5b9..0b4e3e129 100644
--- a/redwood-gradle-plugin/src/test/fixture/protocol-no-layout-modifiers/gradle.properties
+++ b/redwood-gradle-plugin/src/test/fixture/protocol-no-layout-modifiers/gradle.properties
@@ -1,1 +1,2 @@
+kotlin.js.compiler=ir
 kotlin.mpp.stability.nowarn=true
diff --git a/redwood-gradle-plugin/src/test/fixture/schema-project-accessor/gradle.properties b/redwood-gradle-plugin/src/test/fixture/schema-project-accessor/gradle.properties
index 2dcadc5b9..0b4e3e129 100644
--- a/redwood-gradle-plugin/src/test/fixture/schema-project-accessor/gradle.properties
+++ b/redwood-gradle-plugin/src/test/fixture/schema-project-accessor/gradle.properties
@@ -1,1 +1,2 @@
+kotlin.js.compiler=ir
 kotlin.mpp.stability.nowarn=true
diff --git a/redwood-gradle-plugin/src/test/fixture/schema-project-reference/gradle.properties b/redwood-gradle-plugin/src/test/fixture/schema-project-reference/gradle.properties
index 2dcadc5b9..0b4e3e129 100644
--- a/redwood-gradle-plugin/src/test/fixture/schema-project-reference/gradle.properties
+++ b/redwood-gradle-plugin/src/test/fixture/schema-project-reference/gradle.properties
@@ -1,1 +1,2 @@
+kotlin.js.compiler=ir
 kotlin.mpp.stability.nowarn=true
diff --git a/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/ProtocolTest.kt b/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/ProtocolTest.kt
index 70741a417..ab9d640f9 100644
--- a/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/ProtocolTest.kt
+++ b/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/ProtocolTest.kt
@@ -116,15 +116,18 @@ class ProtocolTest {
     )
 
     composition.setContent {
+      // TODO This test will fail if you inline the when. Bug in Compose compiler?
+      //  https://issuetracker.google.com/issues/268248361
+      val onClick = when (state) {
+        0 -> { { state = 1 } }
+        1 -> { { state = 2 } }
+        2 -> { null }
+        3 -> { null }
+        else -> fail()
+      }
       Button(
         "state: $state",
-        onClick = when (state) {
-          0 -> { { state = 1 } }
-          1 -> { { state = 2 } }
-          2 -> { null }
-          3 -> { null }
-          else -> fail()
-        },
+        onClick = onClick,
       )
     }
 
diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/testingGeneration.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/testingGeneration.kt
index eb58577a9..c6cda4114 100644
--- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/testingGeneration.kt
+++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/testingGeneration.kt
@@ -17,6 +17,7 @@ package app.cash.redwood.tooling.codegen
 
 import app.cash.redwood.tooling.schema.ProtocolWidget.ProtocolTrait
 import app.cash.redwood.tooling.schema.Schema
+import app.cash.redwood.tooling.schema.SchemaSet
 import app.cash.redwood.tooling.schema.Widget
 import app.cash.redwood.tooling.schema.Widget.Children
 import app.cash.redwood.tooling.schema.Widget.Event
@@ -49,7 +50,8 @@ public fun SunspotTester(scope: CoroutineScope): RedwoodTester {
   )
 }
 */
-internal fun generateTester(schema: Schema): FileSpec {
+internal fun generateTester(schemaSet: SchemaSet): FileSpec {
+  val schema = schemaSet.schema
   val testerFunction = schema.getTesterFunction()
   return FileSpec.builder(testerFunction.packageName, testerFunction.simpleName)
     .addFunction(
@@ -61,7 +63,7 @@ internal fun generateTester(schema: Schema): FileSpec {
         .addCode("scope = scope,\n")
         .addCode("provider = %T(⇥\n", schema.getWidgetFactoriesType())
         .apply {
-          for (dependency in schema.allSchemas) {
+          for (dependency in schemaSet.all) {
             addCode("%N = %T(),\n", dependency.type.flatName, dependency.getMutableWidgetFactoryType())
           }
         }
diff --git a/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/ComposeGenerationTest.kt b/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/ComposeGenerationTest.kt
index edea0bc23..9949bfd7a 100644
--- a/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/ComposeGenerationTest.kt
+++ b/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/ComposeGenerationTest.kt
@@ -43,7 +43,7 @@ class ComposeGenerationTest {
   )
 
   @Test fun `scoped and unscoped children`() {
-    val schema = parseSchema(ScopedAndUnscopedSchema::class)
+    val schema = parseSchema(ScopedAndUnscopedSchema::class).schema
 
     val fileSpec = generateComposable(schema, schema.widgets.single())
     assertThat(fileSpec.toString()).apply {
@@ -53,7 +53,7 @@ class ComposeGenerationTest {
   }
 
   @Test fun `scope is annotated with layout scope marker`() {
-    val schema = parseSchema(ScopedAndUnscopedSchema::class)
+    val schema = parseSchema(ScopedAndUnscopedSchema::class).schema
 
     val fileSpec = generateScope(schema, FqType(listOf("example", "RowScope")))
     assertThat(fileSpec.toString()).contains(
@@ -85,7 +85,7 @@ class ComposeGenerationTest {
   )
 
   @Test fun `default is supported for all property types`() {
-    val schema = parseSchema(DefaultSchema::class)
+    val schema = parseSchema(DefaultSchema::class).schema
 
     val fileSpec = generateComposable(schema, schema.widgets.single())
     assertThat(fileSpec.toString()).apply {
@@ -109,7 +109,7 @@ class ComposeGenerationTest {
   )
 
   @Test fun `layout modifier is the last non child parameter`() {
-    val schema = parseSchema(MultipleChildSchema::class)
+    val schema = parseSchema(MultipleChildSchema::class).schema
 
     val fileSpec = generateComposable(schema, schema.widgets.single())
     assertThat(fileSpec.toString()).contains(
diff --git a/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/ComposeProtocolGenerationTest.kt b/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/ComposeProtocolGenerationTest.kt
index b4a9aaf5f..b91783bad 100644
--- a/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/ComposeProtocolGenerationTest.kt
+++ b/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/ComposeProtocolGenerationTest.kt
@@ -37,7 +37,7 @@ class ComposeProtocolGenerationTest {
   )
 
   @Test fun `id property does not collide`() {
-    val schema = parseProtocolSchema(IdPropertyNameCollisionSchema::class)
+    val schema = parseProtocolSchema(IdPropertyNameCollisionSchema::class).schema
 
     val fileSpec = generateProtocolWidget(schema, schema.widgets.single())
     assertThat(fileSpec.toString()).contains(
@@ -49,9 +49,9 @@ class ComposeProtocolGenerationTest {
   }
 
   @Test fun `dependency layout modifiers are included in serialization`() {
-    val schema = parseProtocolSchema(PrimarySchema::class)
+    val schemaSet = parseProtocolSchema(PrimarySchema::class)
 
-    val fileSpec = generateProtocolLayoutModifierSerialization(schema)
+    val fileSpec = generateProtocolLayoutModifierSerialization(schemaSet)
     assertThat(fileSpec.toString()).apply {
       contains("is PrimaryModifier -> PrimaryModifierSerializer.encode(json, this)")
       contains("is SecondaryModifier -> SecondaryModifierSerializer.encode(json, this)")
diff --git a/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/LayoutModifierGenerationTest.kt b/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/LayoutModifierGenerationTest.kt
index 363aff119..c0dd6f67d 100644
--- a/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/LayoutModifierGenerationTest.kt
+++ b/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/LayoutModifierGenerationTest.kt
@@ -44,7 +44,7 @@ class LayoutModifierGenerationTest {
   data class ContentDescription(val text: String)
 
   @Test fun `simple names do not collide`() {
-    val schema = parseSchema(SimpleNameCollisionSchema::class)
+    val schema = parseSchema(SimpleNameCollisionSchema::class).schema
 
     val topType = schema.layoutModifiers.single { it.type.flatName == "LayoutModifierGenerationTestContentDescription" }
     val topTypeSpec = generateLayoutModifierInterface(schema, topType)
@@ -68,7 +68,7 @@ class LayoutModifierGenerationTest {
   object ScopedLayoutModifier
 
   @Test fun `layout modifier functions are stable`() {
-    val schema = parseSchema(ScopedModifierSchema::class)
+    val schema = parseSchema(ScopedModifierSchema::class).schema
 
     val modifier = schema.layoutModifiers.single { it.type.names.last() == "ScopedLayoutModifier" }
     val scope = modifier.scopes.single { it.names.last() == "LayoutModifierScope" }
diff --git a/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/TestingGenerationTest.kt b/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/TestingGenerationTest.kt
index b6336f491..6ff468600 100644
--- a/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/TestingGenerationTest.kt
+++ b/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/TestingGenerationTest.kt
@@ -46,9 +46,10 @@ class TestingGenerationTest {
   )
 
   @Test fun `tester happy path`() {
-    val schema = parseSchema(HappyPathSchema::class)
+    val schemaSet = parseSchema(HappyPathSchema::class)
+    val schema = schemaSet.schema
 
-    val testerFileSpec = generateTester(schema)
+    val testerFileSpec = generateTester(schemaSet)
     assertThat(testerFileSpec.toString()).contains(
       """
       |@OptIn(RedwoodCodegenApi::class)
diff --git a/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/WidgetGenerationTest.kt b/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/WidgetGenerationTest.kt
index b7283e87e..6cf516074 100644
--- a/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/WidgetGenerationTest.kt
+++ b/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/WidgetGenerationTest.kt
@@ -39,7 +39,7 @@ class WidgetGenerationTest {
   data class Button(@Property(1) val text: String)
 
   @Test fun `simple names do not collide`() {
-    val schema = parseSchema(SimpleNameCollisionSchema::class)
+    val schema = parseSchema(SimpleNameCollisionSchema::class).schema
 
     val fileSpec = generateWidgetFactory(schema)
     assertThat(fileSpec.toString()).apply {
@@ -49,7 +49,7 @@ class WidgetGenerationTest {
   }
 
   @Test fun tagInWidgetFactoryKDoc() {
-    val schema = parseSchema(SimpleNameCollisionSchema::class)
+    val schema = parseSchema(SimpleNameCollisionSchema::class).schema
 
     val fileSpec = generateWidgetFactory(schema)
     assertThat(fileSpec.toString()).contains(
@@ -62,7 +62,7 @@ class WidgetGenerationTest {
   }
 
   @Test fun tagInWidgetKdoc() {
-    val schema = parseSchema(SimpleNameCollisionSchema::class)
+    val schema = parseSchema(SimpleNameCollisionSchema::class).schema
     val button = schema.widgets.single { it.type.flatName == "WidgetGenerationTestButton" }
 
     val fileSpec = generateWidget(schema, button)
diff --git a/redwood-tooling-schema/src/test/kotlin/app/cash/redwood/tooling/schema/SchemaParserTest.kt b/redwood-tooling-schema/src/test/kotlin/app/cash/redwood/tooling/schema/SchemaParserTest.kt
index c3e78f373..d49d155b9 100644
--- a/redwood-tooling-schema/src/test/kotlin/app/cash/redwood/tooling/schema/SchemaParserTest.kt
+++ b/redwood-tooling-schema/src/test/kotlin/app/cash/redwood/tooling/schema/SchemaParserTest.kt
@@ -406,7 +406,7 @@ class SchemaParserTest {
   )
 
   @Test fun eventTypes() {
-    val schema = parseProtocolSchema(EventTypeSchema::class)
+    val schema = parseProtocolSchema(EventTypeSchema::class).schema
     val widget = schema.widgets.single()
     assertThat(widget.traits.single { it.name == "requiredEvent" }).isInstanceOf<Event>()
     assertThat(widget.traits.single { it.name == "optionalEvent" }).isInstanceOf<Event>()
@@ -427,7 +427,7 @@ class SchemaParserTest {
   )
 
   @Test fun eventArguments() {
-    val schema = parseProtocolSchema(EventArgumentsSchema::class)
+    val schema = parseProtocolSchema(EventArgumentsSchema::class).schema
     val widget = schema.widgets.single()
 
     val noArguments = widget.traits.single { it.name == "noArguments" } as Event
@@ -470,7 +470,7 @@ class SchemaParserTest {
   object ObjectWidget
 
   @Test fun objectWidget() {
-    val schema = parseProtocolSchema(ObjectSchema::class)
+    val schema = parseProtocolSchema(ObjectSchema::class).schema
     val widget = schema.widgets.single()
     assertThat(widget.traits).isEmpty()
   }
@@ -570,7 +570,7 @@ class SchemaParserTest {
   )
 
   @Test fun schemaTagDefault() {
-    val schema = parseProtocolSchema(SchemaTag::class)
+    val schema = parseProtocolSchema(SchemaTag::class).schema
 
     val widget = schema.widgets.single()
     assertThat(widget.tag).isEqualTo(1)
@@ -591,7 +591,7 @@ class SchemaParserTest {
 
   @Test fun schemaTagOffsetsMemberTags() {
     val schema = parseProtocolSchema(SchemaDependencyTagOffsetsMemberTags::class)
-    val dependency = schema.dependencies.single()
+    val dependency = schema.dependencies.values.single()
 
     val widget = dependency.widgets.single()
     assertThat(widget.tag).isEqualTo(4_000_001)