From c4b853c364357d58423319ca2fc1407625d208ed Mon Sep 17 00:00:00 2001
From: Kevin Cianfarini <kevin.cianfarini@octoenergy.com>
Date: Sat, 9 Mar 2024 18:12:41 -0500
Subject: [PATCH] Add support for Kotlin/JS targets (#54)

---
 .github/workflows/release.yml                 |   2 +-
 .github/workflows/test.yml                    |   2 +-
 build.gradle.kts                              |  11 +
 compose/build.gradle.kts                      |   9 +
 core/build.gradle.kts                         |  11 +-
 ...nMemoryFeatureFlagDataStoreOverrideTest.kt |   8 +-
 .../monarch/MixinFeatureFlagManagerTest.kt    |  20 +-
 .../ObservableMixinFeatureFlagManagerTest.kt  |  42 +-
 gradle/libs.versions.toml                     |   2 +
 .../environment-variable/build.gradle.kts     |  23 +
 .../monarch/environment/environment.js.kt     |   7 +
 integrations/launch-darkly/build.gradle.kts   |   2 +-
 kotlin-js-store/yarn.lock                     | 554 ++++++++++++++++++
 .../build.gradle.kts                          |   9 +
 .../mixins/JsonFeatureFlagManagerMixinTest.kt |   6 +-
 .../monarch/mixins/JsonFeatureFlagTest.kt     |   2 +-
 test/build.gradle.kts                         |   9 +
 .../monarch/FakeFeatureFlagManagerTest.kt     |   8 +-
 18 files changed, 680 insertions(+), 47 deletions(-)
 create mode 100644 integrations/environment-variable/src/jsMain/kotlin/io/github/kevincianfarini/monarch/environment/environment.js.kt
 create mode 100644 kotlin-js-store/yarn.lock

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 505cb57..cfea9df 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -17,7 +17,7 @@ jobs:
       - uses: actions/setup-java@v4.0.0
         with:
           distribution: 'zulu'
-          java-version: 19
+          java-version: 17
 
       - name: Build and publish artifacts
         env:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index ded6d23..9039d40 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -19,7 +19,7 @@ jobs:
       - uses: actions/setup-java@v4.0.0
         with:
           distribution: 'zulu'
-          java-version: 19
+          java-version: 17
 
       - run: ./gradlew -p . ${{matrix.job}}
 
diff --git a/build.gradle.kts b/build.gradle.kts
index 6bd5df5..9bd53d5 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.gradle.api.tasks.testing.logging.TestLogEvent
+
 plugins {
     alias(libs.plugins.android.library) apply false
     alias(libs.plugins.dokka)
@@ -32,4 +34,13 @@ subprojects {
             if (jvmVersion.isPresent) jvmTarget = jvmVersion.get()
         }
     }
+
+    tasks.withType<AbstractTestTask> {
+        testLogging {
+            exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
+            showStandardStreams = true
+            showStackTraces = true
+            events(TestLogEvent.FAILED, TestLogEvent.PASSED, TestLogEvent.SKIPPED)
+        }
+    }
 }
diff --git a/compose/build.gradle.kts b/compose/build.gradle.kts
index dd5ca63..4c13f09 100644
--- a/compose/build.gradle.kts
+++ b/compose/build.gradle.kts
@@ -13,6 +13,15 @@ kotlin {
     iosSimulatorArm64()
     iosX64()
     jvm()
+    js {
+        nodejs {
+            testTask {
+                useMocha {
+                    timeout = "5s"
+                }
+            }
+        }
+    }
     linuxArm64()
     linuxX64()
     macosArm64()
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index 50f9635..1488bef 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -12,6 +12,15 @@ kotlin {
     iosSimulatorArm64()
     iosX64()
     jvm()
+    js {
+        nodejs {
+            testTask {
+                useMocha {
+                    timeout = "5s"
+                }
+            }
+        }
+    }
     linuxArm64()
     linuxX64()
     macosArm64()
@@ -32,7 +41,7 @@ kotlin {
         }
         commonTest.dependencies {
             implementation(libs.kotlin.test)
-            implementation(libs.kotlinx.coroutines.core)
+            implementation(libs.kotlinx.coroutines.test)
             implementation(libs.turbine)
             implementation(project(":test"))
         }
diff --git a/core/src/commonTest/kotlin/io/github/kevincianfarini/monarch/InMemoryFeatureFlagDataStoreOverrideTest.kt b/core/src/commonTest/kotlin/io/github/kevincianfarini/monarch/InMemoryFeatureFlagDataStoreOverrideTest.kt
index 44debf2..3bebfb7 100644
--- a/core/src/commonTest/kotlin/io/github/kevincianfarini/monarch/InMemoryFeatureFlagDataStoreOverrideTest.kt
+++ b/core/src/commonTest/kotlin/io/github/kevincianfarini/monarch/InMemoryFeatureFlagDataStoreOverrideTest.kt
@@ -3,7 +3,7 @@ package io.github.kevincianfarini.monarch
 import app.cash.turbine.test
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
 import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertNotEquals
@@ -80,7 +80,7 @@ class InMemoryFeatureFlagDataStoreOverrideTest {
         overrideValue: Any,
         delegateValue: Any,
         produceFlow: InMemoryFeatureFlagDataStoreOverride.(String) -> Flow<*>
-    ) = runBlocking {
+    ) = runTest {
         val key = "foo"
         val delegate = InMemoryFeatureFlagDataStore().apply { setValue(key, delegateValue) }
         val storeOverride = storeOverride(
@@ -110,7 +110,7 @@ class InMemoryFeatureFlagDataStoreOverrideTest {
     private fun storeCacheFallsBackToDelegateFlowParameterized(
         delegateValue: Any,
         produceFlow: InMemoryFeatureFlagDataStoreOverride.(String) -> Flow<*>
-    ) = runBlocking {
+    ) = runTest {
         val key = "foo"
         val delegate = InMemoryFeatureFlagDataStore().apply { setValue(key, delegateValue) }
         val storeOverride = storeOverride(delegate = delegate)
@@ -138,7 +138,7 @@ class InMemoryFeatureFlagDataStoreOverrideTest {
         initialValue: Any,
         setNewValue: InMemoryFeatureFlagDataStoreOverride.(String) -> Unit,
         produceFlow: InMemoryFeatureFlagDataStoreOverride.(String) -> Flow<*>
-    ) = runBlocking {
+    ) = runTest {
         val key = "foo"
         val storeOverride = storeOverride(initialOverrides = mapOf(key to initialValue))
         storeOverride.produceFlow(key).test {
diff --git a/core/src/commonTest/kotlin/io/github/kevincianfarini/monarch/MixinFeatureFlagManagerTest.kt b/core/src/commonTest/kotlin/io/github/kevincianfarini/monarch/MixinFeatureFlagManagerTest.kt
index a77986b..cd57146 100644
--- a/core/src/commonTest/kotlin/io/github/kevincianfarini/monarch/MixinFeatureFlagManagerTest.kt
+++ b/core/src/commonTest/kotlin/io/github/kevincianfarini/monarch/MixinFeatureFlagManagerTest.kt
@@ -4,7 +4,7 @@ import kotlin.test.*
 
 class MixinFeatureFlagManagerTest {
 
-    @Test fun `manager gets string value`() {
+    @Test fun manager_gets_string_value() {
         val store = InMemoryFeatureFlagDataStore().apply { setValue("foo", "bar") }
         assertEquals(
             expected = "bar",
@@ -12,19 +12,19 @@ class MixinFeatureFlagManagerTest {
         )
     }
 
-    @Test fun `manager gets default string value`() = assertEquals(
+    @Test fun manager_gets_default_string_value() = assertEquals(
         expected = "blah",
         actual = manager().currentValueOf(StringFeature),
     )
 
-    @Test fun `manager gets boolean value`() {
+    @Test fun manager_gets_boolean_value() {
         val store = InMemoryFeatureFlagDataStore().apply { setValue("bool", true) }
         assertTrue(manager(store).currentValueOf(BooleanFeature))
     }
 
-    @Test fun `manager gets default boolean value`() = assertFalse(manager().currentValueOf(BooleanFeature))
+    @Test fun manager_gets_default_boolean_value() = assertFalse(manager().currentValueOf(BooleanFeature))
 
-    @Test fun `manager gets double value`() {
+    @Test fun manager_gets_double_value() {
         val store = InMemoryFeatureFlagDataStore().apply { setValue("double", 15.7) }
         assertEquals(
             expected = 15.7,
@@ -33,13 +33,13 @@ class MixinFeatureFlagManagerTest {
         )
     }
 
-    @Test fun `manager gets default double value`() = assertEquals(
+    @Test fun manager_gets_default_double_value() = assertEquals(
         expected = 1.5,
         actual = manager().currentValueOf(DoubleFeature),
         absoluteTolerance = 0.05,
     )
 
-    @Test fun `manager gets long value`() {
+    @Test fun manager_gets_long_value() {
         val store = InMemoryFeatureFlagDataStore().apply { setValue("long", 27L) }
         assertEquals(
             expected = 27L,
@@ -47,12 +47,12 @@ class MixinFeatureFlagManagerTest {
         )
     }
 
-    @Test fun `manager gets default long value`() = assertEquals(
+    @Test fun manager_gets_default_long_value() = assertEquals(
         expected = 1027L,
         actual = manager().currentValueOf(LongFeature),
     )
 
-    @Test fun `manager gets mixin value`() {
+    @Test fun manager_gets_mixin_value() {
         val store = InMemoryFeatureFlagDataStore().apply { setValue("some_int", "1") }
         assertEquals(
             expected = 1,
@@ -60,7 +60,7 @@ class MixinFeatureFlagManagerTest {
         )
     }
 
-    @Test fun `manager errors with unrecognized flag type`() {
+    @Test fun manager_errors_with_unrecognized_flag_type() {
         // the below IS NOT a `BooleanOption` and therefore will go unrecognized
         val someRandomFlag = object : FeatureFlag<Boolean> {
             override val key: String = "random_key"
diff --git a/core/src/commonTest/kotlin/io/github/kevincianfarini/monarch/ObservableMixinFeatureFlagManagerTest.kt b/core/src/commonTest/kotlin/io/github/kevincianfarini/monarch/ObservableMixinFeatureFlagManagerTest.kt
index 9338f28..71819b3 100644
--- a/core/src/commonTest/kotlin/io/github/kevincianfarini/monarch/ObservableMixinFeatureFlagManagerTest.kt
+++ b/core/src/commonTest/kotlin/io/github/kevincianfarini/monarch/ObservableMixinFeatureFlagManagerTest.kt
@@ -1,13 +1,13 @@
 package io.github.kevincianfarini.monarch
 
 import app.cash.turbine.test
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
 import kotlin.test.*
 
 class ObservableMixinFeatureFlagManagerTest {
 
-    @Test fun `manager gets string value`() {
-        runBlocking {
+    @Test fun manager_gets_string_value() {
+        runTest {
             val store = InMemoryFeatureFlagDataStore().apply { setValue("foo", "bar") }
             manager(store).valuesOf(StringFeature).test {
                 assertEquals("bar", awaitItem())
@@ -16,8 +16,8 @@ class ObservableMixinFeatureFlagManagerTest {
         }
     }
 
-    @Test fun `manager gets default string value`() {
-        runBlocking {
+    @Test fun manager_gets_default_string_value() {
+        runTest {
             manager().valuesOf(StringFeature).test {
                 assertEquals("blah", awaitItem())
                 cancelAndIgnoreRemainingEvents()
@@ -25,8 +25,8 @@ class ObservableMixinFeatureFlagManagerTest {
         }
     }
 
-    @Test fun `manager gets boolean value`() {
-        runBlocking {
+    @Test fun manager_gets_boolean_value() {
+        runTest {
             val store = InMemoryFeatureFlagDataStore().apply { setValue("bool", true) }
             manager(store).valuesOf(BooleanFeature).test {
                 assertTrue(awaitItem())
@@ -35,8 +35,8 @@ class ObservableMixinFeatureFlagManagerTest {
         }
     }
 
-    @Test fun `manager gets default boolean value`() {
-        runBlocking {
+    @Test fun manager_gets_default_boolean_value() {
+        runTest {
             manager().valuesOf(BooleanFeature).test {
                 assertFalse(awaitItem())
                 cancelAndIgnoreRemainingEvents()
@@ -44,8 +44,8 @@ class ObservableMixinFeatureFlagManagerTest {
         }
     }
 
-    @Test fun `manager gets double value`() {
-        runBlocking {
+    @Test fun manager_gets_double_value() {
+        runTest {
             val store = InMemoryFeatureFlagDataStore().apply { setValue("double", 15.7) }
             manager(store).valuesOf(DoubleFeature).test {
                 assertEquals(expected = 15.7, actual = awaitItem(), absoluteTolerance = 0.05)
@@ -54,8 +54,8 @@ class ObservableMixinFeatureFlagManagerTest {
         }
     }
 
-    @Test fun `manager gets default double value`() {
-        runBlocking {
+    @Test fun manager_gets_default_double_value() {
+        runTest {
             manager().valuesOf(DoubleFeature).test {
                 assertEquals(expected = 1.5, actual = awaitItem(), absoluteTolerance = 0.05)
                 cancelAndIgnoreRemainingEvents()
@@ -63,8 +63,8 @@ class ObservableMixinFeatureFlagManagerTest {
         }
     }
 
-    @Test fun `manager gets long value`() {
-        runBlocking {
+    @Test fun manager_gets_long_value() {
+        runTest {
             val store = InMemoryFeatureFlagDataStore().apply { setValue("long", 27L) }
             manager(store).valuesOf(LongFeature).test {
                 assertEquals(expected = 27L, actual = awaitItem())
@@ -73,8 +73,8 @@ class ObservableMixinFeatureFlagManagerTest {
         }
     }
 
-    @Test fun `manager gets default long value`() {
-        runBlocking {
+    @Test fun manager_gets_default_long_value() {
+        runTest {
             manager().valuesOf(LongFeature).test {
                 assertEquals(expected = 1027L, actual = awaitItem())
                 cancelAndIgnoreRemainingEvents()
@@ -82,8 +82,8 @@ class ObservableMixinFeatureFlagManagerTest {
         }
     }
 
-    @Test fun `manager gets mixin value`() {
-        runBlocking {
+    @Test fun manager_gets_mixin_value() {
+        runTest {
             val store = InMemoryFeatureFlagDataStore().apply { setValue("some_int", "1") }
             manager(store, listOf(ObservableIntDecodingMixin)).valuesOf(IntFeatureFlag).test {
                 assertEquals(expected = 1, actual = awaitItem())
@@ -92,8 +92,8 @@ class ObservableMixinFeatureFlagManagerTest {
         }
     }
 
-    @Test fun `manager errors with unrecognized flag type`() {
-        runBlocking {
+    @Test fun manager_errors_with_unrecognized_flag_type() {
+        runTest {
             // the below IS NOT a `BooleanOption` and therefore will go unrecognized
             val someRandomFlag = object : FeatureFlag<Boolean> {
                 override val key: String = "random_key"
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index be03cbc..e1b58d4 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -5,6 +5,7 @@ android-minSdk = "21"
 compose = "1.6.0"
 dokka = "1.9.10"
 kotlin = "1.9.22"
+kotlin-nodejs = "18.16.12-pre.634"
 kotlinx-coroutines = "1.8.0"
 kotlinx-serialization = "1.6.2"
 launchdarkly-android = "5.0.0"
@@ -13,6 +14,7 @@ turbine = "1.0.0"
 
 [libraries]
 compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "compose" }
+kotlin-nodejs = { module = "org.jetbrains.kotlin-wrappers:kotlin-node", version.ref = "kotlin-nodejs" }
 kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
 kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
 kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
diff --git a/integrations/environment-variable/build.gradle.kts b/integrations/environment-variable/build.gradle.kts
index 0f9b629..b1c7566 100644
--- a/integrations/environment-variable/build.gradle.kts
+++ b/integrations/environment-variable/build.gradle.kts
@@ -8,12 +8,32 @@ kotlin {
 
     explicitApi()
 
+    iosArm64()
+    iosSimulatorArm64()
+    iosX64()
     jvm()
+    js {
+        nodejs {
+            testTask {
+                useMocha {
+                    timeout = "5s"
+                }
+            }
+        }
+    }
     linuxArm64()
     linuxX64()
     macosArm64()
     macosX64()
     mingwX64()
+    tvosArm64()
+    tvosSimulatorArm64()
+    tvosX64()
+    watchosArm32()
+    watchosArm64()
+    watchosDeviceArm64()
+    watchosSimulatorArm64()
+    watchosX64()
 
     sourceSets {
         commonMain.dependencies {
@@ -22,5 +42,8 @@ kotlin {
         commonTest.dependencies {
             implementation(libs.kotlin.test)
         }
+        jsMain.dependencies {
+            implementation(libs.kotlin.nodejs)
+        }
     }
 }
\ No newline at end of file
diff --git a/integrations/environment-variable/src/jsMain/kotlin/io/github/kevincianfarini/monarch/environment/environment.js.kt b/integrations/environment-variable/src/jsMain/kotlin/io/github/kevincianfarini/monarch/environment/environment.js.kt
new file mode 100644
index 0000000..067704c
--- /dev/null
+++ b/integrations/environment-variable/src/jsMain/kotlin/io/github/kevincianfarini/monarch/environment/environment.js.kt
@@ -0,0 +1,7 @@
+package io.github.kevincianfarini.monarch.environment
+
+import node.process.process
+
+internal actual fun getSystemEnvVar(key: String): String? {
+    return process.env[key]
+}
\ No newline at end of file
diff --git a/integrations/launch-darkly/build.gradle.kts b/integrations/launch-darkly/build.gradle.kts
index 2c89b9f..a5ffde0 100644
--- a/integrations/launch-darkly/build.gradle.kts
+++ b/integrations/launch-darkly/build.gradle.kts
@@ -19,7 +19,7 @@ android {
 
 kotlin {
     explicitApi()
-    jvmToolchain(19)
+    jvmToolchain(17)
 
     iosArm64()
     iosSimulatorArm64()
diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock
new file mode 100644
index 0000000..efd0815
--- /dev/null
+++ b/kotlin-js-store/yarn.lock
@@ -0,0 +1,554 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+ansi-colors@4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
+  integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
+
+ansi-regex@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
+  integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+
+ansi-styles@^4.0.0, ansi-styles@^4.1.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+  integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+  dependencies:
+    color-convert "^2.0.1"
+
+anymatch@~3.1.2:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
+  integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
+  dependencies:
+    normalize-path "^3.0.0"
+    picomatch "^2.0.4"
+
+argparse@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
+  integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
+
+balanced-match@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
+  integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+
+binary-extensions@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
+  integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
+
+brace-expansion@^1.1.7:
+  version "1.1.11"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+  integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+  dependencies:
+    balanced-match "^1.0.0"
+    concat-map "0.0.1"
+
+brace-expansion@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
+  integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
+  dependencies:
+    balanced-match "^1.0.0"
+
+braces@~3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+  dependencies:
+    fill-range "^7.0.1"
+
+browser-stdout@1.3.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
+  integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==
+
+buffer-from@^1.0.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
+  integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
+
+camelcase@^6.0.0:
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
+  integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
+
+chalk@^4.1.0:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+  integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
+chokidar@3.5.3:
+  version "3.5.3"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
+  integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
+  dependencies:
+    anymatch "~3.1.2"
+    braces "~3.0.2"
+    glob-parent "~5.1.2"
+    is-binary-path "~2.1.0"
+    is-glob "~4.0.1"
+    normalize-path "~3.0.0"
+    readdirp "~3.6.0"
+  optionalDependencies:
+    fsevents "~2.3.2"
+
+cliui@^7.0.2:
+  version "7.0.4"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
+  integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
+  dependencies:
+    string-width "^4.2.0"
+    strip-ansi "^6.0.0"
+    wrap-ansi "^7.0.0"
+
+color-convert@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+  integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+  dependencies:
+    color-name "~1.1.4"
+
+color-name@~1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+concat-map@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+  integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
+
+debug@4.3.4:
+  version "4.3.4"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
+  integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
+  dependencies:
+    ms "2.1.2"
+
+decamelize@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837"
+  integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==
+
+diff@5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b"
+  integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==
+
+emoji-regex@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
+  integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+
+escalade@^3.1.1:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27"
+  integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==
+
+escape-string-regexp@4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
+  integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
+
+fill-range@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+  dependencies:
+    to-regex-range "^5.0.1"
+
+find-up@5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
+  integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
+  dependencies:
+    locate-path "^6.0.0"
+    path-exists "^4.0.0"
+
+flat@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241"
+  integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==
+
+format-util@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/format-util/-/format-util-1.0.5.tgz#1ffb450c8a03e7bccffe40643180918cc297d271"
+  integrity sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg==
+
+fs.realpath@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+  integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
+
+fsevents@~2.3.2:
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
+  integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+
+get-caller-file@^2.0.5:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
+  integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
+
+glob-parent@~5.1.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+  integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+  dependencies:
+    is-glob "^4.0.1"
+
+glob@7.2.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
+  integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.0.4"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
+has-flag@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+  integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+he@1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
+  integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
+
+inflight@^1.0.4:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+  integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
+  dependencies:
+    once "^1.3.0"
+    wrappy "1"
+
+inherits@2:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+is-binary-path@~2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+  integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+  dependencies:
+    binary-extensions "^2.0.0"
+
+is-extglob@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+  integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+
+is-fullwidth-code-point@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
+  integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
+
+is-glob@^4.0.1, is-glob@~4.0.1:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+  integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-number@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+is-plain-obj@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
+  integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
+
+is-unicode-supported@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
+  integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==
+
+js-yaml@4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
+  integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
+  dependencies:
+    argparse "^2.0.1"
+
+locate-path@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
+  integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
+  dependencies:
+    p-locate "^5.0.0"
+
+log-symbols@4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
+  integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==
+  dependencies:
+    chalk "^4.1.0"
+    is-unicode-supported "^0.1.0"
+
+minimatch@5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b"
+  integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==
+  dependencies:
+    brace-expansion "^2.0.1"
+
+minimatch@^3.0.4:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
+  integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
+  dependencies:
+    brace-expansion "^1.1.7"
+
+mocha@10.2.0:
+  version "10.2.0"
+  resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.2.0.tgz#1fd4a7c32ba5ac372e03a17eef435bd00e5c68b8"
+  integrity sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==
+  dependencies:
+    ansi-colors "4.1.1"
+    browser-stdout "1.3.1"
+    chokidar "3.5.3"
+    debug "4.3.4"
+    diff "5.0.0"
+    escape-string-regexp "4.0.0"
+    find-up "5.0.0"
+    glob "7.2.0"
+    he "1.2.0"
+    js-yaml "4.1.0"
+    log-symbols "4.1.0"
+    minimatch "5.0.1"
+    ms "2.1.3"
+    nanoid "3.3.3"
+    serialize-javascript "6.0.0"
+    strip-json-comments "3.1.1"
+    supports-color "8.1.1"
+    workerpool "6.2.1"
+    yargs "16.2.0"
+    yargs-parser "20.2.4"
+    yargs-unparser "2.0.0"
+
+ms@2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+ms@2.1.3:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+  integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
+nanoid@3.3.3:
+  version "3.3.3"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25"
+  integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==
+
+normalize-path@^3.0.0, normalize-path@~3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+  integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+once@^1.3.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+  integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
+  dependencies:
+    wrappy "1"
+
+p-limit@^3.0.2:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
+  integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
+  dependencies:
+    yocto-queue "^0.1.0"
+
+p-locate@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
+  integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
+  dependencies:
+    p-limit "^3.0.2"
+
+path-exists@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+  integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
+path-is-absolute@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+  integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
+
+picomatch@^2.0.4, picomatch@^2.2.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+  integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+randombytes@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
+  integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
+  dependencies:
+    safe-buffer "^5.1.0"
+
+readdirp@~3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
+  integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
+  dependencies:
+    picomatch "^2.2.1"
+
+require-directory@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+  integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
+
+safe-buffer@^5.1.0:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+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"
+
+source-map-support@0.5.21:
+  version "0.5.21"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
+  integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
+  dependencies:
+    buffer-from "^1.0.0"
+    source-map "^0.6.0"
+
+source-map@^0.6.0:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+  integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+string-width@^4.1.0, string-width@^4.2.0:
+  version "4.2.3"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+  integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+  dependencies:
+    emoji-regex "^8.0.0"
+    is-fullwidth-code-point "^3.0.0"
+    strip-ansi "^6.0.1"
+
+strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+  dependencies:
+    ansi-regex "^5.0.1"
+
+strip-json-comments@3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
+  integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
+
+supports-color@8.1.1:
+  version "8.1.1"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
+  integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
+  dependencies:
+    has-flag "^4.0.0"
+
+supports-color@^7.1.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
+  integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+  dependencies:
+    has-flag "^4.0.0"
+
+to-regex-range@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+  integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+  dependencies:
+    is-number "^7.0.0"
+
+typescript@5.0.4:
+  version "5.0.4"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b"
+  integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==
+
+workerpool@6.2.1:
+  version "6.2.1"
+  resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343"
+  integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==
+
+wrap-ansi@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+  integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+  dependencies:
+    ansi-styles "^4.0.0"
+    string-width "^4.1.0"
+    strip-ansi "^6.0.0"
+
+wrappy@1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+  integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
+
+y18n@^5.0.5:
+  version "5.0.8"
+  resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
+  integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
+
+yargs-parser@20.2.4:
+  version "20.2.4"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54"
+  integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==
+
+yargs-parser@^20.2.2:
+  version "20.2.9"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
+  integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
+
+yargs-unparser@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb"
+  integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==
+  dependencies:
+    camelcase "^6.0.0"
+    decamelize "^4.0.0"
+    flat "^5.0.2"
+    is-plain-obj "^2.1.0"
+
+yargs@16.2.0:
+  version "16.2.0"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
+  integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
+  dependencies:
+    cliui "^7.0.2"
+    escalade "^3.1.1"
+    get-caller-file "^2.0.5"
+    require-directory "^2.1.1"
+    string-width "^4.2.0"
+    y18n "^5.0.5"
+    yargs-parser "^20.2.2"
+
+yocto-queue@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
+  integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
diff --git a/mixins/kotlinx-serialization-json/build.gradle.kts b/mixins/kotlinx-serialization-json/build.gradle.kts
index 60e51d0..b92bc9f 100644
--- a/mixins/kotlinx-serialization-json/build.gradle.kts
+++ b/mixins/kotlinx-serialization-json/build.gradle.kts
@@ -12,6 +12,15 @@ kotlin {
     iosSimulatorArm64()
     iosX64()
     jvm()
+    js {
+        nodejs {
+            testTask {
+                useMocha {
+                    timeout = "5s"
+                }
+            }
+        }
+    }
     linuxArm64()
     linuxX64()
     macosArm64()
diff --git a/mixins/kotlinx-serialization-json/src/commonTest/kotlin/io/github/kevincianfarini/monarch/mixins/JsonFeatureFlagManagerMixinTest.kt b/mixins/kotlinx-serialization-json/src/commonTest/kotlin/io/github/kevincianfarini/monarch/mixins/JsonFeatureFlagManagerMixinTest.kt
index 5f0fbf1..afaf521 100644
--- a/mixins/kotlinx-serialization-json/src/commonTest/kotlin/io/github/kevincianfarini/monarch/mixins/JsonFeatureFlagManagerMixinTest.kt
+++ b/mixins/kotlinx-serialization-json/src/commonTest/kotlin/io/github/kevincianfarini/monarch/mixins/JsonFeatureFlagManagerMixinTest.kt
@@ -9,14 +9,14 @@ import kotlin.test.assertNull
 
 class JsonFeatureFlagManagerMixinTest {
 
-    @Test fun `returns null on unhandled feature flag`() = assertNull(
+    @Test fun returns_null_on_unhandled_feature_flag() = assertNull(
         mixin().currentValueOfOrNull(
             flag = NotJson,
             store = InMemoryFeatureFlagDataStore(),
         )
     )
 
-    @Test fun `returns default on null value result`() = assertEquals(
+    @Test fun returns_default_on_null_value_result() = assertEquals(
         expected = SomeJsonFlag.default,
         actual = mixin().currentValueOfOrNull(
             flag = SomeJsonFlag,
@@ -24,7 +24,7 @@ class JsonFeatureFlagManagerMixinTest {
         )
     )
 
-    @Test fun `returns deserialized value`() {
+    @Test fun returns_deserialized_value() {
         val store = InMemoryFeatureFlagDataStore().apply {
             setValue(
                 key = SomeJsonFlag.key,
diff --git a/mixins/kotlinx-serialization-json/src/commonTest/kotlin/io/github/kevincianfarini/monarch/mixins/JsonFeatureFlagTest.kt b/mixins/kotlinx-serialization-json/src/commonTest/kotlin/io/github/kevincianfarini/monarch/mixins/JsonFeatureFlagTest.kt
index 5381841..604fdef 100644
--- a/mixins/kotlinx-serialization-json/src/commonTest/kotlin/io/github/kevincianfarini/monarch/mixins/JsonFeatureFlagTest.kt
+++ b/mixins/kotlinx-serialization-json/src/commonTest/kotlin/io/github/kevincianfarini/monarch/mixins/JsonFeatureFlagTest.kt
@@ -7,7 +7,7 @@ import kotlin.test.assertEquals
 
 class JsonFeatureFlagTest {
 
-    @Test fun `deserializes with supplied serializer`() {
+    @Test fun deserializes_with_supplied_serializer() {
         val jsonString = """{"bar":2}"""
         assertEquals(
             expected = Foo(2),
diff --git a/test/build.gradle.kts b/test/build.gradle.kts
index 5479209..69b8b81 100644
--- a/test/build.gradle.kts
+++ b/test/build.gradle.kts
@@ -12,6 +12,15 @@ kotlin {
     iosSimulatorArm64()
     iosX64()
     jvm()
+    js {
+        nodejs {
+            testTask {
+                useMocha {
+                    timeout = "5s"
+                }
+            }
+        }
+    }
     linuxArm64()
     linuxX64()
     macosArm64()
diff --git a/test/src/commonTest/kotlin/io/github/kevincianfarini/monarch/FakeFeatureFlagManagerTest.kt b/test/src/commonTest/kotlin/io/github/kevincianfarini/monarch/FakeFeatureFlagManagerTest.kt
index 70ff820..853c771 100644
--- a/test/src/commonTest/kotlin/io/github/kevincianfarini/monarch/FakeFeatureFlagManagerTest.kt
+++ b/test/src/commonTest/kotlin/io/github/kevincianfarini/monarch/FakeFeatureFlagManagerTest.kt
@@ -8,7 +8,7 @@ import kotlinx.coroutines.test.runTest
 
 class FakeFeatureFlagManagerTest {
 
-    @Test fun `returns default value`() {
+    @Test fun returns_default_value() {
         runTest {
             assertEquals(
                 expected = SomeFlag.default,
@@ -17,7 +17,7 @@ class FakeFeatureFlagManagerTest {
         }
     }
 
-    @Test fun `returns explicitly set value`() {
+    @Test fun returns_explicitly_set_value() {
         runTest {
             val manager = InMemoryFeatureFlagManager().apply {
                 setCurrentValueOf(SomeFlag, 1L)
@@ -29,7 +29,7 @@ class FakeFeatureFlagManagerTest {
         }
     }
 
-    @Test fun `observing returns default value`() {
+    @Test fun observing_returns_default_value() {
         runTest {
             assertEquals(
                 expected = SomeFlag.default,
@@ -40,7 +40,7 @@ class FakeFeatureFlagManagerTest {
         }
     }
 
-    @Test fun `observing emits updates to flags`() {
+    @Test fun observing_emits_updates_to_flags() {
         runTest {
             val manager = InMemoryFeatureFlagManager()
             manager.valuesOf(SomeFlag).test {