Compose Multiplatform entry point for the browser, using ComposeViewport
(confirmed as the current, non-deprecated bootstrap for the pinned 1.12.0
against JetBrains' own compose-multiplatform example repo and the 1.12.0
CHANGELOG, since hosted docs didn't carry the exact signature). Wires in
every wasmJs-specific Koin module this whole effort left unregistered
(CorePrefsWasmJsModule, CoreDatastoreWasmJsModule, CoreNetworkWasmJsModule,
CoreBleWasmJsModule) alongside the v0 feature slice per the workpad's AC9:
connections, messaging, node, settings. Map and every other feature module
are absent by dependency-list omission, not a runtime flag.
Shaped directly on desktopApp's module: same DatabaseManager/MessageQueue/
process-lifecycle/BuildConfigProvider platform-stub pattern, with
browser-native replacements (localStorage-backed prefs/datastore, OPFS/Web
Worker Room persistence, Web Bluetooth, WebSocket-only MQTT) instead of
no-ops wherever a real implementation exists, and honest no-ops (TAK,
widgets, notifications, phone location/compass, bundled-asset seeding)
wherever the browser sandbox genuinely can't provide the platform concept.
This module is NOT included in settings.gradle.kts yet. Adding `:webApp`
to the include list breaks root-level Gradle configuration for every
other module: KotlinRootNpmResolver throws "IllegalStateException:
:core:common is not configured for JS usage" before any task runs.
Isolated experimentally: the trigger is specifically `binaries.executable()`,
not `browser()` — with `:webApp` included and only `binaries.executable()`
removed, `./gradlew :core:common:help --dry-run` and `./gradlew projects`
both configure cleanly. So this is inherent to declaring an executable
Kotlin/Wasm binary anywhere in a multi-project build that also contains
other wasmJs-target subprojects, not something scoped to this module's
own config. The likely fix is a Gradle composite build (webApp as its own
build via includeBuild(), consuming the libraries through dependency
substitution instead of as a subproject) — not attempted here: it needs
build-logic's convention plugins and the version catalog shared across a
build boundary, plus substitution rules for webApp's ~15 transitive
project() dependencies, which is a real restructuring that deserves its
own pass rather than a blind attempt at the tail of this one.
:webApp:compileKotlinWasmJs and :webApp:compileTestKotlinWasmJs both pass
in isolation (verified via an ad-hoc temporary settings.gradle.kts include
plus a scoped gradle-runner pass); :webApp:wasmJsBrowserDistribution has
not been made to succeed even in isolation, a second, separate packaging
gap. See .agent_plans/web-target-workpad.md's webApp milestone entry for
the full diagnosis.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
detekt's source set list never included src/wasmJsMain or src/nonWebMain,
so every prior "detekt clean" claim in this effort never actually scanned
wasmJs code. Found while adding :webApp (whose entire source lives in
wasmJsMain and reported NO-SOURCE forever) and fixed at the root: both
source sets are now scanned repo-wide. Real findings it surfaced are
suppressed with honest, specific justification (embedded js("...") string
interop that static analysis can't see into) or fixed outright — nothing
silenced to dodge a genuine issue.
core:ble's own compileTestKotlinWasmJs had never actually been exercised:
7 of its 9 commonTest files reference Kable directly, which has no wasmJs
target, so they silently never compiled for wasmJs until this pass tried.
Moved them to a new nonWebTest source set, matching this effort's existing
android/jvm/ios-only test pattern elsewhere.
core:repository had a latent iOS-hierarchy trap: a manual nonWebTest
dependsOn edge (needed once core:testing was pulled out of commonTest)
silently disabled Kotlin's default hierarchy template, which is what wires
iosMain to the leaf iOS compilations. Restored the explicit
applyHierarchyTemplate(default) call alongside the manual edge.
Also tried retrofitting every wasmJs-enabled module to wasmJs { browser() }
to satisfy KGP's root npm resolver ahead of the webApp module (see next
commit) — reverted repo-wide: browser() arms each module's
wasmJsBrowserTest task, which allTests then picks up with no
karma/headless-Chrome runner configured anywhere in this repo, a CI-gate
regression that outweighs the benefit. Every wasmJs-enabled module's
comment now explains this explicitly instead of the stale "that's for the
eventual webApp executable" text, which turned out to be wrong.
core:ui's MeshtasticNavigationSuite gained an optional visibleDestinations
parameter (default: today's full tab set) so a host can hide a tab it has
no entry provider for without a per-platform expect/actual — needed by
webApp, which omits Map.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
feature:settings is the largest of this effort's four v0 feature
modules (all now wasmJs-enabled) and the only one requiring a real
architectural decision, not a mechanical port: two commonMain files
(TAKConfigItemList.kt, defining TAKConfigScreen/TakServerScreen, and
TAKConfigPreviews.kt) directly use core:takserver's TAKServerManager/
TAKMeshIntegration/TAKDataPackageGenerator/TakMeshTestRunner/
TakProtocol/TakTestResult types. core:takserver can never get a wasmJs
target -- its production implementation binds an inbound TLS
SSLServerSocket listener, which a browser sandbox can never accept --
so this module now excludes TAK the same way core:service's
MeshServiceOrchestrator already does (TakServerIntegration/
NoopTakServerIntegration, commit c3d19fb82).
A new expect val isTakSupportedOnPlatform (tak/TakAvailability.kt,
true everywhere except wasmJs) gates TAK at three sites:
ModuleRoute.TAK.isSupported, the "TAK Server" menu item in
RadioConfig.kt, and -- a real gap found during this pass, not flagged
by any grep -- the hiddenFeaturesUnlocked easter-egg branch in
ModuleConfigurationScreen.kt, which rendered raw unfiltered
ModuleRoute.entries and bypassed the gate entirely. Two further
expect/actual seams (TakModuleConfigContent, registerTakServerDestination)
handle the navigation graph itself: real on android/jvm/iOS (wiring
the unchanged TAKConfigScreen/TakServerScreen), an honest empty
composable / no-registration on wasmJs -- a defensive fallback for a
direct deep link, since the menu gate above is the primary but not
only way to reach these destinations. TAKConfigItemList.kt,
TAKConfigPreviews.kt, TakPermissionUtil.kt, and PrefExporter.kt move
to nonWebMain wholesale.
Seven other expect/actual pairs get real wasmJs implementations:
rememberSystemTimeZonePosixString (real Intl/Date JS interop, not a
hardcoded fallback), SecurityKeyBackupActions/RingtoneTrailingIcon/
DeviceLocationButton (honest no-ops matching existing iOS/JVM
precedent -- no browser equivalent for Android-specific concepts),
rememberLogExporter/captureAppLogcat (no-ops, consistent with
core:ui's already-deferred file-save capability on web),
getAboutLibrariesJson (empty string -- the library itself ships a
real wasmJs variant, but wiring its JSON asset through Compose
resources wasn't done this pass), and SettingsMainScreen (reuses
DesktopSettingsScreen.kt after confirming it was already pure Compose
Multiplatform with no jvm-only imports -- relocated from jvmMain to
commonMain rather than duplicated; web and desktop share the same
wide-surface layout, unlike Android's phone-shaped one).
meshtastic.kmp.jvm.android is removed from this module's plugins
block (its own applyHierarchyTemplate call would conflict with this
module's -- Gradle allows exactly one per project) and replaced with
an inlined jvmAndroid group nested inside nonWeb, mirroring
core:network's identical precedent. 8 of the module's commonTest
files (importing core:testing, which has no wasmJs target) move to
nonWebTest.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
feature:node depends on core:common/data/database/datastore/domain/
model/navigation/repository/resources/service/ui/di (all already
wasmJs-enabled), coil, jetbrains.compose.material3.adaptive*, and two
libraries not yet checked by this session: com.mikepenz:multiplatform-
markdown-renderer(-m3) and com.patrykandpatrick.vico:compose(-m3) --
both confirmed to publish real wasmJs Gradle variants at their exact
pinned versions (0.45.0, 3.3.1) via Maven Central's own module
metadata.
feature:map (a real, 92-file module with its own unported expect/
actual gaps -- map rendering is deferred, this effort's own v0 scope)
was a commonMain dependency here too, but a grep found zero direct
references to it anywhere in feature:node's own source: the
dependency exists purely for Koin's classpath-based
ComponentScan("org.meshtastic.feature.map") to reach feature:map's own
@Single-annotated MapLayersManager. Since feature:map has no wasmJs
target, the dependency moves to per-platform main source sets
(android/jvm/iOS) instead of commonMain -- feature:map itself is
untouched.
This module also surfaces a real limit in this session's own
screening method: a loose `grep "expect \|actual "` pass missed a
genuine expect/actual pair in ChartDrawGuard.kt
(platformSaveCount/platformRestoreToCount), because an unrelated
`compareAndSet(expect = ...)` call elsewhere in the same file produced
a false-positive-shaped match that made the earlier pass stop looking
closely enough. The wasmJs actual is byte-identical to the existing
jvmMain one (both render via Skia/Skiko's skiaCanvas, unlike Android's
native Canvas) -- confirmed by a real compile, not assumed from the
jvm actual's shape.
Wiring the relocated feature:map dependency to iosMain needed a bare
applyHierarchyTemplate(KotlinHierarchyTemplate.default) call: unlike
core:ble/feature:connections (which both needed a custom "nonWeb"
group), this module's iosMain source set had unreliable
creation/wiring timing with wasmJs() + android registered but no
template call of its own -- the default template alone resolves it,
with iosMain correctly wired to appleMain by the template itself (no
extra manual dependsOn edge needed, unlike those two modules' iOS gap).
2 of 24 commonTest files (importing core:testing, which has no wasmJs
target) move to a new nonWebTest source set.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
feature:messaging depends on core:common/data/database/domain/model/
navigation/prefs/resources/service/ui (all already wasmJs-enabled)
plus androidx.paging.common/paging-compose and jetbrains.compose.
material3.adaptive* (all confirmed to publish real wasmJs Gradle
variants).
Unlike feature:connections, this module needed no interface-extraction
seam: a fresh run of both this session's screening passes (expect/
actual + java.*/android.* imports; transitive nonWebMain-only symbol
references from every dependency) found zero hits. The screening
recipe itself gets sharper here -- blindly grepping every dependency's
declared class/object/interface names has a blind spot for top-level
fun/val/typealias declarations and is noisy against local variables,
so this pass instead enumerates the module's own actual import lines
and checks each imported symbol's real source-set origin. Only
core.database.entity.QuickChatAction (a commonMain Room entity) and
core.resources.* (commonMain generated strings) turned up, both
already proven wasmJs-safe.
3 of 14 commonTest files (importing core:testing, which has no wasmJs
target) move to a manually-created nonWebTest source set, wired to
every leaf test source set via the degrade-safe matching{} pattern
(this module has no android { withHostTest {} } call, so
androidHostTest never materializes -- the same fix core:ui's pass
needed for the same reason). core:testing itself is wired into
nonWebTest by KmpFeatureConventionPlugin's existing afterEvaluate
fix, not declared here.
[DEFERRED] MessageQueue (a core:repository interface) has no wasmJs
implementation, but this isn't a gap introduced here: its only
implementation in this module (WorkManagerMessageQueue) is
androidMain-only already, and the binding is supplied by the platform
executable (androidApp/desktopApp/eventually webApp), not this
feature module -- iOS has no implementation anywhere in the repo
today either.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The v0 web slice (feature:connections/messaging/node/settings, per
this effort's own architecture decision) all share
KmpFeatureConventionPlugin. Its apply() wired core:testing directly
into every consumer's commonTest, unconditionally, at plugin-apply
time -- before the consuming module's own build.gradle.kts kotlin {}
block (and any nonWebTest source set it creates) has even run.
core:testing has no wasmJs target, so every v0 feature module would
have hit the identical compileTestKotlinWasmJs failure the moment it
opted in, no matter what its own build.gradle.kts did to try to route
around it -- a shared-build-logic problem, not a per-module one.
Fixed by deferring the wiring to target.afterEvaluate, which checks
whether the consuming module ended up with a wasmJs target and a
nonWebTest source set and routes core:testing there instead when both
exist, with a fail-fast check() if a module has one but not the other.
Every non-wasmJs feature module keeps resolving core:testing via
commonTest exactly as before -- verified with a real compile+test run
across all nine other consumers (messaging, node, settings,
map-maplibre, intro, discovery, docs, firmware, wifi-provision), zero
regression.
core:domain (a feature:connections dependency, zero expect/actual,
zero java.*/android.* imports, every dependency already wasmJs-clean)
gets a bare wasmJs() -- mechanical.
feature:connections surfaced a sharper version of the screening test
this session has used for every prior module: "no expect/actual, no
java.*/android.* imports" is necessary but not sufficient.
ScannerViewModel.kt/CommonGetDiscoveredDevicesUseCase.kt directly
referenced core:datastore's RecentAddressesDataSource/
FirmwareRecoveryDataSource -- concrete classes that live in that
module's own nonWebMain (Preferences-backed, no wasmJs variant),
reached transitively rather than through any local expect/actual. Two
new feature-local interfaces (RecentAddressesSource,
PendingFirmwareRecoverySource) seam this off: a nonWebMain adapter
delegates to the real DataStore-backed sources unchanged, and wasmJs
gets an honest no-op (no recent-address history, no firmware-recovery
banner on web this pass) -- same shape as core:service's
TakServerIntegration seam. A real localStorage-backed implementation
is deferred until a webApp module exists to wire one in.
Also fixes an unrelated, pre-existing detekt violation
(NoUnusedImports on ProjectExtensions.kt) surfaced while re-running
build-logic/convention's own lint as part of this pass's verification
-- unrelated to this change's own logic, folded in since it was
already in front of us.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
core:ui is the first UI-layer module in this effort -- 204 Kotlin
files, the substrate every feature module depends on. Every one of
its own dependencies (core/*, coil3, jetbrains:markdown, qrcode-kotlin,
every compose-multiplatform-*/jetbrains-* artifact) already publishes
a wasmJs variant, so this was mostly a platform-seam problem, not a
dependency problem: 6 commonMain files carry expect/actual
declarations (an initial survey found 3; a second sweep before writing
any code found 3 more), and the other 198 files needed zero changes.
PlatformUtils.kt's ~23 members are mostly Android-system-settings
concepts (NFC/Bluetooth/Wi-Fi/location/app settings deep-links,
ahead-of-time permission-state checks, native file/document pickers)
with no browser equivalent -- these mirror iOS's own NoopStubs.kt
exactly, the established precedent in this codebase for "the platform
doesn't have this concept, don't block the UI on it." Two exceptions
get real implementations instead: rememberOpenUrl opens the URL via
window.open, and KeepScreenOn is backed by the real Screen Wake Lock
API (hand-declared external binding, kotlinx-browser has none) with
its acquire/release lifecycle kept in one coroutine via
awaitCancellation()/finally so a fast dispose can never race ahead of
an in-flight request and leak the lock.
createClipEntry (ClipboardUtils.kt) upgrades to ClipEntry.withPlainText
-- verified against Compose Multiplatform's own wasmJs klib metadata
that this is the only factory available on this target, with the
actual Clipboard API access handled inside Compose Multiplatform's own
setClipEntry() implementation, not something this module needs to wire
itself. annotatedStringFromHtml (HtmlUtils.kt) keeps the plain-text
fallback that the JVM/desktop actual already uses -- matching this
codebase's own established fidelity bar for HTML rendering off Android,
not a regression introduced here. dynamicColorScheme, SetScreenBrightness,
and DropDownPreference's enum-reflection helpers (found in the second
sweep) all get honest no-ops matching the JVM/iOS actuals already in
place, each documented with why no better browser answer exists.
3 of 27 commonTest files (the ones depending on core:testing, which
has no wasmJs target -- the same gap every prior module's test suite
has hit) move to a new nonWebTest source set; a vestigial, actually-unused
libs.junit dependency moves with them.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
core:service is the central app-orchestration layer: MeshServiceOrchestrator
is constructed by both androidApp's MeshService.kt and desktopApp's
Main.kt, and will be needed by a future web client too -- it can't be
wholesale-excluded the way a leaf feature could be.
Its only web-hostile dependency was two constructor parameters typed
directly against core:takserver: TAKServerManager (already an
interface) and TAKMeshIntegration (a concrete class). core:takserver
can never get a wasmJs target -- its production implementation is a
TLS SSLServerSocket *listener* accepting inbound ATAK/iTAK
connections, and a browser sandbox can never accept inbound
connections at all, a more fundamental impossibility than the
outbound-only TCP case already excluded for MQTT.
MeshServiceOrchestrator only ever read takServerManager.isRunning and
called takMeshIntegration.start()/stop() -- the entire interaction
surface. Following this codebase's own convention (core:repository
hosts portable interfaces, core:*Impl-style modules hold the
platform-coupled implementation), a new minimal TakServerIntegration
interface in core:repository folds those three members into one seam.
TAKMeshIntegration now implements it directly; isRunning delegates to
the real takServerManager.isRunning (its own internal start/stop
re-entrancy latch, previously also named isRunning, is renamed to
isRunningState to keep the two states distinct). Its Koin provider
binds under both types (`@Single(binds = [TAKMeshIntegration::class,
TakServerIntegration::class])`) so feature/settings' debug UI still
resolves the concrete class while core:service resolves only the
interface -- verified sound against Koin's own K2-compiler-plugin
binding model via a real KoinVerificationTest run, not assumed.
core:service's dependency on core:takserver is removed entirely
(confirmed via grep: nothing else in the module referenced it).
wasmJs gets a real, honest no-op TakServerIntegration -- isRunning
always false, start/stop are no-ops -- documented as a permanent
platform impossibility, not a stand-in for future work.
androidApp/desktopApp needed zero changes: both already register
core:takserver's own Koin module directly, which still supplies the
real implementation there.
Three of six commonTest files move to a new nonWebTest source set,
for two unrelated, both-confirmed-empirically reasons:
SharedRadioInterfaceServiceLivenessTest.kt depends on core:testing (no
wasmJs target, the same gap every KMP module's test suite has hit this
session); RadioControllerImplTest.kt and RadioControllerRestoreTest.kt
crash the Kotlin/Wasm compiler ("Serialization of IrErrorType is not
supported anymore") when constructing a real RadioControllerImpl --
bisected to exactly these two files, which both differ from the four
that pass by constructing that class (interface delegation via `by`
plus Lazy<T> constructor params) -- a genuine backend limitation, not
a library gap, logged as deferred rather than worked around.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
core:data ties together core:database, core:datastore, core:di,
core:model, core:network, core:prefs, core:repository (all already
wasmJs-enabled), core:takserver (out of scope, relocated below), and
Room -- the last of which turned out to be the real blocker.
core:takserver's dependency moves to nonWebMain-only: a grep pass
confirmed core:data's own commonMain never actually references
org.meshtastic.tak (the only "TAK" substring matches were the
unrelated PortNum.ATAK_PLUGIN protobuf constant and a code comment),
so this is a mechanical relocation, not a port -- core:takserver
itself stays out of scope (niche ATAK feature).
MqttManagerImpl.kt duplicated the exact TCP+WS transport composition
core:network's MqttTransportSelection.kt already solved for
MQTTRepositoryImpl. Rather than duplicate the platform-selection
logic (and its TCP-exclusion reasoning) a second time, core:network's
mqttTransportFactory drops its `internal` modifier and becomes public;
MqttManagerImpl.kt's one-off probe client now calls it directly.
The real, unpredicted blocker: core:database exposes Room only as
`implementation` in its own commonMain, so RoomDatabase -- the
supertype of MeshtasticDatabase that core:data's repositories reach
through DatabaseProvider -- isn't visible to external consumers on
wasmJs. This never surfaced on android/jvm/iOS because core:data's
own jvmAndroidMain already carries the identical redundant Room
redeclaration, for the same reason, pre-dating this session. Fixed
with a matching wasmJsMain dependency on
androidx.room.runtime/androidx.room.paging (both confirmed to publish
real wasmJs variants; sqlite-bundled stays excluded, native-only and
unrelated to RoomDatabase itself). core:database's own
implementation-vs-api Room exposure would be a cleaner long-term fix
but is out of this pass's scope since nothing else currently needs it.
14 of core:data's 33 commonTest files -- the ones that actually import
core:testing (core:testing itself has no wasmJs target, same gap
core:ble/core:database/core:network hit) -- move to a new nonWebTest
source set; confirmed empirically that core:data's own
compileTestKotlinWasmJs task genuinely needs every commonTest
dependency to resolve, even with no browser()/nodejs() test executor
configured -- this is a real compile-time constraint on every module,
not something a bare wasmJs() sidesteps by omission.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
core:datastore wraps androidx.datastore.core.DataStore<T> for five
payload types (four proto messages plus a Preferences-backed store).
androidx.datastore:datastore -- the core artifact providing
DataStore/OkioSerializer/CorruptionException, not just the
-preferences extension core:prefs already hit -- publishes no wasmJs
variant at any version, confirmed against its Gradle Module Metadata.
Introduces Store<T> (core/datastore/store/Store.kt), a direct analog
to core:prefs's PrefsStore but simpler: each store here already holds
one whole serializable value, so no key/snapshot abstraction is
needed, just data/updateData mirroring DataStore<T>'s own two
members. nonWebMain's DataStoreAdapter wraps a real DataStore<T>
unchanged; wasmJsMain's LocalStorageStore is backed by localStorage,
running each payload's Wire ADAPTER.decode/.encode against an
in-memory okio.Buffer and base64-encoding the resulting bytes
(localStorage is string-only). Corruption policy mirrors the existing
Android/JVM ReplaceFileCorruptionHandler: no value yet or a value that
fails to decode both fall back to the default, with a decode failure
also logged and the recovered default written back so it isn't
re-hit on every access.
The four proto-payload DataSources (ChannelSetDataSource and friends)
needed zero logic changes -- they already depended only on
data/updateData, not on any other DataStore<T> member.
CorePreferencesDataStore (DataStore<Preferences>) has no wasmJs
counterpart: androidx.datastore.preferences's Preferences type itself
has no wasmJs variant, so there's no way to construct a Store<Preferences>
there at all -- this is the payload type being unavailable, not a
missing adapter. It and its three consumers
(RecentAddressesDataSource/BootloaderWarningDataSource/
FirmwareRecoveryDataSource) move to nonWebMain unchanged, deferred
rather than dropped -- a future pass could rewrite them against
core:prefs's own PrefsStore instead.
Caught before shipping: hoisting androidx.datastore/
androidx.datastore.preferences to nonWebMain-only initially demoted
them from api to implementation, mirroring core:prefs's precedent --
but androidApp's google flavor (GoogleMapsDataStore.kt/
GoogleMapsPrefs.kt) imports those packages directly with no dependency
of its own, relying entirely on this module's transitive api exposure.
Kept as api at the nonWebMain/commonMain level instead; verified via a
real compileGoogleDebugKotlin + testGoogleDebugUnitTest run, not just
the fdroid flavor the first pass checked.
CoreDatastoreWasmJsModule is not registered anywhere yet -- no webApp
module exists in this repo pass, same state as core:database's
SingleDatabaseProvider and core:prefs's CorePrefsWasmJsModule.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
core:network is the largest module tackled so far in this effort:
BLE radio transport, TCP radio transport, MQTT (TCP + WebSocket
transports), USB/Serial, mDNS/NSD discovery, and HTTP data sources
for api.meshtastic.org all live here.
BLE and Mock/Replay radio transports already work unchanged --
BaseRadioTransportFactory handles BLE directly via core:ble's Web
Bluetooth actuals, and Mock/Replay are already commonMain-portable.
A new WasmJsRadioTransportFactory implements the platform seam,
failing loudly for TCP and Serial/USB addresses (no browser
equivalent for raw sockets; WebSerial/WebUSB out of scope this pass).
Raw TCP sockets are a permanent browser sandbox limitation, not a
library gap (confirmed via Ktor's own docs/KTOR-6690, recorded
earlier this session). TcpTransport/TcpRadioTransport move to
nonWebMain. MQTT's transport composition becomes a real per-platform
expect/actual (MqttTransportSelection.kt): nonWebMain composes
TCP+WebSocket as before, wasmJs registers WebSocket only --
mqtt-client-transport-ws publishes a wasmJs Gradle variant,
mqtt-client-transport-tcp does not (confirmed via Maven Central's
module metadata directly, not assumed).
New wasmJs actuals: WebNetworkMonitor (real navigator.onLine plus
window online/offline events, via kotlinx-browser -- no custom JS
interop needed), MqttTlsTrust.wasmJs.kt (null -- a browser page
cannot influence TLS trust at all, the platform decision is the only
one available), ConnectionFailures.wasmJs.kt (false -- Ktor's Js
engine surfaces failures through the same IOException family the
shared predicate already covers). CoreNetworkWasmJsModule wires a
real HttpClient(Js) engine for the HTTP data sources; deliberately no
@ComponentScan since commonMain's CoreNetworkModule scan already
reaches this target's @Single classes. Like core:database's
SingleDatabaseProvider and core:prefs's CorePrefsWasmJsModule, it's
not registered anywhere yet -- no webApp module exists this pass.
core:network's own applyHierarchyTemplate call (needed for the
nonWeb/wasmJs split) can't coexist with the separate
meshtastic.kmp.jvm.android convention plugin, which makes its own
call -- Gradle only allows one per project. Replaced with a nested
jvmAndroid group inside the custom nonWeb group; ConnectionFailures's
existing jvmAndroidMain source set needed no changes.
One small upstream fix: core:ble's classifyBleException() had no
wasmJs counterpart, blocking core:network's commonMain
BleRadioTransport.kt. Added as an honest `= null` (Kable doesn't
exist on wasmJs at all, so nothing is ever a recognized Kable
exception there) -- same disjoint-compilation shape as
BleServiceExtensions.kt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
androidx.datastore.preferences publishes no wasmJs variant at any
version, so core:prefs can no longer depend on DataStore<Preferences>
from commonMain. Introduces PrefsStore/PrefsSnapshot/PrefsKey
(core/prefs/store/PrefsStore.kt), a minimal interface mirroring
DataStore<Preferences>'s own shape closely enough that porting each
*PrefsImpl off DataStore is a mechanical key-factory rename, not a
rewrite.
Two implementations: nonWebMain's DataStorePrefsStore adapts a real
DataStore<Preferences> (android/jvm/iOS unchanged underneath), and
wasmJsMain's LocalStoragePrefsStore is backed by the browser's
localStorage directly -- synchronous and built-in, so unlike
core:database's OPFS story this needs no Worker or npm dependency.
Unlike core:database's DatabaseManager split, none of the 17
*PrefsImpl classes needed to move out of commonMain: the platform
difference is fully absorbed inside PrefsStore's two implementations,
so every impl's diff is exactly the mechanical shape promised (defaults,
migration logic, dynamic per-node keys all unchanged). PrefsDataStores.kt's
12 marker interfaces/wrappers get the same mechanical swap.
Enabling core:prefs's wasmJs target exposed a second, one-module-upstream
gap: core:repository had no wasmJs target at all (its `expect class
Location` had android/jvm/iOS actuals but no wasmJs one). Fixed with a
wasmJsMain placeholder mirroring the existing jvmMain placeholder exactly
-- desktop has no real location hardware either, so this introduces no
new product decision, just parity with the platform that already has none.
core:service's MeshLogCleanupWorkerTest fake DataStore<Preferences> is
updated to fake PrefsStore/PrefsSnapshot instead, following the interface
change through its one external consumer.
CorePrefsWasmJsModule (new Koin module, localStorage-backed) is not yet
registered anywhere -- no webApp module exists yet to wire it into, same
as core:database's SingleDatabaseProvider precedent.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Room 3.0.2 + androidx.sqlite:sqlite-web:2.7.0's WebWorkerSQLiteDriver, backed by real
SQLite compiled to WASM (@sqlite.org/sqlite-wasm) running in a dedicated Web Worker
(core/database/worker/{package.json,worker.js}), persisting via OPFS -- same shape
already proven end-to-end this session (insert survives a full page reload) against
the upstream danysantiago/room-web-demo reference. Uses the same nonWebMain
hierarchy-split pattern already proven for core:ble.
DatabaseManager (multi-device switching, legacy-DB migration, cross-transport merge)
is built entirely on DatabaseDataStore (a DataStore<Preferences> wrapper), and
androidx.datastore.preferences has zero wasmJs variant at any published version --
not a temporary gap, the Preferences type itself doesn't resolve for that target. That
whole orchestrator is out of scope for a web client (no BLE/USB device-switching story
exists there), so it moves to nonWebMain unchanged, alongside DatabaseDataStore and its
Koin module. wasmJs gets a new SingleDatabaseProvider: one real, persistent,
non-switching MeshtasticDatabase, nothing to switch between.
BusyTimeoutSQLiteDriver turned out not to be platform-neutral as originally assumed:
androidx.sqlite.SQLiteDriver.open() is suspend on androidx.sqlite's own webMain branch,
non-suspend everywhere else (confirmed from androidx.sqlite:sqlite:2.7.0's own
sources) -- split into two independent, identically-shaped classes rather than one
shared implementation, since the suspend-modifier difference means actual can't
paper over it (same "disjoint compilations" shape as core:ble's
BleServiceExtensions.kt).
getInMemoryDatabaseBuilder()/getDatabaseDirectory()/deleteDatabase()/getFileSystem()
fail loudly or return best-effort placeholders on wasmJs -- confirmed via grep that
their only real caller, DatabaseManager, lives entirely in nonWebMain now, so nothing
wasmJs-reachable needs them to do anything real. Same "fail loudly, don't silently
return wrong data" precedent as core:resources' getString().
androidApp/desktopApp needed a small, necessary addition: registering the new
CoreDatabaseNonWebModule alongside the existing CoreDatabaseModule, since without it
neither platform would have a DatabaseDataStore Koin binding anymore.
Verified independently twice: compileKotlinWasmJs/compileTestKotlinWasmJs pass;
full regression (android/jvm/iOS compiles, allTests -- with the ~21 moved
DatabaseManager*/DAO tests confirmed actually executing via real test-report output,
not just exit code) passes; androidApp and desktopApp both compile clean with the new
DI wiring; detekt/spotlessCheck clean across core:database and both app modules. No
real browser round-trip test was run against this exact worker.js (accepted as a
stretch goal, not required for this pass).
No expect declarations; navigation3-ui:1.1.1 confirmed to publish a wasmJs variant.
Verified via gradle-runner: compileKotlinWasmJs, jvm regression, allTests, detekt,
spotlessCheck all pass.
WebBleDevice/WebBleService were public classes exposing internal JS-interop types
(JsBluetoothDevice, JsBluetoothRemoteGATTService, JsBluetoothRemoteGATTCharacteristic) in
their constructors/companion function -- Kotlin's visibility checker correctly rejects a
public declaration leaking an internal type. Made both classes internal, matching the
documented intent that only the BleDevice/BleService contracts should ever be visible
outside this file.
Also upgraded WebBluetoothApi.kt's byte-conversion helpers from hand-rolled per-byte js()
loops to kotlinx-browser (org.jetbrains.kotlinx:kotlinx-browser:0.5.0, confirmed to publish
a wasmJs variant) -- the real, documented Kotlin/Wasm typed-array interop library, per its
own official docs. DataView still needs one small js() snippet to view it as an Int8Array
(kotlinx-browser has no DataView-specific conversion); everything past that point goes
through the library's real toByteArray()/toInt8Array() extensions instead of a manual
per-index loop.
core:ble:compileKotlinWasmJs now passes -- this code has never compiled before. Verified
via gradle-runner: zero errors (98 expected ExperimentalWasmJsInterop opt-in warnings),
full regression (android/jvm/iOS compiles, allTests, detekt, spotlessCheck) passes across
all five touched modules (core:ble, core:common, core:di, core:model, core:resources).
core:di has no expect declarations, just the target. core:resources' getString()
(blocking, runBlocking-based) can't exist on wasmJs -- split to expect/actual: a real
nonWebMain actual (android/jvm/iOS, same hierarchy-split pattern as core:ble) and a
wasmJsMain actual that fails loudly (there is no way to synchronously block on wasmJs;
callers on web must use the already-suspend getStringSuspend() instead). Renamed the
nonWebMain actual file to GetStringActuals.kt to avoid a JVM facade-class name collision
with commonMain's GetString.kt (both would otherwise generate an identical GetStringKt
class).
core:model's two expect declarations (getShortDateTime, platformRandomBytes) get wasmJs
actuals backed by Intl.DateTimeFormat and the Web Crypto API's getRandomValues() --
platformRandomBytes backs channel PSK/private-key generation, so it must be
cryptographically secure, never Math.random(). Third-party dependencies (takpacket-sdk
0.9.1, compose components-resources 1.12.0) confirmed to publish a wasmJs variant via
their real Gradle Module Metadata before enabling the target.
Verified via gradle-runner: compileKotlinWasmJs passes for all three, full regression
(android/jvm/iOS compiles, allTests, detekt, spotlessCheck) passes across every touched
module.
BuildUtils, Dispatchers, NetworkUtils, DecimalFormatting, LocalizedUnitFormatting,
DateFormatter, MeasurementSystem -- backed by the browser's Intl API (NumberFormat,
DateTimeFormat, RelativeTimeFormat) and navigator.language, via a single interop layer
(JsIntlApi.kt) isolating every external/js() declaration from the plain-Kotlin actuals
above it, mirroring core:ble's WebBluetoothApi.kt discipline.
Uses the current documented Kotlin/Wasm JS interop mechanism (external fun/class/interface
extending JsAny, primitive Kotlin types crossing directly as parameters) -- not @JsFun,
which JetBrains' own docs mark as being deprecated.
Honest, documented platform gaps rather than silent approximations: no browser API exists
for a measurement-system or temperature-unit override the way Android 14+ exposes one, so
region-based inference (reusing core:common's own measurementSystemForRegion) is the best
available signal, same as the JVM/desktop actual's own fallback below Android API 28.
Verified via gradle-runner: compileKotlinWasmJs + compileTestKotlinWasmJs PASS; full
regression on core:common and core:ble (jvm/android/iOS compiles, allTests, detekt,
spotlessCheck) all PASS -- no regressions from adding this target.
Now that org.gradle.isolated-projects is off repo-wide, wasmJs() no longer needs to be
absent from the default configuration graph -- it sits alongside android/jvm/iOS as an
unconditional target on both modules, the same way those targets are registered.
Both modules still fail to compile for wasmJs (expected, tracked separately): core:common
has 15 expect declarations with no wasmJsMain actual yet; core:ble additionally can't
resolve core:di/core:model, neither of which have a wasmJs target registered yet. Verified
these are ordinary missing-actual / dependency-resolution errors, not Isolated Projects
violations -- the failure mode this change exists to fix is confirmed absent.
org.gradle.isolated-projects=true (this repo's prior default) makes registering wasmJs()
on more than one Gradle project impossible: a Kotlin Gradle Plugin-internal cross-project
build service (IncrementalModuleInfoBuildService) violates Isolated Projects the moment a
second project carries the target, independent of anything module-specific and not
fixable per-module. Confirmed via real builds this session. JetBrains' own docs say JS/Wasm
+ Isolated Projects isn't supported yet, with no ETA.
Trade-off, explicit: this gives up whatever build-speed/IDE-sync benefit Isolated Projects
was providing repo-wide, not just for wasmJs work, in exchange for wasmJs being usable at
all. Verified via gradle-runner: configuration now succeeds with two projects (core:ble,
core:common) both carrying an unconditional wasmJs() target, with existing android/jvm/iOS
targets on both modules unaffected.
Same pattern as core/ble: absent from the configuration graph by default (repo's
org.gradle.isolated-projects=true forbids a second project registering wasmJs without
this gate, per the earlier finding on the web-target branch). Unlike core/ble, core:common
has no native-only dependency, so no source-set hierarchy split is needed here -- just the
target plus a wasmJsMain actual for each existing expect (next commit).
Root-caused via KT-80409 (open JetBrains bug): com.android.kotlin.multiplatform.library
(applied by this repo's KmpLibraryConventionPlugin) silently fails to attach androidMain
to a custom hierarchy group via the withAndroidTarget() named helper -- androidMain stays
wired directly to commonMain instead. A KGP engineer's own workaround is to drop to the
lower-level withCompilations predicate, which this reverts to (the form already proven to
work for android/jvm in an earlier pass, before this file was changed to the broken
withAndroidTarget()/withApple() form).
That predicate alone only reaches the two leaf iOS compilations, not the shared iosMain
intermediate the default hierarchy template already creates (where NoopStubs.kt's actuals
live) -- confirmed via a printSourceSetHierarchy dump showing iosMain dependsOn [appleMain]
only, no edge to nonWebMain. Added an explicit sourceSets.getByName("iosMain") { dependsOn
(nonWebMain) } edge for this, the same "drop to a lower-level explicit construct" pattern
as the Android workaround.
Verification of this exact fix is pending -- committing immediately (before any further
verification) after a subagent silently discarded the previous, unverified attempt at this
same fix via an unreviewed git checkout/restore, with zero trace in reflog or stash.
Adds a best-effort, UNCOMPILED wasmJsMain implementation for core/ble (WebBluetoothApi,
WebBleService, WebBleConnection, WebBleScanner, WebBleConnectionFactory,
WebBluetoothRepository, WebMeshtasticRadioProfile, WebBleDevice, di/CoreBleWasmJsModule) —
reviewable, not verified. wasmJs registration is gated behind -Pmeshtastic.web=true and
absent from the default build graph.
Also attempts to fix a real regression discovered this pass: the nonWebMain hierarchy
split (originally committed in 3399ffd02, never actually clean-verified due to earlier,
unrelated config errors blocking every prior verification attempt) breaks
:core:ble:compileKotlinIosArm64/IosSimulatorArm64 on a completely plain build, no
special property needed. This commit's attempted fix (nesting withJvm()/
withAndroidTarget()/withApple() under the custom group instead of a withCompilations
predicate) does NOT resolve it — verified via a real gradle-runner pass. iOS compilation
of core:ble is broken on this branch as of this commit.
Full diagnosis, verbatim errors, and the three-option decision this needs from a human
are in .agent_plans/web-target-workpad.md's [REGRESSION] and [BLOCKER] sections. Separately
and more severely: registering wasmJs() on a second Gradle project in this repo triggers an
Isolated Projects violation via Kotlin Gradle Plugin's own cross-project
IncrementalModuleInfoBuildService, independent of anything module-specific — and core:ble's
wasmJs classpath requires core:common/core:di/core:model to also carry a wasmJs variant, so
a single-module wasmJs target is definitionally impossible here. Under this repo's
org.gradle.isolated-projects=true, a wasmJs target may not be achievable at all without a
policy change or an upstream Kotlin/Gradle fix.
Prepares core/ble for a wasmJs target: Kable has no JS/wasmJs artifact, so every
file that depended on it (KablePlatformSetup, KableBleConnection, KableBleScanner,
KableMeshtasticRadioProfile, KableBleConnectionFactory, KableStateMapping,
BleLoggingConfig, BleExceptionClassifier, ActiveBleConnection, MeshtasticBleDevice,
KermitLogEngine, di/CoreBleModule) moves into a new nativeMain intermediate source
set shared by android/jvm/ios, via a new build-logic opt-in
(meshtasticKmpTargets { web.set(true); hoistNativeOnlyDependencies.set(true) }).
commonMain keeps only the platform-neutral contracts (BleConnection, BleDevice,
BluetoothRepository, MeshtasticBleConstants, MeshtasticRadioProfile, etc.).
No wasmJs actuals yet — this commit is deliberately isolated so the existing
android/jvm/ios targets can be verified unaffected before any wasmJs-specific code
is written.