├── .gitignore ├── README.md ├── backend-js ├── app.js ├── build.gradle ├── package-lock.json ├── package.json └── src │ └── main │ └── kotlin │ ├── com │ └── wadejensen │ │ └── example │ │ ├── main.kt │ │ └── model │ │ ├── Address.kt │ │ └── Person.kt │ └── pappel │ ├── Application.kt │ ├── JSONUtils.kt │ ├── NodeJs.kt │ ├── Router.kt │ ├── async.kt │ ├── fetch.kt │ └── http │ ├── Method.kt │ ├── Protocol.kt │ ├── Request.kt │ ├── RequestInit.kt │ ├── Response.kt │ └── Status.kt ├── build.gradle ├── common-js ├── README.md ├── build.gradle └── src │ └── main │ └── kotlin │ └── com │ └── wadejensen │ └── example │ ├── Console.kt │ ├── Math.kt │ └── main.kt ├── common ├── README.md ├── build.gradle └── src │ ├── main │ └── kotlin │ │ └── com │ │ └── wadejensen │ │ └── example │ │ ├── Console.kt │ │ ├── IConsole.kt │ │ ├── IMath.kt │ │ ├── Math.kt │ │ └── SharedClass.kt │ └── test │ └── kotlin │ └── com │ └── wadejensen │ └── example │ └── SharedClassTest.kt ├── frontend-js ├── build.gradle ├── package-lock.json └── src │ └── main │ ├── kotlin │ └── com │ │ └── wadejensen │ │ └── example │ │ ├── DOMConsole.kt │ │ └── main.kt │ └── web │ ├── app.bundle.js │ └── index.html ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── package-lock.json └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Gradle template 3 | .gradle 4 | build/ 5 | 6 | # Ignore Gradle GUI config 7 | gradle-app.setting 8 | 9 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 10 | !gradle-wrapper.jar 11 | ### JetBrains template 12 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 13 | 14 | *.iml 15 | 16 | ## Directory-based project format: 17 | .idea/ 18 | # if you remove the above rule, at least ignore the following: 19 | 20 | # User-specific stuff: 21 | # .idea/workspace.xml 22 | # .idea/tasks.xml 23 | # .idea/dictionaries 24 | 25 | # Sensitive or high-churn files: 26 | # .idea/dataSources.ids 27 | # .idea/dataSources.xml 28 | # .idea/sqlDataSources.xml 29 | # .idea/dynamic.xml 30 | # .idea/uiDesigner.xml 31 | 32 | # Gradle: 33 | # .idea/gradle.xml 34 | # .idea/libraries 35 | 36 | # Mongo Explorer plugin: 37 | # .idea/mongoSettings.xml 38 | 39 | ## File-based project format: 40 | *.ipr 41 | *.iws 42 | 43 | ## Plugin-specific files: 44 | 45 | # IntelliJ 46 | /out/ 47 | 48 | # mpeltonen/sbt-idea plugin 49 | .idea_modules/ 50 | 51 | # JIRA plugin 52 | atlassian-ide-plugin.xml 53 | 54 | # Crashlytics plugin (for Android Studio and IntelliJ) 55 | com_crashlytics_export_strings.xml 56 | crashlytics.properties 57 | crashlytics-build.properties 58 | ### Java template 59 | *.class 60 | 61 | # Mobile Tools for Java (J2ME) 62 | .mtj.tmp/ 63 | 64 | # Package Files # 65 | # *.jar 66 | # *.war 67 | # *.ear 68 | 69 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 70 | hs_err_pid* 71 | 72 | web/js/* 73 | 74 | frontend-js/src/main/web/app.bundle.js 75 | 76 | node_modules 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multiplatform Kotlin example targeting Javascript on the backend and frontend 2 | 3 | This project demonstrates: 4 | * Sharing runtime independent code between Javascript on both Node.js and in the browser 5 | * A typed wrapper around the Node.js Express framework 6 | 7 | Requires the following: 8 | * Kotlin 1.2 9 | * JDK 8+ installed 10 | * NPM installed 11 | * Node.js installed 12 | * Gradle installed 13 | 14 | ## Structure 15 | Multi-module Gradle project. 16 | 17 | * ``common`` - common module, shared Kotlin source code, platform independent code 18 | * ``common-js`` - JavaScript runtimes platform dependent code 19 | * ``backend-js`` - Express application transpiled to Node.js JavaScript 20 | * ``frontend-js`` - application transpiled for frontend JavaScript, packed in [WebPack](https://webpack.js.org/), 21 | it's only statically served by Node.js 22 | 23 | ## Compiling 24 | From the root of repo run: 25 | ```bash 26 | gradle build 27 | ``` 28 | or change directories to only build a specific subproject, eg. 29 | ```bash 30 | cd backend 31 | gradle build 32 | ``` 33 | 34 | ### Creating the Express app 35 | ```kt 36 | import pappel.Application 37 | 38 | val shared = SharedClass(Console(), Math()) 39 | val app = Application() 40 | 41 | app.get("/primes") { _, _ -> 42 | shared.platform = "Node.js" 43 | shared.printMe() 44 | println(shared.givePrimes(100)) 45 | } 46 | 47 | app.startHttpServer(3000) 48 | val path = require("path") 49 | 50 | val staticWebContentPath = path.join(__dirname, "../frontend-js/src/main/web") 51 | println("Serving content from: $staticWebContentPath") 52 | app.serveStaticContent(staticWebContentPath) 53 | 54 | println("Kotlin - Node.js webserver deployed and ready.") 55 | ``` 56 | 57 | ## Platform implementation specifics 58 | * prime number calculation is platform independent, single code shared for all platforms 59 | * text output on screen is platform dependent 60 | * **Frontend JavaScript** - it adds elements in the DOM of HTML page to display primes 61 | * **Node.js JavaScript** - uses `res.send()` to send primes as text in response to a web request 62 | 63 | ### Type safe use of Express in Node.js 64 | 65 | ##### Simple HTTP GET call using async - await pattern inside an Express route handler. (Uses a coroutine wrapper of native JS `Promise`s) 66 | ```kt 67 | app.get("/async-get") { _, res -> 68 | println("Async get route pinged!") 69 | 70 | async { 71 | val resp: Response = await { fetch("https://jsonplaceholder.typicode.com/todos/1") } 72 | val data: Any? = await { resp.json() } 73 | 74 | data?.also { console.dir(data) } 75 | res.send(JSON.stringify(data)) 76 | } 77 | } 78 | ``` 79 | ##### HTTP POST call using async - await pattern inside an Express route handler. 80 | Message body is passed as a Kotlin data class object 81 | Headers are passed as a Map 82 | (Uses a coroutine wrapper of native JS `Promise`s) 83 | ```kt 84 | app.get("/async-post") { _, res -> 85 | println("async-post-data route pinging") 86 | 87 | val wade = "{\"name\":\"Wade Jensen\", \"age\": 22, \"address\": {\"streetNum\": 123, \"streetName\": \"Fake street\", \"suburb\": \"Surry Hills\", \"postcode\": 2010}}" 88 | val person: Person = JSON.parse(wade) 89 | async { 90 | val request = RequestInit( 91 | method = Method.POST, 92 | headers = mapOf("username" to "wjensen", "password" to "1234567"), 93 | body = person) 94 | 95 | println("Request object:") 96 | console.dir(request) 97 | 98 | val resp = await { fetch("https://jsonplaceholder.typicode.com/posts", request) } 99 | val data: Any? = await { resp.json() } 100 | data?.also { 101 | println("Response object:") 102 | console.dir(data) 103 | } 104 | res.send(JSON.stringify(data)) 105 | } 106 | } 107 | ``` 108 | 109 | ##### Parse JSON strings into Kotlin objects inside an Express route handler 110 | ``` 111 | app.get("/parse-json") { _, res -> 112 | 113 | val data = "{\"name\":\"Wade Jensen\", \"age\": 22, \"address\": {\"streetNum\": 123, \"streetName\": \"Fake street\", \"suburb\": \"Surry Hills\", \"postcode\": 2010}}" 114 | println(data) 115 | val person: Person = JSON.parse(data) 116 | res.send(""" 117 | name = ${person.name}, 118 | age = ${person.age}, 119 | 120 | address.streetNum = ${person.address.streetNum}, 121 | address.streetName = ${person.address.streetName}, 122 | address.suburb = ${person.address.suburb}, 123 | address.postcode = ${person.address.postcode} 124 | """.trimIndent()) 125 | } 126 | ``` 127 | ##### Simple HTTP GET call handled with native JS `Promise`s inside an Express route handler 128 | ```kt 129 | app.get("/promise-get") { _, res -> 130 | println("promise-get route pinged!") 131 | 132 | val resp: Promise = fetch("https://jsonplaceholder.typicode.com/todos/1") 133 | resp 134 | .then { result: Response -> result.json() } 135 | .then { json -> JSON.stringify(json) } 136 | .then { strResult -> 137 | println(strResult) 138 | res.send(strResult) 139 | } 140 | } 141 | ``` 142 | 143 | ### Hot reload 144 | To automatically recompile on save to filesystem events: 145 | 146 | Install Nodemon (wrapper around `node` which watches for flushes to the filesystem and restarts the node server) 147 | ```bash 148 | npm install -g nodemon 149 | ``` 150 | 151 | Open two terminals and run the following: 152 | 153 | ```bash 154 | /home/wjensen/repos/kotlin-nodejs-example/backend-js > gradle --continuous build 155 | Continuous build is an incubating feature. 156 | 157 | BUILD SUCCESSFUL in 10s 158 | 10 actionable tasks: 10 up-to-date 159 | 160 | Waiting for changes to input files of tasks... (ctrl-d then enter to exit) 161 | new file: /home/wjensen/repos/kotlin-nodejs-example/backend/src/main/kotlin/com/wadejensen/example/main.kt 162 | Change detected, executing build... 163 | 164 | BUILD SUCCESSFUL in 18s 165 | 10 actionable tasks: 3 executed, 7 up-to-date 166 | ``` 167 | 168 | ```bash 169 | /home/wjensen/repos/kotlin-nodejs-example/backend-js > nodemon app.js 170 | [nodemon] 1.18.4 171 | [nodemon] to restart at any time, enter `rs` 172 | [nodemon] watching: *.* 173 | [nodemon] starting `node app.js` 174 | Starting server on port 3000. 175 | Server started successfully 176 | Serving content from: /home/wjensen/repos/kotlin-nodejs-example/frontend-js/src/main/web 177 | Kotlin - Node.js webserver deployed and ready. 178 | [nodemon] restarting due to changes... 179 | [nodemon] restarting due to changes... 180 | [nodemon] starting `node app.js` 181 | Starting server on port 3000. 182 | Server started successfully 183 | Serving content from: /home/wjensen/repos/kotlin-nodejs-example/frontend-js/src/main/web 184 | Kotlin - Node.js webserver ready. 185 | ``` 186 | 187 | The example project was built based on the ideas of (copied blatantly from) several example projects, including: 188 | * https://github.com/techprd/kotlin_node_js_seed (Something using Kotlin JS and express that was simple enough for 189 | me to get my head around) 190 | * https://github.com/wojta/hello-kotlin-multiplatform (A super-set of this repo, which includes JVM and android targets 191 | except the Node example is a console app, rather than an express webserver) 192 | * https://github.com/blazer82/pappel-framework (An experimental Node.js framework for Kotlin and React which 193 | wraps express to make it Kotlin-friendly) 194 | -------------------------------------------------------------------------------- /backend-js/app.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | let backend_js = require('backend-js'); 4 | 5 | backend_js.com.wadejensen.example.start(); -------------------------------------------------------------------------------- /backend-js/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.moowork.node" 2 | apply plugin: "kotlin2js" 3 | 4 | 5 | dependencies { 6 | compile "org.jetbrains.kotlin:kotlin-stdlib-js:js-$kotlin_version" 7 | compile project(':common-js') 8 | } 9 | 10 | repositories { 11 | jcenter() 12 | maven { url "https://kotlin.bintray.com/kotlinx" } 13 | } 14 | 15 | compileKotlin2Js { 16 | kotlinOptions.metaInfo = true 17 | kotlinOptions.sourceMap = true 18 | kotlinOptions.suppressWarnings = true 19 | kotlinOptions.verbose = true 20 | kotlinOptions.main = "call" 21 | kotlinOptions.moduleKind = "umd" 22 | } 23 | 24 | def outputDir = "${projectDir}/node_modules" 25 | 26 | task assembleJs(type: Copy) { 27 | configurations.compile.each { File file -> 28 | from(zipTree(file.absolutePath), { 29 | includeEmptyDirs = false 30 | include { fileTreeElement -> 31 | def path = fileTreeElement.path 32 | path.endsWith(".js") && (path.startsWith("META-INF/resources/") || 33 | !path.startsWith("META-INF/")) 34 | } 35 | }) 36 | } 37 | from compileKotlin2Js.destinationDir 38 | into "${outputDir}" 39 | dependsOn classes 40 | } 41 | 42 | //task jar2Module(type: NodeTask) { 43 | // 44 | //} 45 | 46 | assemble.dependsOn assembleJs -------------------------------------------------------------------------------- /backend-js/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello_js_node_app", 3 | "version": "0.0.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.3.5", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", 10 | "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", 11 | "requires": { 12 | "mime-types": "~2.1.18", 13 | "negotiator": "0.6.1" 14 | } 15 | }, 16 | "array-flatten": { 17 | "version": "1.1.1", 18 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 19 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 20 | }, 21 | "body-parser": { 22 | "version": "1.18.3", 23 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", 24 | "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", 25 | "requires": { 26 | "bytes": "3.0.0", 27 | "content-type": "~1.0.4", 28 | "debug": "2.6.9", 29 | "depd": "~1.1.2", 30 | "http-errors": "~1.6.3", 31 | "iconv-lite": "0.4.23", 32 | "on-finished": "~2.3.0", 33 | "qs": "6.5.2", 34 | "raw-body": "2.3.3", 35 | "type-is": "~1.6.16" 36 | } 37 | }, 38 | "bytes": { 39 | "version": "3.0.0", 40 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", 41 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" 42 | }, 43 | "content-disposition": { 44 | "version": "0.5.2", 45 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", 46 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" 47 | }, 48 | "content-type": { 49 | "version": "1.0.4", 50 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 51 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 52 | }, 53 | "cookie": { 54 | "version": "0.3.1", 55 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 56 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 57 | }, 58 | "cookie-parser": { 59 | "version": "1.4.3", 60 | "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.3.tgz", 61 | "integrity": "sha1-D+MfoZ0AC5X0qt8fU/3CuKIDuqU=", 62 | "requires": { 63 | "cookie": "0.3.1", 64 | "cookie-signature": "1.0.6" 65 | } 66 | }, 67 | "cookie-signature": { 68 | "version": "1.0.6", 69 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 70 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 71 | }, 72 | "debug": { 73 | "version": "2.6.9", 74 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 75 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 76 | "requires": { 77 | "ms": "2.0.0" 78 | } 79 | }, 80 | "depd": { 81 | "version": "1.1.2", 82 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 83 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 84 | }, 85 | "destroy": { 86 | "version": "1.0.4", 87 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 88 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 89 | }, 90 | "ee-first": { 91 | "version": "1.1.1", 92 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 93 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 94 | }, 95 | "encodeurl": { 96 | "version": "1.0.2", 97 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 98 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 99 | }, 100 | "escape-html": { 101 | "version": "1.0.3", 102 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 103 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 104 | }, 105 | "etag": { 106 | "version": "1.8.1", 107 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 108 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 109 | }, 110 | "express": { 111 | "version": "4.16.3", 112 | "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", 113 | "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", 114 | "requires": { 115 | "accepts": "~1.3.5", 116 | "array-flatten": "1.1.1", 117 | "body-parser": "1.18.2", 118 | "content-disposition": "0.5.2", 119 | "content-type": "~1.0.4", 120 | "cookie": "0.3.1", 121 | "cookie-signature": "1.0.6", 122 | "debug": "2.6.9", 123 | "depd": "~1.1.2", 124 | "encodeurl": "~1.0.2", 125 | "escape-html": "~1.0.3", 126 | "etag": "~1.8.1", 127 | "finalhandler": "1.1.1", 128 | "fresh": "0.5.2", 129 | "merge-descriptors": "1.0.1", 130 | "methods": "~1.1.2", 131 | "on-finished": "~2.3.0", 132 | "parseurl": "~1.3.2", 133 | "path-to-regexp": "0.1.7", 134 | "proxy-addr": "~2.0.3", 135 | "qs": "6.5.1", 136 | "range-parser": "~1.2.0", 137 | "safe-buffer": "5.1.1", 138 | "send": "0.16.2", 139 | "serve-static": "1.13.2", 140 | "setprototypeof": "1.1.0", 141 | "statuses": "~1.4.0", 142 | "type-is": "~1.6.16", 143 | "utils-merge": "1.0.1", 144 | "vary": "~1.1.2" 145 | }, 146 | "dependencies": { 147 | "body-parser": { 148 | "version": "1.18.2", 149 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", 150 | "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", 151 | "requires": { 152 | "bytes": "3.0.0", 153 | "content-type": "~1.0.4", 154 | "debug": "2.6.9", 155 | "depd": "~1.1.1", 156 | "http-errors": "~1.6.2", 157 | "iconv-lite": "0.4.19", 158 | "on-finished": "~2.3.0", 159 | "qs": "6.5.1", 160 | "raw-body": "2.3.2", 161 | "type-is": "~1.6.15" 162 | } 163 | }, 164 | "iconv-lite": { 165 | "version": "0.4.19", 166 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", 167 | "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" 168 | }, 169 | "qs": { 170 | "version": "6.5.1", 171 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", 172 | "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" 173 | }, 174 | "raw-body": { 175 | "version": "2.3.2", 176 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", 177 | "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", 178 | "requires": { 179 | "bytes": "3.0.0", 180 | "http-errors": "1.6.2", 181 | "iconv-lite": "0.4.19", 182 | "unpipe": "1.0.0" 183 | }, 184 | "dependencies": { 185 | "depd": { 186 | "version": "1.1.1", 187 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", 188 | "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" 189 | }, 190 | "http-errors": { 191 | "version": "1.6.2", 192 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", 193 | "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", 194 | "requires": { 195 | "depd": "1.1.1", 196 | "inherits": "2.0.3", 197 | "setprototypeof": "1.0.3", 198 | "statuses": ">= 1.3.1 < 2" 199 | } 200 | }, 201 | "setprototypeof": { 202 | "version": "1.0.3", 203 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", 204 | "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" 205 | } 206 | } 207 | }, 208 | "statuses": { 209 | "version": "1.4.0", 210 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", 211 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" 212 | } 213 | } 214 | }, 215 | "finalhandler": { 216 | "version": "1.1.1", 217 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", 218 | "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", 219 | "requires": { 220 | "debug": "2.6.9", 221 | "encodeurl": "~1.0.2", 222 | "escape-html": "~1.0.3", 223 | "on-finished": "~2.3.0", 224 | "parseurl": "~1.3.2", 225 | "statuses": "~1.4.0", 226 | "unpipe": "~1.0.0" 227 | }, 228 | "dependencies": { 229 | "statuses": { 230 | "version": "1.4.0", 231 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", 232 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" 233 | } 234 | } 235 | }, 236 | "forwarded": { 237 | "version": "0.1.2", 238 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 239 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 240 | }, 241 | "fresh": { 242 | "version": "0.5.2", 243 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 244 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 245 | }, 246 | "http-errors": { 247 | "version": "1.6.3", 248 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", 249 | "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", 250 | "requires": { 251 | "depd": "~1.1.2", 252 | "inherits": "2.0.3", 253 | "setprototypeof": "1.1.0", 254 | "statuses": ">= 1.4.0 < 2" 255 | } 256 | }, 257 | "iconv-lite": { 258 | "version": "0.4.23", 259 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", 260 | "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", 261 | "requires": { 262 | "safer-buffer": ">= 2.1.2 < 3" 263 | } 264 | }, 265 | "inherits": { 266 | "version": "2.0.3", 267 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 268 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 269 | }, 270 | "ipaddr.js": { 271 | "version": "1.8.0", 272 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", 273 | "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" 274 | }, 275 | "kotlin": { 276 | "version": "1.2.60", 277 | "resolved": "https://registry.npmjs.org/kotlin/-/kotlin-1.2.60.tgz", 278 | "integrity": "sha1-PX7CMooNRigeIbnXz1MUAyIXsBA=" 279 | }, 280 | "media-typer": { 281 | "version": "0.3.0", 282 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 283 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 284 | }, 285 | "merge-descriptors": { 286 | "version": "1.0.1", 287 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 288 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 289 | }, 290 | "methods": { 291 | "version": "1.1.2", 292 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 293 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 294 | }, 295 | "mime": { 296 | "version": "1.4.1", 297 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", 298 | "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" 299 | }, 300 | "mime-db": { 301 | "version": "1.36.0", 302 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz", 303 | "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==" 304 | }, 305 | "mime-types": { 306 | "version": "2.1.20", 307 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz", 308 | "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==", 309 | "requires": { 310 | "mime-db": "~1.36.0" 311 | } 312 | }, 313 | "ms": { 314 | "version": "2.0.0", 315 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 316 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 317 | }, 318 | "negotiator": { 319 | "version": "0.6.1", 320 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 321 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" 322 | }, 323 | "node-fetch": { 324 | "version": "2.2.0", 325 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.2.0.tgz", 326 | "integrity": "sha512-OayFWziIxiHY8bCUyLX6sTpDH8Jsbp4FfYd1j1f7vZyfgkcOnAyM4oQR16f8a0s7Gl/viMGRey8eScYk4V4EZA==" 327 | }, 328 | "on-finished": { 329 | "version": "2.3.0", 330 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 331 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 332 | "requires": { 333 | "ee-first": "1.1.1" 334 | } 335 | }, 336 | "parseurl": { 337 | "version": "1.3.2", 338 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", 339 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" 340 | }, 341 | "path-to-regexp": { 342 | "version": "0.1.7", 343 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 344 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 345 | }, 346 | "proxy-addr": { 347 | "version": "2.0.4", 348 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", 349 | "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", 350 | "requires": { 351 | "forwarded": "~0.1.2", 352 | "ipaddr.js": "1.8.0" 353 | } 354 | }, 355 | "qs": { 356 | "version": "6.5.2", 357 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 358 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 359 | }, 360 | "range-parser": { 361 | "version": "1.2.0", 362 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", 363 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" 364 | }, 365 | "raw-body": { 366 | "version": "2.3.3", 367 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", 368 | "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", 369 | "requires": { 370 | "bytes": "3.0.0", 371 | "http-errors": "1.6.3", 372 | "iconv-lite": "0.4.23", 373 | "unpipe": "1.0.0" 374 | } 375 | }, 376 | "safe-buffer": { 377 | "version": "5.1.1", 378 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 379 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" 380 | }, 381 | "safer-buffer": { 382 | "version": "2.1.2", 383 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 384 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 385 | }, 386 | "send": { 387 | "version": "0.16.2", 388 | "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", 389 | "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", 390 | "requires": { 391 | "debug": "2.6.9", 392 | "depd": "~1.1.2", 393 | "destroy": "~1.0.4", 394 | "encodeurl": "~1.0.2", 395 | "escape-html": "~1.0.3", 396 | "etag": "~1.8.1", 397 | "fresh": "0.5.2", 398 | "http-errors": "~1.6.2", 399 | "mime": "1.4.1", 400 | "ms": "2.0.0", 401 | "on-finished": "~2.3.0", 402 | "range-parser": "~1.2.0", 403 | "statuses": "~1.4.0" 404 | }, 405 | "dependencies": { 406 | "statuses": { 407 | "version": "1.4.0", 408 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", 409 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" 410 | } 411 | } 412 | }, 413 | "serve-static": { 414 | "version": "1.13.2", 415 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", 416 | "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", 417 | "requires": { 418 | "encodeurl": "~1.0.2", 419 | "escape-html": "~1.0.3", 420 | "parseurl": "~1.3.2", 421 | "send": "0.16.2" 422 | } 423 | }, 424 | "setprototypeof": { 425 | "version": "1.1.0", 426 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", 427 | "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" 428 | }, 429 | "statuses": { 430 | "version": "1.5.0", 431 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 432 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 433 | }, 434 | "type-is": { 435 | "version": "1.6.16", 436 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", 437 | "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", 438 | "requires": { 439 | "media-typer": "0.3.0", 440 | "mime-types": "~2.1.18" 441 | } 442 | }, 443 | "unpipe": { 444 | "version": "1.0.0", 445 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 446 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 447 | }, 448 | "utils-merge": { 449 | "version": "1.0.1", 450 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 451 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 452 | }, 453 | "vary": { 454 | "version": "1.1.2", 455 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 456 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 457 | } 458 | } 459 | } 460 | -------------------------------------------------------------------------------- /backend-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello_js_node_app", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "body-parser": "^1.18.3", 6 | "cookie-parser": "~1.4.3", 7 | "express": "^4.16.3", 8 | "kotlin": "~1.2.10", 9 | "node-fetch": "^2.2.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend-js/src/main/kotlin/com/wadejensen/example/main.kt: -------------------------------------------------------------------------------- 1 | package com.wadejensen.example 2 | 3 | import com.wadejensen.example.model.Person 4 | import org.w3c.fetch.Response 5 | import pappel.Application 6 | import pappel.await 7 | import pappel.* 8 | import pappel.http.Method 9 | import pappel.http.RequestInit 10 | import kotlin.js.Promise 11 | 12 | external val process: dynamic 13 | external val __dirname: dynamic 14 | 15 | /** 16 | * main function for JavaScript 17 | */ 18 | fun main(vararg args: String) { 19 | //nothing here 20 | } 21 | 22 | /** 23 | * We start this function from app.js" 24 | */ 25 | fun start() { 26 | val shared = SharedClass(Console(), Math()) 27 | val fetch = require("node-fetch") 28 | 29 | val app = Application() 30 | 31 | app.get("/primes") { _, _ -> 32 | shared.platform = "Node.js" 33 | shared.printMe() 34 | println(shared.givePrimes(100)) 35 | } 36 | 37 | /** 38 | * Express route handler listening on `https://hostname:port/async-get`. 39 | * Makes a simple HTTP GET request asyncronously using the w3c window.fetch API. 40 | * Uses a Kotlin coroutine wrapper around native JS `Promise`s, to mimic the ES7 async - await pattern. 41 | */ 42 | app.get("/async-get") { _, res -> 43 | println("async-get route pinged!") 44 | 45 | async { 46 | val resp: Response = await { fetch("https://jsonplaceholder.typicode.com/todos/1") } 47 | val data: Any? = await { resp.json() } 48 | 49 | data?.also { console.dir(data) } 50 | res.send(JSON.stringify(data)) 51 | } 52 | } 53 | 54 | /** 55 | * Express route handler listening on `https://hostname:port/async-post`. 56 | * Makes a HTTP POST request asyncronously using the w3c window.fetch API. 57 | * The fetch API is called from a typed wrapper which accepts Kotlin data class objects 58 | * as the message body, a Map for the headers, and an enum for the request type. 59 | * Uses a Kotlin coroutine wrapper around native JS `Promise`s, to mimic the ES7 async - await pattern. 60 | */ 61 | app.get("/async-post") { _, res -> 62 | println("async-post route pinged") 63 | 64 | val wade = "{\"name\":\"Wade Jensen\", \"age\": 22, \"address\": {\"streetNum\": 123, \"streetName\": \"Fake street\", \"suburb\": \"Surry Hills\", \"postcode\": 2010}}" 65 | val person: Person = JSON.parse(wade) 66 | async { 67 | val request = RequestInit( 68 | method = Method.POST, 69 | headers = mapOf("username" to "wjensen", "password" to "1234567"), 70 | body = person) 71 | 72 | println("Request object:") 73 | console.dir(request) 74 | 75 | val resp = await { fetch("https://jsonplaceholder.typicode.com/posts", request) } 76 | val data: Any? = await { resp.json() } 77 | data?.also { 78 | println("Response object:") 79 | console.dir(data) 80 | } 81 | res.send(JSON.stringify(data)) 82 | } 83 | } 84 | 85 | /** Express route handler listening on `https://hostname:port/parse-json`. 86 | * Parses a JSON string into a Kotlin object (POJO) and then accesses fields in a type-safe way, sending the result 87 | * in a text format back to the browser of the requester. 88 | */ 89 | app.get("/parse-json") { _, res -> 90 | println("parse-json route pinged") 91 | 92 | val data = "{\"name\":\"Wade Jensen\", \"age\": 22, \"address\": {\"streetNum\": 123, \"streetName\": \"Fake street\", \"suburb\": \"Surry Hills\", \"postcode\": 2010}}" 93 | println(data) 94 | val person: Person = JSON.parse(data) 95 | res.send(""" 96 | name = ${person.name}, 97 | age = ${person.age}, 98 | 99 | address.streetNum = ${person.address.streetNum}, 100 | address.streetName = ${person.address.streetName}, 101 | address.suburb = ${person.address.suburb}, 102 | address.postcode = ${person.address.postcode} 103 | """.trimIndent()) 104 | } 105 | 106 | /** 107 | * Express route handler listening on `https://hostname:port/promise-get`. 108 | * Makes a simple HTTP GET request asyncronously using the w3c window.fetch API. 109 | * Handle the result using native JS `Promise`s, then send the result as a webpage response. 110 | */ 111 | app.get("/promise-get") { _, res -> 112 | println("promise-get route pinged!") 113 | 114 | val resp: Promise = fetch("https://jsonplaceholder.typicode.com/todos/1") 115 | resp 116 | .then { result: Response -> result.json() } 117 | .then { json -> JSON.stringify(json) } 118 | .then { strResult -> 119 | println(strResult) 120 | res.send(strResult) 121 | } 122 | } 123 | 124 | app.startHttpServer(3000) 125 | val path = require("path") 126 | 127 | val staticWebContentPath = path.join(__dirname, "../../frontend-js/src/main/web") as String 128 | println("Serving content from: $staticWebContentPath") 129 | app.serveStaticContent(staticWebContentPath) 130 | 131 | println("Kotlin - Node.js webserver ready.") 132 | } 133 | -------------------------------------------------------------------------------- /backend-js/src/main/kotlin/com/wadejensen/example/model/Address.kt: -------------------------------------------------------------------------------- 1 | package com.wadejensen.example.model 2 | 3 | data class Address(val streetNum: Int, val streetName: String, val suburb: String, val postcode: Int) 4 | -------------------------------------------------------------------------------- /backend-js/src/main/kotlin/com/wadejensen/example/model/Person.kt: -------------------------------------------------------------------------------- 1 | package com.wadejensen.example.model 2 | 3 | data class Person(val name: String, val age: Int, val address: Address) 4 | -------------------------------------------------------------------------------- /backend-js/src/main/kotlin/pappel/Application.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from Raphael Stäbler's Pappel Node.js framework for Kotlin 3 | * https://github.com/blazer82/pappel-framework 4 | */ 5 | 6 | package pappel 7 | 8 | import pappel.http.Request 9 | import pappel.http.Response 10 | import kotlin.js.Promise 11 | external val process: dynamic 12 | external val __dirname: dynamic 13 | 14 | /** 15 | * Typed and slightly simplified wrapper around the popular Express middleware application used in node.js 16 | * https://expressjs.com/en/api.html#app 17 | * 18 | * Tweaked from the work of Raphael Stäbler 19 | * https://medium.com/@raphaelstbler/how-i-wrote-a-full-stack-webapp-for-node-js-and-react-with-kotlin-bd18c45ee517 20 | * See from https://github.com/blazer82/pappel-framework 21 | */ 22 | class Application { 23 | private val express: dynamic = require("express") 24 | private val app: dynamic = express() 25 | private val router: Router = express.Router() 26 | 27 | fun startHttpServer(port: Int): Unit { 28 | println("Starting server on port ${port}.") 29 | val bodyParser = require("body-parser") 30 | app.use(bodyParser.raw()) 31 | val http = require("http") 32 | http.createServer(this.app) 33 | listen(port) // TODO accept callback lambda 34 | println("Server started successfully") 35 | } 36 | 37 | /** 38 | * Starts listening on [port]. 39 | * @param port TCP port to listen on. 40 | * @return Promise 41 | */ 42 | fun listen(port: Int): dynamic = app.listen(port) // TODO: Add error handling 43 | 44 | /** 45 | * Handles all requests for [path]. 46 | * @param path Path relative to the router's base path 47 | * @param callback Callback to handle requests 48 | */ 49 | fun all(path: String, callback: (request: Request, response: Response) -> Unit) { 50 | app.all(path) { 51 | req, res -> callback.invoke(Request(req), Response(res)) 52 | } 53 | } 54 | 55 | /** 56 | * Handles DELETE requests for [path]. 57 | * @param path Path relative to the router's base path 58 | * @param callback Callback to handle requests 59 | */ 60 | fun delete(path: String, callback: (request: Request, response: Response) -> Unit) { 61 | app.delete(path) { 62 | req, res -> callback.invoke(Request(req), Response(res)) 63 | } 64 | } 65 | 66 | /** 67 | * Handles GET requests for [path]. 68 | * @param path Path relative to the router's base path 69 | * @param callback Callback to handle requests 70 | */ 71 | fun get(path: String, callback: (request: Request, response: Response) -> Unit) { 72 | app.get(path) { 73 | req, res -> callback.invoke(Request(req), Response(res)) 74 | } 75 | } 76 | 77 | /** 78 | * Registers a global request [callback]. 79 | * @param callback Callback to handle requests 80 | */ 81 | fun onRequest(callback: (request: Request, response: Response, next: () -> Unit) -> Unit) { 82 | app.use { 83 | req, res, n -> callback.invoke(Request(req), Response(res), n as () -> Unit) 84 | } 85 | } 86 | 87 | /** 88 | * Handles POST requests for [path]. 89 | * @param path Path relative to the router's base path 90 | * @param callback Callback to handle requests 91 | */ 92 | fun post(path: String, callback: (request: Request, response: Response) -> Unit) { 93 | app.post(path) { 94 | req, res -> callback.invoke(Request(req), Response(res)) 95 | } 96 | } 97 | 98 | /** 99 | * Handles PUT requests for [path]. 100 | * @param path Path relative to the router's base path 101 | * @param callback Callback to handle requests 102 | */ 103 | fun put(path: String, callback: (request: Request, response: Response) -> Unit) { 104 | app.put(path) { 105 | req, res -> callback.invoke(Request(req), Response(res)) 106 | } 107 | } 108 | 109 | /** 110 | * Uses [router] for [path]. 111 | * @param path Path relative to the router's base path 112 | * @param router Instance of another router to use for [path] 113 | */ 114 | fun use(path: String, router: Router) { 115 | app.use(path, router.expressRouter) 116 | } 117 | 118 | /** 119 | * Enables serving of static content beneath the specified filepath 120 | */ 121 | fun serveStaticContent(path: String) = app.use(express.static(path)) 122 | } -------------------------------------------------------------------------------- /backend-js/src/main/kotlin/pappel/JSONUtils.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from Raphael Stäbler's Pappel Node.js framework for Kotlin 3 | * https://github.com/blazer82/pappel-framework 4 | */ 5 | 6 | package pappel 7 | 8 | import kotlin.js.Json 9 | import kotlin.js.json 10 | 11 | /** 12 | * JSON utilities for easier conversion of types between JavaScript and Kotlin. 13 | * 14 | * Most of the time these functions will be directly incorporated into the framework 15 | * methods and won't be used in any application code. 16 | */ 17 | class JSONUtils { 18 | 19 | companion object { 20 | 21 | /** 22 | * Converts a map of [data] into JSON. 23 | * @return Json 24 | */ 25 | fun toJSON(data: Map): Json { 26 | val arrayOfPairs: Array> = data.map { 27 | entry -> 28 | when { 29 | entry.value is Map<*, *> -> { 30 | Pair(entry.key, toJSON(entry.value as Map)) 31 | } 32 | entry.value is Iterable<*> -> { 33 | Pair(entry.key, toJSON(entry.value as Iterable)) 34 | } 35 | else -> { 36 | entry.toPair() 37 | } 38 | } 39 | }.toTypedArray() 40 | 41 | return json(*arrayOfPairs) 42 | } 43 | 44 | /** 45 | * Converts any iterable [data] into JSON. 46 | * @return Json 47 | */ 48 | fun toJSON(data: Iterable): Array { 49 | val array: Array = data.map { 50 | entry -> 51 | if (entry is Map<*,*>) { 52 | toJSON(entry as Map) 53 | } 54 | else { 55 | entry 56 | } 57 | }.toTypedArray() 58 | 59 | return array 60 | } 61 | 62 | /** 63 | * Converts any JSON input data to a map. 64 | * @return Map 65 | */ 66 | fun retrieveMap(json: Any): Map? { 67 | if (jsTypeOf(json) != "object") { 68 | return null 69 | } 70 | 71 | val map: MutableMap = mutableMapOf() 72 | 73 | val keys = js("Object.keys(json)") as Array 74 | 75 | for (key in keys) { 76 | when { 77 | js("typeof json[key]") == "object" -> { 78 | map.put(key, retrieveMap(js("json[key]") as Any)) 79 | } 80 | else -> { 81 | map.put(key, js("json[key]") as String) 82 | } 83 | } 84 | } 85 | 86 | return map 87 | } 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /backend-js/src/main/kotlin/pappel/NodeJs.kt: -------------------------------------------------------------------------------- 1 | package pappel 2 | 3 | external fun require(module: String): dynamic -------------------------------------------------------------------------------- /backend-js/src/main/kotlin/pappel/Router.kt: -------------------------------------------------------------------------------- 1 | package pappel 2 | 3 | /** 4 | * Typed wrapper around the popular Express middleware router used in node.js 5 | * https://expressjs.com/en/api.html#router 6 | * 7 | * Tweaked from the work of Raphael Stäbler 8 | * https://medium.com/@raphaelstbler/how-i-wrote-a-full-stack-webapp-for-node-js-and-react-with-kotlin-bd18c45ee517 9 | * See from https://github.com/blazer82/pappel-framework 10 | */ 11 | data class Router(val expressRouter: dynamic) 12 | 13 | -------------------------------------------------------------------------------- /backend-js/src/main/kotlin/pappel/async.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from Raphael Stäbler's Pappel Node.js framework for Kotlin 3 | * https://github.com/blazer82/pappel-framework 4 | */ 5 | 6 | package pappel 7 | 8 | import kotlin.coroutines.experimental.* 9 | import kotlin.js.Promise 10 | 11 | /** 12 | * Async functionality for async/pappel.await ability. 13 | * 14 | * Starts an asynchronous execution [block] usually used in an async/pappel.await construct. 15 | */ 16 | fun async(block: suspend () -> Unit) { 17 | block.startCoroutine(StandaloneCoroutine(EmptyCoroutineContext)) 18 | } 19 | 20 | /** 21 | * Await functionality for async/pappel.await ability. 22 | * 23 | * Suspends current asychronous execution block and awaits the resolution of a Promise<[T]>. 24 | * Must be used within an [async] block. 25 | */ 26 | suspend fun await(block: () -> Promise): T = suspendCoroutine { 27 | continuation -> 28 | block().then { 29 | value -> 30 | continuation.resume(value) 31 | }.catch { 32 | error -> continuation.resumeWithException(error) 33 | } 34 | 35 | } 36 | 37 | private class StandaloneCoroutine(override val context: CoroutineContext): Continuation { 38 | override fun resume(value: Unit) {} 39 | 40 | override fun resumeWithException(error: Throwable) {} 41 | } 42 | 43 | //suspend fun Promise.await(): T = suspendCoroutine { cont -> 44 | // then({ cont.resume(it) }, { cont.resumeWithException(it) }) 45 | //} 46 | // 47 | //fun launch(block: suspend () -> Unit) { 48 | // block.startCoroutine(object : Continuation { 49 | // override val context: CoroutineContext get() = EmptyCoroutineContext 50 | // override fun resume(value: Unit) {} 51 | // override fun resumeWithException(e: Throwable) { console.log("Coroutine failed: $e") } 52 | // }) 53 | //} -------------------------------------------------------------------------------- /backend-js/src/main/kotlin/pappel/fetch.kt: -------------------------------------------------------------------------------- 1 | package pappel 2 | 3 | import org.w3c.fetch.RequestInit 4 | import org.w3c.fetch.Response 5 | import kotlin.js.Promise 6 | 7 | fun fetch(input: dynamic): Promise { 8 | return Fetch.jsFetch(input) as Promise 9 | } 10 | 11 | fun fetch(input: dynamic, init: RequestInit): Promise { 12 | return Fetch.jsFetch(input, init.asDynamic()) 13 | } 14 | 15 | object Fetch { 16 | val jsFetch: dynamic = require("node-fetch") 17 | } 18 | -------------------------------------------------------------------------------- /backend-js/src/main/kotlin/pappel/http/Method.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from Raphael Stäbler's Pappel Node.js framework for Kotlin 3 | * https://github.com/blazer82/pappel-framework 4 | */ 5 | 6 | package pappel.http 7 | 8 | /** 9 | * Enum type for HTTP methods. 10 | */ 11 | enum class Method(val value: String) { 12 | GET("GET"), 13 | POST("POST"), 14 | PUT("PUT"), 15 | DELETE("DELETE"), 16 | } -------------------------------------------------------------------------------- /backend-js/src/main/kotlin/pappel/http/Protocol.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from Raphael Stäbler's Pappel Node.js framework for Kotlin 3 | * https://github.com/blazer82/pappel-framework 4 | */ 5 | 6 | package pappel.http 7 | 8 | /** 9 | * Enum type for HTTP protocols. 10 | */ 11 | enum class Protocol(val value: String) { 12 | HTTP("http"), 13 | HTTPS("https"), 14 | } -------------------------------------------------------------------------------- /backend-js/src/main/kotlin/pappel/http/Request.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from Raphael Stäbler's Pappel Node.js framework for Kotlin 3 | * https://github.com/blazer82/pappel-framework 4 | */ 5 | 6 | package pappel.http 7 | 8 | import pappel.JSONUtils 9 | 10 | class Request(external private val req: dynamic) { 11 | 12 | val baseURL: String 13 | val body: Map? 14 | val cookies: Map? 15 | val hostname: String 16 | val ip: String 17 | val ips: Array? 18 | val method: Method 19 | val parameters: Map? 20 | val path: String 21 | val protocol: Protocol 22 | val query: Map? 23 | // TODO: Add route 24 | 25 | init { 26 | baseURL = req.baseUrl as String 27 | body = null 28 | cookies = null 29 | hostname = req.hostname as String 30 | ip = req.ip as String 31 | ips = req.ips as? Array 32 | method = Method.valueOf(req.method as String) 33 | parameters = JSONUtils.retrieveMap(req.params) as? Map 34 | path = req.path as String 35 | protocol = Protocol.valueOf((req.protocol as String).toUpperCase()) 36 | query = JSONUtils.retrieveMap(req.query) as? Map 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /backend-js/src/main/kotlin/pappel/http/RequestInit.kt: -------------------------------------------------------------------------------- 1 | package pappel.http 2 | 3 | import org.w3c.fetch.* 4 | import pappel.JSONUtils 5 | import kotlin.js.json 6 | 7 | /** 8 | * A fake constructor for a dynamic JS RequestInit object. 9 | * Primarily used in calls to [[org.w3c.fetch]] 10 | */ 11 | fun RequestInit( 12 | method: Method = Method.GET, 13 | headers: Map? = null, 14 | body: Any? = null, 15 | referrer: String? = null, 16 | referrerPolicy: dynamic = null, 17 | mode: RequestMode? = null, 18 | credentials: RequestCredentials? = null, 19 | cache: RequestCache? = null, 20 | redirect: RequestRedirect? = null, 21 | integrity: String? = null, 22 | keepalive: Boolean? = null, 23 | window: Any? = null): RequestInit { 24 | 25 | val o = js("({})") 26 | 27 | o["method"] = method.value 28 | o["headers"] = headers 29 | ?.entries 30 | ?.map { it.toPair() } 31 | ?.toTypedArray() 32 | ?.let { json(*it) } 33 | o["body"] = JSON.stringify(body) 34 | o["referrer"] = referrer 35 | o["referrerPolicy"] = referrerPolicy 36 | o["mode"] = mode 37 | o["credentials"] = credentials 38 | o["cache"] = cache 39 | o["redirect"] = redirect 40 | o["integrity"] = integrity 41 | o["keepalive"] = keepalive 42 | o["window"] = window 43 | 44 | return o 45 | } 46 | -------------------------------------------------------------------------------- /backend-js/src/main/kotlin/pappel/http/Response.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from Raphael Stäbler's Pappel Node.js framework for Kotlin 3 | * https://github.com/blazer82/pappel-framework 4 | */ 5 | 6 | package pappel.http 7 | 8 | import pappel.JSONUtils 9 | 10 | /** 11 | * HTTP Response class. 12 | * 13 | * Encapsulates HTTP responses as used within [Router] callbacks. 14 | * @constructor Creates a new Response based on an expressjs response. 15 | */ 16 | class Response(external private val res: dynamic) { 17 | 18 | /** 19 | * Ends response. 20 | * May be called instead of [send] or [sendJSON]. 21 | */ 22 | fun end() { 23 | res.end() 24 | } 25 | 26 | /** 27 | * Renders [view] and sends it to the client. 28 | * @param view Relative path to view 29 | */ 30 | fun render(view: String) { 31 | res.render("$view.html") 32 | } 33 | 34 | /** 35 | * Renders [view] and sends it to [callback]. 36 | * @param view Relative path to view 37 | * @param callback Callback to process rendered view in 38 | */ 39 | fun render(view: String, callback: (String?) -> Unit) { 40 | res.render("$view.html") { 41 | _, html -> callback.invoke(html as? String) 42 | } 43 | } 44 | 45 | /** 46 | * Renders [view] using [parameters] and sends it to the client. 47 | * @param view Relative path to view 48 | * @param parameters Parameters to pass through to view template 49 | */ 50 | fun render(view: String, parameters: Map) { 51 | res.render("$view.html", JSONUtils.toJSON(parameters)) 52 | } 53 | 54 | /** 55 | * Renders [view] using [parameters] and sends it to [callback]. 56 | * @param view Relative path to view 57 | * @param parameters Parameters to pass through to view template 58 | * @param callback Callback to process rendered view in 59 | */ 60 | fun render(view: String, parameters: Map, callback: (String?) -> Unit) { 61 | res.render("$view.html", JSONUtils.toJSON(parameters)) { 62 | _, html -> callback.invoke(html as? String) 63 | } 64 | } 65 | 66 | /** 67 | * Sends response [string]. 68 | * @param string String to send to the client 69 | */ 70 | fun send(string: String) { 71 | res.send(string) 72 | } 73 | 74 | /** 75 | * Sends [data] response as JSON. 76 | * @param data Map to send to the client as JSON 77 | */ 78 | fun sendJSON(data: Map) { 79 | res.json(JSONUtils.toJSON(data)) 80 | } 81 | 82 | /** 83 | * Sends [data] response as JSON. 84 | * @param data Iterable to send to the client as JSON 85 | */ 86 | fun sendJSON(data: Iterable) { 87 | res.json(JSONUtils.toJSON(data)) 88 | } 89 | 90 | /** 91 | * Sets content [type] HTTP header. 92 | * @param type Content type string 93 | */ 94 | fun setContentType(type: String) { 95 | res.type(type) 96 | } 97 | 98 | /** 99 | * Sets HTTP header [field] to [value]. 100 | * @param field Header field name 101 | * @param value Header field value 102 | */ 103 | fun setHeader(field: String, value: String) { 104 | res.append(field, value) 105 | } 106 | 107 | /** 108 | * Sets multiple HTTP headers. 109 | * @param fields Header field names and values 110 | */ 111 | fun setHeaders(fields: Map) { 112 | fields.forEach { 113 | entry -> res.append(entry.key, entry.value) 114 | } 115 | } 116 | 117 | /** 118 | * Sets HTTP [status] 119 | * @param status [Status] 120 | */ 121 | fun setStatus(status: Status) { 122 | res.status(status.code) 123 | } 124 | 125 | } -------------------------------------------------------------------------------- /backend-js/src/main/kotlin/pappel/http/Status.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from Raphael Stäbler's Pappel Node.js framework for Kotlin 3 | * https://github.com/blazer82/pappel-framework 4 | */ 5 | 6 | package pappel.http 7 | 8 | enum class Status(val code: Int) { 9 | CONTINUE(100), 10 | SWITCHING_PROTOCOLS(101), 11 | PROCESSING(102), 12 | 13 | OK(200), 14 | CREATED(201), 15 | ACCEPTED(202), 16 | NON_AUTHORITATIVE_INFORMATION(203), 17 | NO_CONTENT(204), 18 | RESET_CONTENT(205), 19 | PARTIAL_CONTENT(206), 20 | 21 | MULTIPLE_CHOICES(300), 22 | MOVED_PERMANENTLY(301), 23 | FOUND(302), 24 | SEE_OTHER(303), 25 | USE_PROXY(305), 26 | SWITCH_PROXY(306), 27 | TEMPORARY_REDIRECT(307), 28 | PERMANENT_REDIRECT(308), 29 | 30 | BAD_REQUEST(400), 31 | UNAUTHORIZED(401), 32 | PAYMENT_REQUIRED(402), 33 | FORBIDDEN(403), 34 | NOT_FOUND(404), 35 | METHOD_NOT_ALLOWED(405), 36 | NOT_ACCEPTABLE(406), 37 | PROXY_AUTHENTICATION_REQUIRED(407), 38 | REQUEST_TIMEOUT(408), 39 | CONFLICT(409), 40 | GONE(410), 41 | LENGTH_REQUIRED(411), 42 | PRECONDITION_FAILED(412), 43 | PAYLOAD_TOO_LARGE(413), 44 | URI_TOO_LONG(414), 45 | UNSUPPORTED_MEDIA_TYPE(415), 46 | RANGE_NOT_SATISFIABLE(416), 47 | EXPECTATION_FAILED(417), 48 | MISDIRECTED_REQUEST(419), 49 | UPGRADE_REQUIRED(426), 50 | PRECONDITION_REQUIRED(428), 51 | TOO_MANY_REQUESTS(429), 52 | REQUEST_HEADER_FIELDS_TOO_LARGE(431), 53 | UNAVAILABLE_FOR_LEGAL_REASONS(451), 54 | 55 | INTERNAL_SERVER_ERROR(500), 56 | NOT_IMPLEMENTED(501), 57 | BAD_GATEWAY(502), 58 | SERVICE_UNAVAILABLE(503), 59 | GATEWAY_TIMEOUT(504), 60 | HTTP_VERSION_NOT_SUPPORTED(505), 61 | VARIANT_ALSO_NEGOTIATES(506), 62 | NOT_EXTENDED(510), 63 | NETWORK_AUTHENTICATION_REQUIRED(511), 64 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | 4 | ext { 5 | kotlin_version = "1.2.60" 6 | serialization_version = "0.6.1" 7 | } 8 | 9 | repositories { 10 | jcenter() 11 | mavenCentral() 12 | google() 13 | maven { url 'http://oss.sonatype.org/content/repositories/snapshots' } 14 | maven { url "https://plugins.gradle.org/m2/" } 15 | maven { url "https://dl.bintray.com/kotlin/kotlin-eap" } 16 | maven { url "https://kotlin.bintray.com/kotlinx" } 17 | } 18 | 19 | dependencies { 20 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 21 | classpath "com.moowork.gradle:gradle-node-plugin:1.2.0" 22 | classpath "org.jetbrains.kotlin:kotlin-frontend-plugin:0.0.31" 23 | } 24 | } 25 | 26 | allprojects { 27 | repositories { 28 | jcenter() 29 | google() 30 | maven { url 'http://dl.bintray.com/kotlin/kotlin-eap-1.2' } 31 | maven { url 'https://jitpack.io' } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /common-js/README.md: -------------------------------------------------------------------------------- 1 | #### A common Kotlin module which can be shared between JS targets on both the frontend and backend. 2 | #### Can be used a a dependency for Kotlin JS in the browser, as well as Node.js 3 | 4 | Useful for defining JS specific code, eg. networking, file system interaction, Promises 5 | 6 | The example module exposes a `SharedClass` which calculates and prints prime numbers. 7 | The implementation of printing and the math library are passed as interfaces 8 | to show how they can be overridden in the platform-specific libs. 9 | See `com.wadejensen.example.console` and `com.wadejensen.example.Math` in `common-js` to see 10 | ```kt 11 | class SharedClass(val console: IConsole, val math: IMath) { 12 | var platform: String = "" 13 | fun printMe() 14 | fun printPrimes(n: Long) 15 | fun givePrimes(n: Long): List 16 | ``` 17 | 18 | Note: It is possible to use the default `println` and `kotlin.math.sqrt` which Kotlin provides. 19 | These pieces of functionality are just passed as parameters to motivate the example project. -------------------------------------------------------------------------------- /common-js/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin-platform-js' 2 | 3 | dependencies { 4 | compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version" 5 | // Tells Gradle that `common-js` provides a platform-specific implementation expected by common 6 | expectedBy project(':common') 7 | testCompile "org.jetbrains.kotlin:kotlin-test-js:$kotlin_version" 8 | } 9 | 10 | compileKotlin2Js { 11 | kotlinOptions.metaInfo = true 12 | kotlinOptions.sourceMap = true 13 | kotlinOptions.suppressWarnings = true 14 | kotlinOptions.verbose = true 15 | kotlinOptions.moduleKind = "umd" 16 | } 17 | 18 | compileTestKotlin2Js { 19 | kotlinOptions.metaInfo = true 20 | kotlinOptions.sourceMap = true 21 | kotlinOptions.suppressWarnings = true 22 | kotlinOptions.verbose = true 23 | kotlinOptions.moduleKind = "umd" 24 | } 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /common-js/src/main/kotlin/com/wadejensen/example/Console.kt: -------------------------------------------------------------------------------- 1 | package com.wadejensen.example 2 | 3 | // The `actual` keyword declares this is the implementation `expect`ed by the common platform module 4 | // which satisfies the interface IConsole for the js platform target 5 | // (see `com.wadejensen.example.Console` in `common`). 6 | // https://kotlinlang.org/docs/reference/multiplatform.html#platform-specific-declarations 7 | /** 8 | * Console that logs in JS console object. 9 | */ 10 | actual class Console : IConsole { 11 | 12 | 13 | actual override fun println(s: String) { 14 | //prints text by adding it to the 'console' element 15 | console.log(s) 16 | } 17 | } -------------------------------------------------------------------------------- /common-js/src/main/kotlin/com/wadejensen/example/Math.kt: -------------------------------------------------------------------------------- 1 | package com.wadejensen.example 2 | 3 | // The `actual` keyword declares this is the implementation `expect`ed by the common platform module 4 | // which satisfies the interface IConsole for the js platform target 5 | // (see `com.wadejensen.example.Console` in `common`). 6 | // https://kotlinlang.org/docs/reference/multiplatform.html#platform-specific-declarations 7 | actual class Math : IMath { 8 | // Dynamically `eval`uate JS code within Kotlin to get the JS Math singleton object 9 | private val mathJs: dynamic = js("Math") 10 | // Call to JS on dynamic object 11 | actual override fun sqrt(x: Double): Double = mathJs.sqrt(x) 12 | } -------------------------------------------------------------------------------- /common-js/src/main/kotlin/com/wadejensen/example/main.kt: -------------------------------------------------------------------------------- 1 | package com.wadejensen.example 2 | 3 | /** 4 | * main function for JavaScript 5 | */ 6 | fun main(vararg args: String) { 7 | //nothing here, it's executed before DOM is ready 8 | } 9 | 10 | /** 11 | * We start this function from 12 |
13 | 
14 | 15 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4608M 2 | org.gradle.configureondemand=false 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wadejensen/kotlin-nodejs-example/71f6115d0ebac26e5de91bd80c6ae44f9b21c974/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Jun 23 18:33:15 CEST 2018 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.6-all.zip 7 | `\#Mon=Feb 15 20\:19\:15 CET 2016 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 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 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 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 Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "undefined": { 6 | "version": "0.1.0", 7 | "resolved": "https://registry.npmjs.org/undefined/-/undefined-0.1.0.tgz", 8 | "integrity": "sha1-m3BqSzKtMMIMpP5l3cu72sMr3tA=" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':common' // Module which contains platform independent logic and models 2 | include ':common-js' // Module which contains js specific code 3 | // include 'common_jvm' // You could have a JVM specific module here 4 | // include 'android' // You could have an Android specific module here 5 | include ':frontend-js' // Module which contains code specific to js on the browser 6 | include ':backend-js' // Module which contains code specific to js on node 7 | --------------------------------------------------------------------------------