Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0) [![Download](https://api.bintray.com/packages/icerockdev/moko/moko-javascript/images/download.svg) ](https://bintray.com/icerockdev/moko/moko-javascript/_latestVersion) ![kotlin-version](https://img.shields.io/badge/kotlin-1.4.32-orange)
[![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0) [![Download](https://img.shields.io/maven-central/v/dev.icerock.moko/javascript) ](https://repo1.maven.org/maven2/dev/icerock/moko/javascript) ![kotlin-version](https://img.shields.io/badge/kotlin-1.4.32-orange)

# Mobile Kotlin javascript
This is a Kotlin MultiPlatform library that ...
This is a Kotlin MultiPlatform library that allows you to run JavaScript code from common Kotlin code

## Table of Contents
- [Features](#features)
Expand All @@ -14,7 +14,8 @@ This is a Kotlin MultiPlatform library that ...
- [License](#license)

## Features
...
- Evaluate JavaScript code from Kotlin common code
- Pass objects to JavaScript as global vars

## Requirements
- Gradle version 6.0+
Expand Down Expand Up @@ -43,7 +44,16 @@ dependencies {
```

## Usage
...
```kotlin
val javaScriptEngine = JavaScriptEngine()
val result: JsType = javaScriptEngine.evaluate(
context = emptyMap(),
script = """ "Hello" + "World" """.trimIndent()
)
if (result is JsType.Str) {
println(result.value)
}
```

## Samples
More examples can be found in the [sample directory](sample).
Expand Down
28 changes: 24 additions & 4 deletions buildSrc/src/main/kotlin/Deps.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,50 @@ object Deps {
private const val androidAppCompatVersion = "1.1.0"
private const val materialDesignVersion = "1.0.0"
private const val androidLifecycleVersion = "2.1.0"
private const val androidCoreTestingVersion = "2.1.0"
private const val androidCoreTestingVersion = "1.3.0"
private const val testJUnitExtVersion = "1.1.2"
private const val quickjsVersion = "0.9.0"

private const val coroutinesVersion = "1.4.2"
private const val mokoTestVersion = "0.2.0"
private const val kotlinxSerializationVersion = "1.1.0"
private const val mokoTestVersion = "0.3.0"

const val mokoJavascriptVersion = "0.1.0"

object Android {
const val compileSdk = 30
const val targetSdk = 30
const val minSdk = 16
const val minSdk = 18
}

object Libs {
object Android {
const val appCompat = "androidx.appcompat:appcompat:$androidAppCompatVersion"
const val material = "com.google.android.material:material:$materialDesignVersion"
const val lifecycle = "androidx.lifecycle:lifecycle-extensions:$androidLifecycleVersion"

const val kotlinTestJUnit = "org.jetbrains.kotlin:kotlin-test-junit:$kotlinTestVersion"
const val testRunner = "androidx.test:runner:$androidCoreTestingVersion"
const val testRules = "androidx.test:rules:$androidCoreTestingVersion"
const val testJUnitExt = "androidx.test.ext:junit:$testJUnitExtVersion"
const val testJUnitExtKtx = "androidx.test.ext:junit-ktx:$testJUnitExtVersion"

const val quickjs = "app.cash.quickjs:quickjs-android:$quickjsVersion"
}

object MultiPlatform {
const val coroutines =
"org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion"
const val kotlinSerialization =
"org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion"

const val kotlinTest =
"org.jetbrains.kotlin:kotlin-test-common:$kotlinTestVersion"
const val kotlinTestAnnotations =
"org.jetbrains.kotlin:kotlin-test-annotations-common:$kotlinTestVersion"
const val mokoTest = "dev.icerock.moko:test-core:$mokoTestVersion"
const val mokoTestRobolectric = "dev.icerock.moko:test-roboelectric:$mokoTestVersion"

const val mokoTest = "dev.icerock.moko:test:$mokoTestVersion"
const val mokoJavascript = "dev.icerock.moko:javascript:$mokoJavascriptVersion"
}
}
Expand Down
76 changes: 69 additions & 7 deletions buildSrc/src/main/kotlin/publication-convention.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,17 +1,79 @@
import java.util.Base64

plugins {
id("org.gradle.maven-publish")
id("signing")
}

publishing {
group = "dev.icerock.moko"
version = Deps.mokoJavascriptVersion
group = "dev.icerock.moko"
version = Deps.mokoJavascriptVersion

val javadocJar by tasks.registering(Jar::class) {
archiveClassifier.set("javadoc")
}

repositories.maven("https://api.bintray.com/maven/icerockdev/moko/moko-javascript/;publish=1") {
name = "bintray"
publishing {
repositories.maven("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/") {
name = "OSSRH"

credentials {
username = System.getenv("BINTRAY_USER")
password = System.getenv("BINTRAY_KEY")
username = System.getenv("OSSRH_USER")
password = System.getenv("OSSRH_KEY")
}
}

publications.withType<MavenPublication> {
// Stub javadoc.jar artifact
artifact(javadocJar.get())

// Provide artifacts information requited by Maven Central
pom {
name.set("MOKO JavaScript")
description.set("JavaScript code evaluation from common code for Kotlin Multiplatform Mobile")
url.set("https://github.com/icerockdev/moko-javascript")
licenses {
license {
name.set("Apache-2.0")
distribution.set("repo")
url.set("https://github.com/icerockdev/moko-javascript/blob/master/LICENSE.md")
}
}

developers {
developer {
id.set("Tetraquark")
name.set("Vladislav Areshkin")
email.set("vareshkin@icerockdev.com")
}
developer {
id.set("Dorofeev")
name.set("Andrey Dorofeev")
email.set("adorofeev@icerockdev.com")
}
developer {
id.set("Alex009")
name.set("Aleksey Mikhailov")
email.set("aleksey.mikhailov@icerockdev.com")
}
}

scm {
connection.set("scm:git:ssh://github.com/icerockdev/moko-javascript.git")
developerConnection.set("scm:git:ssh://github.com/icerockdev/moko-javascript.git")
url.set("https://github.com/icerockdev/moko-javascript")
}
}
}

signing {
val signingKeyId: String? = System.getenv("SIGNING_KEY_ID")
val signingPassword: String? = System.getenv("SIGNING_PASSWORD")
val signingKey: String? = System.getenv("SIGNING_KEY")?.let { base64Key ->
String(Base64.getDecoder().decode(base64Key))
}
if (signingKeyId != null) {
useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword)
sign(publishing.publications)
}
}
}
47 changes: 47 additions & 0 deletions javascript/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,53 @@ plugins {
id("publication-convention")
}

android {
testOptions.unitTests.isIncludeAndroidResources = true
defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

packagingOptions {
exclude("META-INF/*.kotlin_module")
exclude("META-INF/*.kotlin_module")
exclude("META-INF/AL2.0")
exclude("META-INF/LGPL2.1")
}

sourceSets {
getByName("androidTest").java.srcDirs(
file("src/androidAndroidTest/kotlin"),
file("src/mobileDeviceTest/kotlin")
)
}
}

kotlin {
sourceSets {
val mobileDeviceTest by creating

val commonTest by getting
val iosTest by getting
val androidAndroidTest by getting

mobileDeviceTest.dependsOn(commonTest)
iosTest.dependsOn(mobileDeviceTest)
androidAndroidTest.dependsOn(mobileDeviceTest)
}
}

dependencies {
androidMainImplementation(Deps.Libs.Android.quickjs)

commonMainImplementation(Deps.Libs.MultiPlatform.kotlinSerialization)

commonTestImplementation(Deps.Libs.MultiPlatform.kotlinTest)
commonTestImplementation(Deps.Libs.MultiPlatform.kotlinTestAnnotations)
commonTestImplementation(Deps.Libs.MultiPlatform.mokoTest)

androidTestImplementation(Deps.Libs.Android.kotlinTestJUnit)
androidTestImplementation(Deps.Libs.Android.testRunner)
androidTestImplementation(Deps.Libs.Android.testRules)
androidTestImplementation(Deps.Libs.Android.testJUnitExt)
androidTestImplementation(Deps.Libs.Android.testJUnitExtKtx)
}
2 changes: 1 addition & 1 deletion javascript/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="dev.icerock.moko.javascript" />
<manifest package="dev.icerock.moko.javascript" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.icerock.moko.javascript

import app.cash.quickjs.QuickJs
import app.cash.quickjs.QuickJsException
import kotlinx.serialization.SerializationException
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject

actual class JavaScriptEngine actual constructor() {
private val quickJs: QuickJs = QuickJs.create()
private val json: Json = Json.Default

@Volatile
var isClosed = false
private set

actual fun evaluate(
context: Map<String, JsType>,
script: String
): JsType {
if (isClosed) throw JavaScriptEvaluationException(message = "Engine already closed")

return try {
internalEvaluate(context, script)
} catch (exception: QuickJsException) {
throw JavaScriptEvaluationException(exception, exception.message)
}
}

actual fun close() {
if (isClosed) return
quickJs.close()
isClosed = true
}

private fun internalEvaluate(
context: Map<String, JsType>,
script: String
): JsType {
val scriptWithContext = convertContextMapToJsScript(context) + script + "\n"
val result = quickJs.evaluate(scriptWithContext)
return handleQuickJsResult(result)
}

// TODO fix pass of arguments - now wrapping of string and json invalid and will be broken on multilined strings
private fun convertContextMapToJsScript(context: Map<String, JsType>): String {
if (context.isEmpty()) return ""

return context.mapNotNull { pair ->
prepareValueForJs(pair.value)?.let { "var ${pair.key} = $it;" }
}.joinToString(separator = "")
}

private fun prepareValueForJs(valueWrapper: JsType): String? {
return when (valueWrapper) {
is JsType.Bool -> valueWrapper.value.toString()
is JsType.DoubleNum -> valueWrapper.value.toString()
is JsType.Json -> valueWrapper.value.let {
Json.encodeToString(JsonElement.serializer(), it)
}.let {
it.replace("\"", "\\\"")
}.let {
"JSON.parse(\"$it\")"
}
is JsType.Str -> valueWrapper.value.let {
it.replace("\"", "\\\"")
}.let {
"\"$it\""
}
JsType.Null -> null
}
}

private fun handleQuickJsResult(result: Any?): JsType {
return when (result) {
null -> JsType.Null
is Boolean -> JsType.Bool(result)
is Int -> JsType.DoubleNum(result.toDouble())
is Double -> JsType.DoubleNum(result)
is Float -> JsType.DoubleNum(result.toDouble())
is String -> try {
val serializeResult = json.parseToJsonElement(result)
if (serializeResult is JsonObject) {
JsType.Json(serializeResult)
} else {
JsType.Str(result)
}
} catch (ex: SerializationException) {
JsType.Str(result)
} catch (ex: IllegalStateException) {
JsType.Str(result)
}
else -> throw JavaScriptEvaluationException(
message = "Impossible JavaScriptEngine handler state with result [$result]"
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,16 @@
package dev.icerock.moko.javascript

expect class JavaScriptEngine() {
/**
* Evaluate some [script] with external [context].
*
* @throws JavaScriptEvaluationException in case of an error in the engine evaluation or if the
* engine has already been closed.
*/
fun evaluate(context: Map<String, JsType>, script: String): JsType

/**
* Closes the engine and releases the allocated memory.
*/
fun close()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.icerock.moko.javascript

class JavaScriptEvaluationException(
cause: Throwable? = null,
message: String? = null
) : Exception(message, cause)
Loading