├── .gitignore ├── LICENSE ├── README.md ├── backend ├── build.gradle └── src │ └── main │ ├── kotlin │ └── Server.kt │ └── resources │ ├── application.conf │ └── css │ └── style.css ├── build.gradle ├── frontend ├── build.gradle └── src │ └── main │ └── kotlin │ └── frontend.kt ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── kxhtml-isomorphic-js ├── build.gradle └── src │ └── main │ └── kotlin │ └── JsDate.kt ├── kxhtml-isomorphic-jvm ├── build.gradle └── src │ └── main │ └── kotlin │ └── JvmDate.kt ├── package.json ├── settings.gradle └── shared ├── build.gradle └── src ├── main └── kotlin │ ├── Date.kt │ ├── DateSerializer.kt │ ├── Message.kt │ ├── MessageSerializer.kt │ └── Render.kt └── test └── kotlin └── DateTest.kt /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .idea 3 | node_modules 4 | *.iml 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kxhtml-isomorphic 2 | 3 | This sample demonstrates using Kotlin to create an application with _isomorphic HTML rendering_, with the same code 4 | used to render HTML on the server and on the client. The application is a [multiplatform project](http://kotlinlang.org/docs/reference/multiplatform.html), 5 | compiled to JVM bytecode on the server side and to JS for the browser. Most of the libraries used by the application are also multiplatform. 6 | 7 | The sample uses the following technologies: 8 | 9 | * [ktor](http://ktor.io) as the backend framework and the HTTP client implementation on the backend; 10 | * [kotlinx.html](https://github.com/Kotlin/kotlinx.html) to render the HTML; 11 | * [kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) to serialize the data on the backend 12 | and deserialize it on the frontend; 13 | * [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) to run asynchronous operations on the frontend 14 | (and also as underlying technology in Ktor). 15 | * [kotlin.test](http://kotlinlang.org/api/latest/kotlin.test/index.html) for testing multiplatform code. 16 | 17 | ## Running the application 18 | 19 | To work with the project in IntelliJ IDEA, please make sure that you have the Kotlin plugin version 1.2.20 or newer, 20 | because earlier versions do not contain some of the changes required to work correctly with multiplatform projects. 21 | Also, running tests under Mocha requires IntelliJ IDEA Ultimate, as IntelliJ IDEA Community Edition does not provide 22 | any support for node. 23 | 24 | To run the application, use `./gradlew run` or open `build.gradle` as a Gradle project in the IDE and run the 25 | `backend` module (create a run configuration of type Gradle, select "kxhtml-isomorphic:backend" module and the "run" task). 26 | Then open http://localhost:8080 in the browser. The application will start showing the most recent commits to the 27 | Kotlin project. The first five commits are rendered on the server; subsequent commits are serialized to JSON and 28 | rendered by the client. 29 | 30 | To run the tests for the shared code, right-click the `DateTest` class in the IDE and select the Run option. 31 | To run DateTest under Mocha, you need to first run `npm install` in the root directory of the project. 32 | 33 | ## Source code highlights 34 | 35 | The [shared module](https://github.com/yole/kxhtml-isomorphic/tree/master/shared/src/main/kotlin) contains the 36 | cross-platform part of the application, and is compiled both to the JVM and JS. It contains the following main parts: 37 | 38 | * The [Message class](https://github.com/yole/kxhtml-isomorphic/blob/master/shared/src/main/kotlin/Message.kt) is the 39 | data model for the application (just a single class in this case). 40 | * The [Date class](https://github.com/yole/kxhtml-isomorphic/blob/master/shared/src/main/kotlin/Date.kt) defines 41 | a multiplatform API for working with dates. This demonstrates using the `expect` and `actual` keywords to define 42 | a set of APIs with platform-specific implementations. 43 | * The [renderMessage function](https://github.com/yole/kxhtml-isomorphic/blob/master/shared/src/main/kotlin/Render.kt) 44 | renders the message to HTML. It is called from both the front-end and the back-end code. 45 | 46 | The [DateTest class](https://github.com/yole/kxhtml-isomorphic/blob/master/shared/src/test/kotlin/DateTest.kt) contains 47 | some simple tests for the date logic. The tests can run under both JUnit on the JVM and Mocha on Node. 48 | 49 | The [kxhtml-isomorphic-jvm](https://github.com/yole/kxhtml-isomorphic/tree/master/kxhtml-isomorphic-jvm) and 50 | [kxhtml-isomorphic-js](https://github.com/yole/kxhtml-isomorphic/tree/master/kxhtml-isomorphic-js) modules contain 51 | platform-dependent implementations of the date logic. The JVM implementation uses the Calendar class from the JDK, 52 | while the JS implementation delegates to the browser-provided Date class. 53 | 54 | The [backend module](https://github.com/yole/kxhtml-isomorphic/tree/master/backend) is a simple Ktor application 55 | that renders the initial HTML page and exposes a JSON API for retrieving updates. It is also responsible for serving 56 | the JavaScript code of the frontend module (see the `copyBundleJs` task in its build.gradle to see how this is set up). 57 | 58 | The [frontend module](https://github.com/yole/kxhtml-isomorphic/tree/master/frontend) contains the code which runs in 59 | the browser (periodically calling the backend to fetch new data and delegating to the shared rendering code to display 60 | the received data on the page). 61 | -------------------------------------------------------------------------------- /backend/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin' 2 | apply plugin: 'application' 3 | 4 | mainClassName = 'io.ktor.server.netty.DevelopmentEngine' 5 | 6 | repositories { 7 | maven { url "https://dl.bintray.com/kotlin/ktor" } 8 | maven { url "https://dl.bintray.com/kotlin/kotlinx" } 9 | } 10 | 11 | dependencies { 12 | compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 13 | compile "io.ktor:ktor-server-core:$ktor_version" 14 | compile "io.ktor:ktor-server-netty:$ktor_version" 15 | compile "io.ktor:ktor-client-apache:$ktor_version" 16 | compile "io.ktor:ktor-client-json:$ktor_version" 17 | compile "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$serialization_version" 18 | compile project(":kxhtml-isomorphic-jvm") 19 | } 20 | 21 | kotlin { 22 | experimental { 23 | coroutines "enable" 24 | } 25 | } 26 | 27 | compileKotlin { 28 | kotlinOptions.jvmTarget = "1.8" 29 | } 30 | compileTestKotlin { 31 | kotlinOptions.jvmTarget = "1.8" 32 | } 33 | 34 | task copyBundleJs(type: Copy, dependsOn: ":frontend:bundle") { 35 | from("${rootProject.findProject(":frontend").buildDir}/bundle") { 36 | include "*.js" 37 | include "*.js.map" 38 | } 39 | into "$buildDir/resources/main/js" 40 | } 41 | 42 | processResources.dependsOn(copyBundleJs) 43 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/Server.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.kxhtml.isomorphic 2 | 3 | import io.ktor.application.Application 4 | import io.ktor.application.call 5 | import io.ktor.application.install 6 | import io.ktor.client.HttpClient 7 | import io.ktor.client.engine.apache.Apache 8 | import io.ktor.client.features.json.JsonFeature 9 | import io.ktor.client.request.get 10 | import io.ktor.content.resources 11 | import io.ktor.content.static 12 | import io.ktor.features.CallLogging 13 | import io.ktor.features.DefaultHeaders 14 | import io.ktor.features.StatusPages 15 | import io.ktor.http.ContentType 16 | import io.ktor.http.HttpStatusCode 17 | import io.ktor.response.respond 18 | import io.ktor.response.respondText 19 | import io.ktor.routing.Routing 20 | import io.ktor.routing.get 21 | import kotlinx.coroutines.experimental.runBlocking 22 | import kotlinx.html.* 23 | import kotlinx.html.stream.createHTML 24 | import kotlinx.serialization.SerialContext 25 | import kotlinx.serialization.json.JSON 26 | import java.util.* 27 | 28 | data class Committer(val name: String, val date: String) 29 | 30 | data class Commit(val message: String, val author: Committer, val committer: Committer) 31 | 32 | data class CommitData(val commit: Commit) 33 | 34 | val messages = mutableListOf() 35 | var lastSentMessageIndex = 0 36 | 37 | fun Application.main() { 38 | runBlocking { 39 | val client = HttpClient(Apache) { 40 | install(JsonFeature) 41 | } 42 | 43 | val data = client.get>("https://api.github.com/repos/jetbrains/kotlin/commits") 44 | data.mapTo(messages) { datum -> 45 | Message(datum.commit.message.smartTruncate(), datum.commit.author.name, parseDate(datum.commit.author.date)) 46 | } 47 | } 48 | 49 | install(DefaultHeaders) 50 | install(CallLogging) 51 | install(StatusPages) { 52 | exception { 53 | call.respond(HttpStatusCode.InternalServerError) 54 | } 55 | } 56 | install(Routing) { 57 | get("/") { 58 | call.respondText(renderIndexPage(), ContentType.Text.Html) 59 | } 60 | get("/newMessages") { 61 | call.respondText(serializeNewMessages(), ContentType.Application.Json) 62 | } 63 | static("static/js") { 64 | resources("js") 65 | } 66 | static("static/css") { 67 | resources("css") 68 | } 69 | } 70 | val port = environment.config.config("ktor").config("deployment").property("port").getString() 71 | println("Backend running on http://localhost:$port") 72 | } 73 | 74 | private fun renderIndexPage(): String { 75 | val messages = messages.take(5) 76 | lastSentMessageIndex = 5 77 | return createHTML().html { 78 | head { 79 | script(src = "static/js/frontend.bundle.js") {} 80 | styleLink("static/css/style.css") 81 | } 82 | body { 83 | div("content") { 84 | for (message in messages) { 85 | renderMessage(message) 86 | } 87 | } 88 | } 89 | } 90 | } 91 | 92 | private fun serializeNewMessages(): String { 93 | return messages.drop(lastSentMessageIndex++).firstOrNull()?.toJSON() ?: "[]" 94 | } 95 | 96 | private fun String.smartTruncate(): String { 97 | if (length < 50) return this 98 | val index = lastIndexOf(' ', 50) 99 | if (index < 0) return substring(0, 50) + "..." 100 | return substring(0, index).trim() + "..." 101 | } 102 | -------------------------------------------------------------------------------- /backend/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | ktor { 2 | deployment { 3 | port = 8080 4 | } 5 | 6 | application { 7 | modules = [ org.jetbrains.kxhtml.isomorphic.ServerKt.main ] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /backend/src/main/resources/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 36pt; 3 | } 4 | 5 | .message { 6 | padding: 10px; 7 | margin: 10px; 8 | background-color: lavender; 9 | } 10 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | buildscript { 3 | ext.kotlin_version = '1.2.21' 4 | ext.ktor_version = '0.9.1' 5 | ext.kotlinx_html_version = '0.6.8' 6 | ext.frontend_plugin_version = '0.0.27' 7 | ext.serialization_version = '0.4.1' 8 | 9 | repositories { 10 | maven { url 'http://dl.bintray.com/kotlin/kotlin-eap' } 11 | maven { url "https://plugins.gradle.org/m2/" } 12 | maven { url "https://kotlin.bintray.com/kotlinx" } 13 | jcenter() 14 | } 15 | dependencies { 16 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 17 | classpath "org.jetbrains.kotlinx:kotlinx-gradle-serialization-plugin:$serialization_version" 18 | } 19 | } 20 | 21 | repositories { 22 | maven { url 'http://dl.bintray.com/kotlin/kotlin-eap-1.2' } 23 | maven { url "https://kotlin.bintray.com/kotlinx" } 24 | jcenter() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /frontend/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | dependencies { 3 | classpath "org.jetbrains.kotlin:kotlin-frontend-plugin:$frontend_plugin_version" 4 | } 5 | } 6 | 7 | apply plugin: 'kotlin2js' 8 | apply plugin: "kotlin-dce-js" 9 | apply plugin: 'org.jetbrains.kotlin.frontend' 10 | 11 | dependencies { 12 | compile project(":kxhtml-isomorphic-js") 13 | compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core-js:0.22.1' 14 | } 15 | 16 | compileKotlin2Js { 17 | kotlinOptions { 18 | sourceMap = true 19 | sourceMapEmbedSources = "always" 20 | moduleKind = 'commonjs' 21 | metaInfo = false 22 | } 23 | } 24 | 25 | kotlinFrontend { 26 | webpackBundle { 27 | bundleName = "frontend" 28 | sourceMapEnabled = true 29 | } 30 | } 31 | 32 | kotlin { 33 | experimental { 34 | coroutines "enable" 35 | } 36 | } -------------------------------------------------------------------------------- /frontend/src/main/kotlin/frontend.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.kxhtml.isomorphic.frontend 2 | 3 | import kotlinx.coroutines.experimental.async 4 | import kotlinx.html.div 5 | import kotlinx.html.dom.append 6 | import org.jetbrains.kxhtml.isomorphic.Message 7 | import org.jetbrains.kxhtml.isomorphic.fromJSON 8 | import org.jetbrains.kxhtml.isomorphic.renderMessage 9 | import org.w3c.dom.HTMLElement 10 | import org.w3c.xhr.XMLHttpRequest 11 | import kotlin.browser.document 12 | import kotlin.browser.window 13 | import kotlin.coroutines.experimental.suspendCoroutine 14 | 15 | suspend fun httpGet(url: String): String = suspendCoroutine { c -> 16 | val xhr = XMLHttpRequest() 17 | xhr.onreadystatechange = { 18 | if (xhr.readyState == XMLHttpRequest.DONE) { 19 | if (xhr.status / 100 == 2) { 20 | c.resume(xhr.response as String) 21 | } 22 | else { 23 | c.resumeWithException(RuntimeException("HTTP error: ${xhr.status}")) 24 | } 25 | } 26 | null 27 | } 28 | xhr.open("GET", url) 29 | xhr.send() 30 | } 31 | 32 | fun fetchNewMessages() { 33 | async { 34 | console.log("Fetching new messages") 35 | val data = httpGet("http://localhost:8080/newMessages") 36 | val message = Message.fromJSON(data) 37 | val contentDiv = document.getElementsByClassName("content").item(0) as HTMLElement 38 | contentDiv.append.div { 39 | renderMessage(message) 40 | } 41 | } 42 | } 43 | 44 | fun main(args: Array) { 45 | console.log("Frontend loaded") 46 | window.setInterval(::fetchNewMessages, 1000) 47 | } 48 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yole/kxhtml-isomorphic/494685b52112dcb020a315e523cb4a8638738bb4/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Nov 14 14:00:25 CET 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /kxhtml-isomorphic-js/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin-platform-js' 2 | apply plugin: 'kotlinx-serialization' 3 | 4 | dependencies { 5 | compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version" 6 | compile "org.jetbrains.kotlinx:kotlinx-html-js:${kotlinx_html_version}" 7 | compile "org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:$serialization_version" 8 | expectedBy project(":shared") 9 | testCompile "org.jetbrains.kotlin:kotlin-test-js:$kotlin_version" 10 | } 11 | 12 | [compileKotlin2Js, compileTestKotlin2Js]*.configure { 13 | kotlinOptions { 14 | sourceMap = true 15 | sourceMapEmbedSources = "always" 16 | moduleKind = 'commonjs' 17 | metaInfo = true 18 | } 19 | } 20 | 21 | task populateNodeModules(type: Copy, dependsOn: compileKotlin2Js) { 22 | from compileKotlin2Js.destinationDir 23 | 24 | configurations.testCompile.each { 25 | from zipTree(it.absolutePath).matching { include '*.js' } 26 | } 27 | 28 | into "${buildDir}/node_modules" 29 | } 30 | 31 | testClasses.dependsOn populateNodeModules 32 | -------------------------------------------------------------------------------- /kxhtml-isomorphic-js/src/main/kotlin/JsDate.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.kxhtml.isomorphic 2 | 3 | actual external class Date { 4 | actual constructor() 5 | actual constructor(value: Number) 6 | 7 | actual fun getDate(): Int 8 | actual fun getMonth(): Int 9 | actual fun getFullYear(): Int 10 | actual fun getHours(): Int 11 | actual fun getMinutes(): Int 12 | actual fun getSeconds(): Int 13 | 14 | actual fun getTime(): Number 15 | 16 | companion object { 17 | fun parse(string: String): Number 18 | } 19 | } 20 | 21 | actual fun parseDate(dateString: String): Date = Date(Date.parse(dateString)) 22 | 23 | actual fun Date.toReadableDateString(): String { 24 | return "${monthAsString()} ${getDate()}, ${getFullYear()}" 25 | } 26 | 27 | actual fun Date.toReadableTimeString(): String = 28 | "${getHours().formatAsTwoDigits()}:${getMinutes().formatAsTwoDigits()}:${getSeconds().formatAsTwoDigits()}" 29 | 30 | private fun Date.monthAsString(): String = months[getMonth()] 31 | 32 | private val months = arrayOf("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec") 33 | 34 | fun Int.formatAsTwoDigits(): String = if (this < 10) "0$this" else toString() 35 | -------------------------------------------------------------------------------- /kxhtml-isomorphic-jvm/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin-platform-jvm' 2 | apply plugin: 'kotlinx-serialization' 3 | 4 | dependencies { 5 | compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 6 | compile "org.jetbrains.kotlinx:kotlinx-html-jvm:${kotlinx_html_version}" 7 | compile "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$serialization_version" 8 | expectedBy project(":shared") 9 | testCompile "junit:junit:4.12" 10 | testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" 11 | testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" 12 | } 13 | -------------------------------------------------------------------------------- /kxhtml-isomorphic-jvm/src/main/kotlin/JvmDate.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.kxhtml.isomorphic 2 | 3 | import java.text.SimpleDateFormat 4 | import java.util.* 5 | 6 | actual class Date { 7 | private val calendar: Calendar 8 | 9 | actual constructor() { 10 | calendar = Calendar.getInstance() 11 | } 12 | 13 | actual constructor(value: Number) { 14 | calendar = Calendar.getInstance().apply { 15 | timeInMillis = value.toLong() 16 | } 17 | } 18 | 19 | constructor(date: java.util.Date) { 20 | calendar = Calendar.getInstance().apply { 21 | time = date 22 | } 23 | } 24 | 25 | val date: java.util.Date get() = calendar.time 26 | 27 | actual fun getDate() = calendar[Calendar.DAY_OF_MONTH] 28 | actual fun getMonth() = calendar[Calendar.MONTH] 29 | actual fun getFullYear() = calendar[Calendar.YEAR] 30 | actual fun getHours() = calendar[Calendar.HOUR_OF_DAY] 31 | actual fun getMinutes() = calendar[Calendar.MINUTE] 32 | actual fun getSeconds() = calendar[Calendar.SECOND] 33 | actual fun getTime(): Number = calendar.timeInMillis 34 | 35 | override fun equals(other: Any?): Boolean = other is Date && other.calendar.time == calendar.time 36 | } 37 | 38 | val apiDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US) 39 | val readableDateFormat = SimpleDateFormat("MMM d, yyyy", Locale.getDefault()) 40 | val readableTimeFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) 41 | 42 | actual fun parseDate(dateString: String): Date = Date(apiDateFormat.parse(dateString)) 43 | 44 | actual fun Date.toReadableDateString() = readableDateFormat.format(date) 45 | actual fun Date.toReadableTimeString() = readableTimeFormat.format(date) 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "mocha": "4.0.1" 4 | } 5 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'kxhtml-isomorphic' 2 | 3 | include 'kxhtml-isomorphic-jvm', 'kxhtml-isomorphic-js' 4 | include 'backend' 5 | include 'shared' 6 | include 'frontend' 7 | -------------------------------------------------------------------------------- /shared/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin-platform-common' 2 | apply plugin: 'kotlinx-serialization' 3 | 4 | dependencies { 5 | compile "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlin_version" 6 | compile "org.jetbrains.kotlinx:kotlinx-html-common:${kotlinx_html_version}" 7 | compile "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serialization_version" 8 | testCompile "org.jetbrains.kotlin:kotlin-test-annotations-common:$kotlin_version" 9 | testCompile "org.jetbrains.kotlin:kotlin-test-common:$kotlin_version" 10 | } 11 | -------------------------------------------------------------------------------- /shared/src/main/kotlin/Date.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.kxhtml.isomorphic 2 | 3 | expect class Date() { 4 | constructor(value: Number) 5 | fun getDate(): Int 6 | fun getMonth(): Int 7 | fun getFullYear(): Int 8 | fun getHours(): Int 9 | fun getMinutes(): Int 10 | fun getSeconds(): Int 11 | fun getTime(): Number 12 | } 13 | 14 | expect fun parseDate(dateString: String): Date 15 | expect fun Date.toReadableDateString(): String 16 | expect fun Date.toReadableTimeString(): String 17 | 18 | fun Date.toReadableDateTimeString() = "${toReadableDateString()} ${toReadableTimeString()}" 19 | -------------------------------------------------------------------------------- /shared/src/main/kotlin/DateSerializer.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.kxhtml.isomorphic 2 | 3 | import kotlinx.serialization.KInput 4 | import kotlinx.serialization.KOutput 5 | import kotlinx.serialization.KSerialClassDesc 6 | import kotlinx.serialization.KSerializer 7 | import kotlinx.serialization.internal.SerialClassDescImpl 8 | 9 | object DateSerializer : KSerializer { 10 | override fun load(input: KInput): Date = Date(input.readLongValue()) 11 | 12 | override fun save(output: KOutput, obj: Date) { 13 | output.writeLongValue(obj.getTime().toLong()) 14 | } 15 | 16 | override val serialClassDesc: KSerialClassDesc 17 | get() = SerialClassDescImpl("org.jetbrains.kxhtml.isomorphic.Date") 18 | } 19 | 20 | -------------------------------------------------------------------------------- /shared/src/main/kotlin/Message.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.kxhtml.isomorphic 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Message(val text: String, val author: String, val date: Date) { 7 | companion object { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /shared/src/main/kotlin/MessageSerializer.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.kxhtml.isomorphic 2 | 3 | import kotlinx.serialization.SerialContext 4 | import kotlinx.serialization.json.JSON 5 | 6 | fun Message.toJSON(): String = 7 | JSON(context = createSerialContext()).stringify(this) 8 | 9 | fun Message.Companion.fromJSON(json: String): Message = 10 | JSON(context = createSerialContext()).parse(json) 11 | 12 | private fun createSerialContext() = SerialContext().apply { 13 | registerSerializer(Date::class, DateSerializer) 14 | } 15 | -------------------------------------------------------------------------------- /shared/src/main/kotlin/Render.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.kxhtml.isomorphic 2 | 3 | import kotlinx.html.* 4 | 5 | fun HtmlBlockTag.renderMessage(message: Message) { 6 | div(classes = "message") { 7 | +message.text 8 | i { 9 | + " by " 10 | + message.author 11 | + " at " 12 | + message.date.toReadableDateTimeString() 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /shared/src/test/kotlin/DateTest.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.kxhtml.isomorphic 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | 6 | class DateTest { 7 | @Test fun testParse() { 8 | val date = parseDate("2017-10-24T13:31:19") 9 | assertEquals(2017, date.getFullYear()) 10 | assertEquals(9, date.getMonth()) 11 | assertEquals(24, date.getDate()) 12 | } 13 | } 14 | --------------------------------------------------------------------------------