├── .circleci └── config.yml ├── .gitignore ├── CHECKS ├── CONTRIBUTING ├── LICENSE ├── Procfile ├── README.md ├── backend ├── build.gradle └── src │ ├── main │ ├── kotlin │ │ └── org │ │ │ └── example │ │ │ └── kotlin │ │ │ └── multiplatform │ │ │ └── backend │ │ │ ├── Application.kt │ │ │ ├── HelloRoute.kt │ │ │ ├── IndexRoute.kt │ │ │ └── Locations.kt │ └── resources │ │ ├── application.conf │ │ ├── index.html │ │ └── logback.xml │ └── test │ └── resources │ └── mockito-extensions │ └── org.mockito.plugins.MockMaker ├── build.gradle ├── buildSrc ├── build.gradle └── src │ └── main │ └── java │ └── Dependencies.kt ├── common ├── build.gradle └── src │ ├── commonDebug │ └── kotlin │ │ └── org │ │ └── example │ │ └── kotlin │ │ └── multiplatform │ │ └── data │ │ └── defaultNetworkConfig.kt │ ├── commonMain │ └── kotlin │ │ └── org │ │ └── example │ │ └── kotlin │ │ └── multiplatform │ │ ├── api │ │ └── Api.kt │ │ ├── data │ │ ├── NetworkDataSource.kt │ │ ├── model │ │ │ └── Greeting.kt │ │ └── responses │ │ │ ├── ErrorResponse.kt │ │ │ └── HelloResponse.kt │ │ └── repository │ │ ├── NetworkRepository.kt │ │ └── model │ │ └── Greeting.kt │ └── commonRelease │ └── kotlin │ └── org │ └── example │ └── kotlin │ └── multiplatform │ └── data │ └── defaultNetworkConfig.kt ├── detekt.yml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── web ├── build.gradle ├── package.json.d └── project.info.json ├── src └── main │ ├── kotlin │ └── org │ │ └── example │ │ └── kotlin │ │ └── multiplatform │ │ └── web │ │ ├── Application.kt │ │ ├── main.kt │ │ └── repository │ │ └── WithNetworkRepository.kt │ └── resources │ └── main.css └── webpack.config.d ├── .gitignore ├── babel.js ├── bundle.js ├── css.js ├── html.js ├── minify.js └── sourcemap.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | defaults: &defaults 2 | working_directory: ~/code 3 | docker: 4 | - image: circleci/openjdk:8-jdk 5 | environment: 6 | JVM_OPTS: -Xmx3200m 7 | GRADLE_OPTS: '-Dorg.gradle.daemon=false' 8 | _JAVA_OPTIONS: "-Xms256m -Xmx1280m -XX:MaxPermSize=350m" 9 | 10 | version: 2 11 | jobs: 12 | lint: 13 | <<: *defaults 14 | steps: 15 | - checkout 16 | - restore_cache: 17 | key: jars--{{ checksum "build.gradle" }}-{{ checksum "backend/build.gradle" }}-{{ checksum "web/build.gradle" }} 18 | - run: 19 | name: Lint project 20 | command: ./gradlew -Porg.gradle.project.buildAndroid=false detekt 21 | - save_cache: 22 | paths: 23 | - ~/.gradle 24 | key: jars--{{ checksum "build.gradle" }}-{{ checksum "backend/build.gradle" }}-{{ checksum "web/build.gradle" }} 25 | - run: 26 | name: Save detekt results 27 | command: | 28 | mkdir -p ~/detekt/ 29 | find . -type f -regex ".*/build/reports/detekt/*" -exec cp {} ~/detekt/ \; 30 | when: always 31 | - store_test_results: 32 | path: ~/detekt 33 | - store_artifacts: 34 | path: ~/detekt 35 | 36 | build-backend: 37 | <<: *defaults 38 | steps: 39 | - checkout 40 | - restore_cache: 41 | key: jars--{{ checksum "build.gradle" }}-{{ checksum "backend/build.gradle" }} 42 | - run: 43 | name: Build project 44 | command: ./gradlew -Porg.gradle.project.buildAndroid=false :backend:assemble --stacktrace 45 | - store_artifacts: 46 | path: backend/build/libs 47 | - run: 48 | name: Run Tests 49 | command: ./gradlew -Porg.gradle.project.buildAndroid=false :backend:test --stacktrace 50 | - run: 51 | name: Save test results 52 | command: | 53 | mkdir -p ~/junit/ 54 | find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/junit/ \; 55 | when: always 56 | - save_cache: 57 | paths: 58 | - ~/.gradle 59 | key: jars--{{ checksum "build.gradle" }}-{{ checksum "backend/build.gradle" }} 60 | - store_test_results: 61 | path: ~/junit 62 | - store_artifacts: 63 | path: ~/junit 64 | 65 | build-web: 66 | <<: *defaults 67 | steps: 68 | - checkout 69 | - restore_cache: 70 | key: jars--{{ checksum "build.gradle" }}-{{ checksum "backend/build.gradle" }}-{{ checksum "web/build.gradle" }} 71 | - run: 72 | name: Build project 73 | command: ./gradlew -Porg.gradle.project.buildAndroid=false :web:assemble --stacktrace 74 | - save_cache: 75 | paths: 76 | - ~/.gradle 77 | key: jars--{{ checksum "build.gradle" }}-{{ checksum "backend/build.gradle" }}-{{ checksum "web/build.gradle" }} 78 | - store_artifacts: 79 | path: web/build/libs 80 | - run: 81 | name: Run Tests 82 | command: ./gradlew -Porg.gradle.project.buildAndroid=false :web:test --stacktrace 83 | - run: 84 | name: Save test results 85 | command: | 86 | mkdir -p ~/junit/ 87 | find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/junit/ \; 88 | when: always 89 | - store_test_results: 90 | path: ~/junit 91 | - store_artifacts: 92 | path: ~/junit 93 | 94 | deploy-backend: 95 | docker: 96 | - image: buildpack-deps:trusty 97 | steps: 98 | - checkout 99 | - run: 100 | name: Deploy Master to Heroku 101 | command: | 102 | git push $HEROKU_URL master 103 | 104 | workflows: 105 | version: 2 106 | pr-checks: 107 | jobs: 108 | - lint 109 | - build-backend 110 | - build-web 111 | # deploy: 112 | # jobs: 113 | # - deploy-backend: 114 | # filters: 115 | # branches: 116 | # only: master 117 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/web,node,java,kotlin,android,intellij,visualstudiocode,ios,fastlane,macos 3 | # Edit at https://www.gitignore.io/?templates=web,node,java,kotlin,android,intellij,visualstudiocode,ios,fastlane,macos 4 | 5 | ### Android ### 6 | # Built application files 7 | *.apk 8 | *.ap_ 9 | *.aab 10 | 11 | # Files for the ART/Dalvik VM 12 | *.dex 13 | 14 | # Java class files 15 | *.class 16 | 17 | # Generated files 18 | bin/ 19 | gen/ 20 | out/ 21 | 22 | # Gradle files 23 | .gradle/ 24 | build/ 25 | 26 | # Local configuration file (sdk path, etc) 27 | local.properties 28 | 29 | # Proguard folder generated by Eclipse 30 | proguard/ 31 | 32 | # Log Files 33 | *.log 34 | 35 | # Android Studio Navigation editor temp files 36 | .navigation/ 37 | 38 | # Android Studio captures folder 39 | captures/ 40 | 41 | # IntelliJ 42 | *.iml 43 | .idea/ 44 | 45 | # Keystore files 46 | # Uncomment the following lines if you do not want to check your keystore files in. 47 | *.jks 48 | *.keystore 49 | 50 | # External native build folder generated in Android Studio 2.2 and later 51 | .externalNativeBuild 52 | 53 | # Google Services (e.g. APIs or Firebase) 54 | google-services.json 55 | 56 | # Freeline 57 | freeline.py 58 | freeline/ 59 | freeline_project_description.json 60 | 61 | # fastlane 62 | */fastlane/report.xml 63 | */fastlane/Preview.html 64 | */fastlane/screenshots 65 | */fastlane/test_output 66 | */fastlane/README.md 67 | 68 | # Version control 69 | vcs.xml 70 | 71 | # lint 72 | */lint/intermediates/ 73 | */lint/generated/ 74 | */lint/outputs/ 75 | */lint/tmp/ 76 | */lint/reports/ 77 | 78 | ### Android Patch ### 79 | gen-external-apklibs 80 | output.json 81 | 82 | ### fastlane ### 83 | # fastlane - A streamlined workflow tool for Cocoa deployment 84 | # 85 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 86 | # screenshots whenever they are needed. 87 | # For more information about the recommended setup visit: 88 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 89 | 90 | # fastlane specific 91 | 92 | # deliver temporary files 93 | 94 | # snapshot generated screenshots 95 | */fastlane/screenshots/**/*.png 96 | */fastlane/screenshots/screenshots.html 97 | 98 | # scan temporary files 99 | 100 | ### Intellij ### 101 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 102 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 103 | 104 | # User-specific stuff 105 | .idea/**/workspace.xml 106 | .idea/**/tasks.xml 107 | .idea/**/usage.statistics.xml 108 | .idea/**/dictionaries 109 | .idea/**/shelf 110 | 111 | # Generated files 112 | .idea/**/contentModel.xml 113 | 114 | # Sensitive or high-churn files 115 | .idea/**/dataSources/ 116 | .idea/**/dataSources.ids 117 | .idea/**/dataSources.local.xml 118 | .idea/**/sqlDataSources.xml 119 | .idea/**/dynamic.xml 120 | .idea/**/uiDesigner.xml 121 | .idea/**/dbnavigator.xml 122 | 123 | # Gradle 124 | .idea/**/gradle.xml 125 | .idea/**/libraries 126 | 127 | # Gradle and Maven with auto-import 128 | # When using Gradle or Maven with auto-import, you should exclude module files, 129 | # since they will be recreated, and may cause churn. Uncomment if using 130 | # auto-import. 131 | .idea/modules.xml 132 | .idea/*.iml 133 | .idea/modules 134 | *.iml 135 | *.ipr 136 | 137 | # CMake 138 | cmake-build-*/ 139 | 140 | # Mongo Explorer plugin 141 | .idea/**/mongoSettings.xml 142 | 143 | # File-based project format 144 | *.iws 145 | 146 | # IntelliJ 147 | 148 | # mpeltonen/sbt-idea plugin 149 | .idea_modules/ 150 | 151 | # JIRA plugin 152 | atlassian-ide-plugin.xml 153 | 154 | # Cursive Clojure plugin 155 | .idea/replstate.xml 156 | 157 | # Crashlytics plugin (for Android Studio and IntelliJ) 158 | com_crashlytics_export_strings.xml 159 | crashlytics.properties 160 | crashlytics-build.properties 161 | fabric.properties 162 | 163 | # Editor-based Rest Client 164 | .idea/httpRequests 165 | 166 | # Android studio 3.1+ serialized cache file 167 | .idea/caches/build_file_checksums.ser 168 | 169 | ### Intellij Patch ### 170 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 171 | 172 | # *.iml 173 | # modules.xml 174 | # .idea/misc.xml 175 | # *.ipr 176 | 177 | # Sonarlint plugin 178 | .idea/sonarlint 179 | 180 | #!! ERROR: ios is undefined. Use list command to see defined gitignore types !!# 181 | 182 | ### Java ### 183 | # Compiled class file 184 | 185 | # Log file 186 | 187 | # BlueJ files 188 | *.ctxt 189 | 190 | # Mobile Tools for Java (J2ME) 191 | .mtj.tmp/ 192 | 193 | # Package Files # 194 | #*.jar 195 | *.war 196 | *.nar 197 | *.ear 198 | *.zip 199 | *.tar.gz 200 | *.rar 201 | 202 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 203 | hs_err_pid* 204 | 205 | ### Kotlin ### 206 | # Compiled class file 207 | 208 | # Log file 209 | 210 | # BlueJ files 211 | 212 | # Mobile Tools for Java (J2ME) 213 | 214 | # Package Files # 215 | 216 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 217 | 218 | ### macOS ### 219 | # General 220 | .DS_Store 221 | .AppleDouble 222 | .LSOverride 223 | 224 | # Icon must end with two \r 225 | Icon 226 | 227 | # Thumbnails 228 | ._* 229 | 230 | # Files that might appear in the root of a volume 231 | .DocumentRevisions-V100 232 | .fseventsd 233 | .Spotlight-V100 234 | .TemporaryItems 235 | .Trashes 236 | .VolumeIcon.icns 237 | .com.apple.timemachine.donotpresent 238 | 239 | # Directories potentially created on remote AFP share 240 | .AppleDB 241 | .AppleDesktop 242 | Network Trash Folder 243 | Temporary Items 244 | .apdisk 245 | 246 | ### Node ### 247 | # Logs 248 | logs 249 | npm-debug.log* 250 | yarn-debug.log* 251 | yarn-error.log* 252 | lerna-debug.log* 253 | 254 | # Diagnostic reports (https://nodejs.org/api/report.html) 255 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 256 | 257 | # Runtime data 258 | pids 259 | *.pid 260 | *.seed 261 | *.pid.lock 262 | 263 | # Directory for instrumented libs generated by jscoverage/JSCover 264 | lib-cov 265 | 266 | # Coverage directory used by tools like istanbul 267 | coverage 268 | *.lcov 269 | 270 | # nyc test coverage 271 | .nyc_output 272 | 273 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 274 | .grunt 275 | 276 | # Bower dependency directory (https://bower.io/) 277 | bower_components 278 | 279 | # node-waf configuration 280 | .lock-wscript 281 | 282 | # Compiled binary addons (https://nodejs.org/api/addons.html) 283 | build/Release 284 | 285 | # Dependency directories 286 | node_modules/ 287 | jspm_packages/ 288 | 289 | # TypeScript v1 declaration files 290 | typings/ 291 | 292 | # TypeScript cache 293 | *.tsbuildinfo 294 | 295 | # Optional npm cache directory 296 | .npm 297 | 298 | # Optional eslint cache 299 | .eslintcache 300 | 301 | # Optional REPL history 302 | .node_repl_history 303 | 304 | # Output of 'npm pack' 305 | *.tgz 306 | 307 | # Yarn Integrity file 308 | .yarn-integrity 309 | 310 | # dotenv environment variables file 311 | .env 312 | .env.test 313 | 314 | # parcel-bundler cache (https://parceljs.org/) 315 | .cache 316 | 317 | # next.js build output 318 | .next 319 | 320 | # nuxt.js build output 321 | .nuxt 322 | 323 | # vuepress build output 324 | .vuepress/dist 325 | 326 | # Serverless directories 327 | .serverless/ 328 | 329 | # FuseBox cache 330 | .fusebox/ 331 | 332 | # DynamoDB Local files 333 | .dynamodb/ 334 | 335 | ### VisualStudioCode ### 336 | .vscode/* 337 | !.vscode/settings.json 338 | !.vscode/tasks.json 339 | !.vscode/launch.json 340 | !.vscode/extensions.json 341 | 342 | ### VisualStudioCode Patch ### 343 | # Ignore all local history of files 344 | .history 345 | 346 | # End of https://www.gitignore.io/api/web,node,java,kotlin,android,intellij,visualstudiocode,ios,fastlane 347 | -------------------------------------------------------------------------------- /CHECKS: -------------------------------------------------------------------------------- 1 | / 2 | /logout 3 | /frontend/web.bundle.js 4 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | If you would like to contribute code to this repository you can do so through GitHub by creating a new branch in the repository and sending a pull request or opening an issue. Please, remember that there are some requirements you have to pass before accepting your contribution: 2 | 3 | * Write clean code and test it. 4 | * The code written will have to match the product owner requirements. 5 | * Follow the repository code style. 6 | * Write good commit messages. 7 | * Do not send pull requests without checking if the project build is OK in the CI environment. 8 | * Review if your changes affects the repository documentation and update it. 9 | * Describe the PR content and don't hesitate to add comments to explain us why you've added or changed something. 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: java -Dserver.port=$PORT $JAVA_OPTS -jar backend/build/libs/*-release.jar 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI Badge](https://circleci.com/gh/wiyarmir/kotlin-multiplatform-frontend-template.svg?style=svg)](https://circleci.com/gh/wiyarmir/kotlin-multiplatform-frontend-template) 2 | ![Kotlin Version](https://img.shields.io/badge/kotlin-v1.4.0-F88909?style=flat&logo=kotlin) 3 | 4 | # Kotlin Multiplatform Frontend Template 5 | 6 | Template that will give you a ready-to-go project including: 7 | 8 | - Backend project with Ktor at [backend/](/backend) 9 | - Frontend project with KotlinJS and React Kotlin at [web/](/web) 10 | - Shared network, domain and presentation at [common/](/common) 11 | 12 | *Looking for a project with Android and iOS as well? It's here: https://github.com/wiyarmir/kotlin-multiplatform-template* 13 | 14 | *Looking for a project with just Android and iOS? It's here: https://github.com/wiyarmir/kotlin-multiplatform-mobile-template* 15 | 16 | ## Building and running the project 17 | 18 | ### Backend 19 | 20 | There is a Gradle task that will produce a JAR ready to go. 21 | 22 | ```bash 23 | $ ./gradlew stage 24 | ``` 25 | 26 | The output is in `backend/build/libs/backend-release.jar`. 27 | 28 | The following environment variables are recognised: 29 | 30 | | Name | Value | 31 | |------|-------| 32 | | PORT | Port where the server will run and listen to incoming connections | 33 | | ENVIRONMENT | Can be either `production` or `development`. If absent, will assume `development`. | 34 | 35 | You can run the backend development server executing: 36 | 37 | ```bash 38 | $ ./gradlew backend:run 39 | ``` 40 | 41 | This will start serving the app in port 9090 by default. 42 | 43 | ### Web 44 | 45 | If you want to run the frontend development server, you can execute: 46 | 47 | ```bash 48 | $ ./gradlew web:run 49 | ``` 50 | 51 | This will start the webpack development server in port 8080, and proxy all calls to files it doesn't know to port 9090. 52 | 53 | If you want the frontend development server to connect to the development backend, you'll need to pass the flag `-Pdebug`. 54 | 55 | ```bash 56 | $ ./gradlew web:run -Pdebug 57 | ``` 58 | 59 | **Warning**: The webpack development server will keep running until you execute `./gradlew web:stop`. 60 | 61 | #### Hot reloading 62 | 63 | In backend, Ktor supports hot reloading, but since the task serving the app is kept alive, you need to execute in a separate console: 64 | 65 | ```bash 66 | $ ./gradlew backend:classes -t 67 | ``` 68 | 69 | This will recompilate classes on file changes, and Ktor will detect it and reload them on the next request it serves. 70 | 71 | For the frontend, it's enough to execute the original run task with `-t` flag. 72 | 73 | ```bash 74 | $ ./gradlew web:run -t 75 | ``` 76 | 77 | ## Deployment 78 | 79 | ### Backend 80 | 81 | There is a `Procfile` that will JustWork™️ on Heroku or Herokuish environments (think [Dokku](https://github.com/dokku/dokku)). 82 | 83 | ### Frontend 84 | 85 | By default, the web bundle and a default `index.html` are included in the backend jar and served at `/`. 86 | 87 | ## Contributing 88 | 89 | If you would like to contribute code to this repository you can do so through GitHub by creating a new branch in the repository and sending a pull request or opening an issue. Please, remember that there are some requirements you have to pass before accepting your contribution: 90 | 91 | * Write clean code and test it. 92 | * The code written will have to match the product owner requirements. 93 | * Follow the repository code style. 94 | * Write good commit messages. 95 | * Do not send pull requests without checking if the project build is OK in the CI environment. 96 | * Review if your changes affects the repository documentation and update it. 97 | * Describe the PR content and don't hesitate to add comments to explain us why you've added or changed something. 98 | 99 | ## License 100 | 101 | Copyright 2019 Kotlin Multiplatform Template 102 | 103 | Licensed under the Apache License, Version 2.0 (the "License"); you may 104 | not use this file except in compliance with the License. You may obtain a 105 | copy of the License at 106 | 107 | http://www.apache.org/licenses/LICENSE-2.0 108 | 109 | Unless required by applicable law or agreed to in writing, software 110 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 111 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 112 | License for the specific language governing permissions and limitations 113 | under the License. 114 | -------------------------------------------------------------------------------- /backend/build.gradle: -------------------------------------------------------------------------------- 1 | import java.nio.file.Paths 2 | 3 | apply plugin: "kotlin" 4 | apply plugin: "kotlinx-serialization" 5 | apply plugin: "application" 6 | apply plugin: "com.github.johnrengelman.shadow" 7 | 8 | mainClassName = "io.ktor.server.netty.EngineMain" 9 | 10 | sourceCompatibility = 1.8 11 | 12 | compileKotlin.kotlinOptions.jvmTarget = '1.8' 13 | compileTestKotlin.kotlinOptions.jvmTarget = '1.8' 14 | 15 | dependencies { 16 | implementation project(":common") 17 | 18 | implementation Libs.kotlinStdlib 19 | implementation Libs.kotlinReflect 20 | 21 | implementation Libs.kotlinxSerializationCore 22 | 23 | implementation Libs.okhttp 24 | 25 | implementation Libs.ktorLocations 26 | implementation Libs.ktorHtmlBuilder 27 | implementation Libs.ktorServerNetty 28 | implementation Libs.ktorSerialization 29 | 30 | implementation Libs.logbackClassic 31 | implementation Libs.fuel 32 | 33 | testImplementation Libs.kotlinxCoroutinesTest 34 | testImplementation Libs.kotlinTestJunit 35 | testImplementation Libs.ktorServerTestHost 36 | testImplementation Libs.mockitoKotlin 37 | } 38 | 39 | compileKotlin { 40 | kotlinOptions.jvmTarget = "1.8" 41 | kotlinOptions.freeCompilerArgs = ['-Xuse-experimental=kotlin.Experimental'] 42 | } 43 | 44 | shadowJar { 45 | archiveClassifier.set('release') 46 | from({ Paths.get(project(':web').buildDir.path, 'distributions') }) 47 | } 48 | 49 | tasks['shadowJar'].mustRunAfter clean 50 | tasks['processResources'].mustRunAfter ':web:build' 51 | 52 | task release() { 53 | dependsOn('clean') 54 | dependsOn(':web:build') 55 | dependsOn('shadowJar') 56 | } 57 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/org/example/kotlin/multiplatform/backend/Application.kt: -------------------------------------------------------------------------------- 1 | package org.example.kotlin.multiplatform.backend 2 | 3 | import io.ktor.application.Application 4 | import io.ktor.application.call 5 | import io.ktor.application.install 6 | import io.ktor.features.CallLogging 7 | import io.ktor.features.Compression 8 | import io.ktor.features.ConditionalHeaders 9 | import io.ktor.features.ContentNegotiation 10 | import io.ktor.features.DefaultHeaders 11 | import io.ktor.features.PartialContent 12 | import io.ktor.features.StatusPages 13 | import io.ktor.http.HttpStatusCode 14 | import io.ktor.locations.KtorExperimentalLocationsAPI 15 | import io.ktor.locations.Locations 16 | import io.ktor.response.respond 17 | import io.ktor.routing.Routing 18 | import io.ktor.serialization.json 19 | import io.ktor.util.error 20 | 21 | @UseExperimental(KtorExperimentalLocationsAPI::class) 22 | fun Application.helloworld() { 23 | 24 | install(DefaultHeaders) 25 | install(CallLogging) 26 | install(ConditionalHeaders) 27 | install(PartialContent) 28 | install(Compression) 29 | install(Locations) 30 | install(StatusPages) { 31 | exception { cause -> 32 | environment.log.error(cause) 33 | call.respond(HttpStatusCode.InternalServerError) 34 | } 35 | } 36 | 37 | install(ContentNegotiation) { 38 | json() 39 | } 40 | 41 | install(Routing) { 42 | index() 43 | hello() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/org/example/kotlin/multiplatform/backend/HelloRoute.kt: -------------------------------------------------------------------------------- 1 | package org.example.kotlin.multiplatform.backend 2 | 3 | import io.ktor.application.call 4 | import io.ktor.http.ContentType 5 | import io.ktor.locations.KtorExperimentalLocationsAPI 6 | import io.ktor.locations.get 7 | import io.ktor.response.respond 8 | import io.ktor.routing.Route 9 | import io.ktor.routing.accept 10 | import org.example.kotlin.multiplatform.data.model.Greeting 11 | import org.example.kotlin.multiplatform.data.responses.HelloResponse 12 | 13 | @UseExperimental(KtorExperimentalLocationsAPI::class) 14 | fun Route.hello() { 15 | 16 | accept(ContentType.Application.Json) { 17 | get { (who) -> 18 | val message = if (who == null) "Unknown entity" else "Nice to meet you $who" 19 | call.respond(HelloResponse(Greeting(message = message))) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/org/example/kotlin/multiplatform/backend/IndexRoute.kt: -------------------------------------------------------------------------------- 1 | package org.example.kotlin.multiplatform.backend 2 | 3 | import io.ktor.http.ContentType 4 | import io.ktor.http.content.defaultResource 5 | import io.ktor.http.content.resource 6 | import io.ktor.http.content.static 7 | import io.ktor.locations.KtorExperimentalLocationsAPI 8 | import io.ktor.routing.Route 9 | import io.ktor.routing.accept 10 | 11 | @UseExperimental(KtorExperimentalLocationsAPI::class) 12 | fun Route.index() { 13 | static("frontend") { 14 | resource("web.js") 15 | } 16 | 17 | accept(ContentType.Text.Html) { 18 | defaultResource("index.html") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/org/example/kotlin/multiplatform/backend/Locations.kt: -------------------------------------------------------------------------------- 1 | @file:UseExperimental(KtorExperimentalLocationsAPI::class) 2 | 3 | package org.example.kotlin.multiplatform.backend 4 | 5 | import io.ktor.locations.KtorExperimentalLocationsAPI 6 | import io.ktor.locations.Location 7 | import org.example.kotlin.multiplatform.api.Api 8 | 9 | @Location("/{trail...}") 10 | data class IndexPage(val trail: List?) 11 | 12 | @Location(Api.path) 13 | class Api { 14 | @Location(Api.V1.path) 15 | class V1 { 16 | @Location("${Api.V1.Paths.greeting}/{who?}") 17 | data class HelloEndpoint(val who: String?) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | ktor { 2 | deployment { 3 | port = 9090 4 | port = ${?PORT} 5 | environment = development 6 | environment = ${?ENVIRONMENT} 7 | autoreload = true 8 | watch = [Template] 9 | } 10 | 11 | application { 12 | id = Template 13 | modules = [org.example.kotlin.multiplatform.backend.ApplicationKt.helloworld] 14 | } 15 | } 16 | 17 | helloworld { 18 | // you can have any app specific config here in the HOCON format 19 | // then it can be read from an Application context as environment.config.property 20 | } 21 | 22 | service { 23 | environment = development 24 | } 25 | -------------------------------------------------------------------------------- /backend/src/main/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Kotlin Multiplatform Template 7 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /backend/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /backend/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline 2 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | maven { url "http://dl.bintray.com/kotlin/kotlin-eap" } 5 | maven { url "https://kotlin.bintray.com/kotlinx" } 6 | maven { url "https://plugins.gradle.org/m2/" } 7 | mavenCentral() 8 | jcenter() 9 | } 10 | 11 | dependencies { 12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$Versions.kotlin" 13 | classpath "org.jetbrains.kotlin:kotlin-serialization:$Versions.kotlin" 14 | classpath "com.github.jengelman.gradle.plugins:shadow:5.2.0" 15 | classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$Versions.detekt" 16 | classpath 'co.touchlab:kotlinxcodesync:0.1.2' 17 | } 18 | } 19 | 20 | subprojects { 21 | buildscript{ 22 | repositories { 23 | google() 24 | maven { url "http://dl.bintray.com/kotlin/kotlin-eap" } 25 | maven { url "https://kotlin.bintray.com/kotlinx" } 26 | maven { url "https://plugins.gradle.org/m2/" } 27 | mavenCentral() 28 | jcenter() 29 | } 30 | } 31 | 32 | repositories { 33 | mavenLocal() 34 | jcenter() 35 | google() 36 | maven { url "https://kotlin.bintray.com/kotlinx" } 37 | maven { url "http://dl.bintray.com/kotlin/kotlinx.html" } 38 | maven { url "http://dl.bintray.com/kotlin/ktor" } 39 | maven { url "https://dl.bintray.com/kotlin/squash" } 40 | maven { url "https://kotlin.bintray.com/kotlin-js-wrappers" } 41 | } 42 | 43 | apply plugin: "io.gitlab.arturbosch.detekt" 44 | 45 | detekt { 46 | toolVersion = Versions.detekt 47 | config = files("$rootDir/detekt.yml") 48 | } 49 | dependencies { 50 | detektPlugins "io.gitlab.arturbosch.detekt:detekt-formatting:$Versions.detekt" 51 | } 52 | } 53 | 54 | // Heroku/herokuish 55 | 56 | task stage() { 57 | group "distribution" 58 | dependsOn(':backend:release') 59 | } 60 | 61 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile).all { 62 | kotlinOptions.freeCompilerArgs += ["-Xuse-experimental=kotlin.Experimental"] 63 | } 64 | -------------------------------------------------------------------------------- /buildSrc/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "org.jetbrains.kotlin.jvm" version "1.4.0" 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | } 8 | 9 | dependencies { 10 | implementation "org.jetbrains.kotlin:kotlin-stdlib" 11 | } 12 | -------------------------------------------------------------------------------- /buildSrc/src/main/java/Dependencies.kt: -------------------------------------------------------------------------------- 1 | object Versions { 2 | const val detekt = "1.9.1" 3 | const val ehcache = "3.6.1" 4 | const val fuel = "1.11.0" 5 | const val html = "0.6.9" 6 | const val jsoup = "1.9.1" 7 | const val junit = "4.12" 8 | const val kotlin = "1.4.0" 9 | const val react = "16.8.6" 10 | const val kotlinJsExt = "1.0.1-pre.79-kotlin-1.3.41" 11 | const val kotlinWrapper = "$react-pre.79-kotlin-1.3.41" 12 | const val kotlinxCoroutines = "1.3.7" 13 | const val ktor = "1.4.0" 14 | const val logback = "1.2.1" 15 | const val mockitoKotlin = "2.1.0" 16 | const val serialization = "1.0.0-RC" 17 | const val squash = "0.2.4" 18 | const val reactRouterDom = "4.3.1" 19 | const val kotlinReactRouterDom = "$reactRouterDom-pre.79-kotlin-1.3.41" 20 | const val okhttp = "4.0.1" 21 | } 22 | 23 | object Libs { 24 | const val ehcache = "org.ehcache:ehcache:${Versions.ehcache}" 25 | const val fuel = "com.github.kittinunf.fuel:fuel:${Versions.fuel}" 26 | const val jsoup = "org.jsoup:jsoup:${Versions.jsoup}" 27 | const val junit = "junit:junit:${Versions.junit}" 28 | 29 | const val kotlinExtensions = "org.jetbrains:kotlin-extensions:${Versions.kotlinJsExt}" 30 | const val kotlinReact = "org.jetbrains:kotlin-react:${Versions.kotlinWrapper}" 31 | const val kotlinReactDom = "org.jetbrains:kotlin-react-dom:${Versions.kotlinWrapper}" 32 | const val kotlinReactRouterDom = "org.jetbrains:kotlin-react-router-dom:${Versions.kotlinReactRouterDom}" 33 | const val kotlinReflect = "org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlin}" 34 | 35 | const val kotlinStdlib = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}" 36 | 37 | const val kotlinTest = "org.jetbrains.kotlin:kotlin-test:${Versions.kotlin}" 38 | const val kotlinTestCommon = "org.jetbrains.kotlin:kotlin-test-common:${Versions.kotlin}" 39 | const val kotlinTestJs = "org.jetbrains.kotlin:kotlin-test-js:${Versions.kotlin}" 40 | const val kotlinTestJunit = "org.jetbrains.kotlin:kotlin-test-junit:${Versions.kotlin}" 41 | 42 | const val kotlinxCoroutinesCore = 43 | "org.jetbrains.kotlinx:kotlinx-coroutines-core-common:${Versions.kotlinxCoroutines}" 44 | const val kotlinxCoroutinesTest = "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.kotlinxCoroutines}" 45 | 46 | const val kotlinxHtmlJs = "org.jetbrains.kotlinx:kotlinx-html-js:${Versions.html}" 47 | 48 | const val kotlinxSerializationCore = "org.jetbrains.kotlinx:kotlinx-serialization-core:${Versions.serialization}" 49 | 50 | const val ktorHtmlBuilder = "io.ktor:ktor-html-builder:${Versions.ktor}" 51 | const val ktorLocations = "io.ktor:ktor-locations:${Versions.ktor}" 52 | const val ktorServerNetty = "io.ktor:ktor-server-netty:${Versions.ktor}" 53 | const val ktorServerTestHost = "io.ktor:ktor-server-test-host:${Versions.ktor}" 54 | const val ktorSerialization = "io.ktor:ktor-serialization:${Versions.ktor}" 55 | 56 | const val ktorClientAuth = "io.ktor:ktor-client-auth:${Versions.ktor}" 57 | const val ktorClientJson = "io.ktor:ktor-client-json:${Versions.ktor}" 58 | const val ktorClientSerialization = "io.ktor:ktor-client-serialization:${Versions.ktor}" 59 | const val ktorClientCore = "io.ktor:ktor-client-core:${Versions.ktor}" 60 | const val ktorClientJs = "io.ktor:ktor-client-js:${Versions.ktor}" 61 | const val ktorClientLogging = "io.ktor:ktor-client-logging:${Versions.ktor}" 62 | const val ktorClientOkHttp = "io.ktor:ktor-client-okhttp:${Versions.ktor}" 63 | 64 | const val okhttp = "com.squareup.okhttp3:okhttp:${Versions.okhttp}" 65 | const val mockWebServer = "com.squareup.okhttp3:mockwebserver:${Versions.okhttp}" 66 | 67 | const val logbackClassic = "ch.qos.logback:logback-classic:${Versions.logback}" 68 | const val mockitoKotlin = "com.nhaarman.mockitokotlin2:mockito-kotlin:${Versions.mockitoKotlin}" 69 | const val squashH2 = "org.jetbrains.squash:squash-h2:${Versions.squash}" 70 | } 71 | -------------------------------------------------------------------------------- /common/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin-multiplatform' 2 | apply plugin: 'kotlinx-serialization' 3 | 4 | kotlin { 5 | 6 | jvm { 7 | 8 | } 9 | 10 | js { 11 | browser{ 12 | 13 | } 14 | compileKotlinJs { 15 | kotlinOptions.metaInfo = true 16 | kotlinOptions.sourceMap = true 17 | kotlinOptions.moduleKind = "commonjs" 18 | kotlinOptions.main = "call" 19 | } 20 | } 21 | 22 | sourceSets { 23 | commonRelease {} 24 | 25 | commonDebug {} 26 | 27 | commonMain { 28 | if (rootProject.hasProperty("debug")) { 29 | dependsOn commonDebug 30 | } else { 31 | dependsOn commonRelease 32 | } 33 | 34 | dependencies { 35 | api Libs.kotlinxCoroutinesCore 36 | api Libs.ktorClientCore 37 | api Libs.ktorClientJson 38 | api Libs.ktorClientLogging 39 | api Libs.ktorClientSerialization 40 | } 41 | } 42 | 43 | commonTest { 44 | dependencies { 45 | api Libs.kotlinTestCommon 46 | } 47 | } 48 | 49 | jvmMain { 50 | dependencies { 51 | } 52 | } 53 | 54 | jvmTest { 55 | dependencies { 56 | api Libs.junit 57 | api Libs.kotlinTestJunit 58 | api Libs.kotlinTest 59 | } 60 | } 61 | 62 | jsMain { 63 | dependencies { 64 | } 65 | } 66 | 67 | jsTest { 68 | dependencies { 69 | } 70 | } 71 | } 72 | } 73 | 74 | configurations { 75 | compileClasspath 76 | } 77 | 78 | detekt { 79 | input = files( 80 | "src/commonMain/kotlin", 81 | "src/jsMain/kotlin", 82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /common/src/commonDebug/kotlin/org/example/kotlin/multiplatform/data/defaultNetworkConfig.kt: -------------------------------------------------------------------------------- 1 | package org.example.kotlin.multiplatform.data 2 | 3 | data class NetworkConfig( 4 | val host: String = "localhost", 5 | val port: Int = 8080, 6 | val secure: Boolean = false 7 | ) 8 | 9 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/org/example/kotlin/multiplatform/api/Api.kt: -------------------------------------------------------------------------------- 1 | package org.example.kotlin.multiplatform.api 2 | 3 | object Paths 4 | 5 | object Api { 6 | const val path = "/api" 7 | 8 | object V1 { 9 | const val path = "/v1" 10 | 11 | object Paths { 12 | const val greeting = "/greeting" 13 | } 14 | } 15 | 16 | fun path(path: String) = "${Api.path}${V1.path}$path" 17 | } 18 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/org/example/kotlin/multiplatform/data/NetworkDataSource.kt: -------------------------------------------------------------------------------- 1 | package org.example.kotlin.multiplatform.data 2 | 3 | import io.ktor.client.HttpClient 4 | import io.ktor.client.features.defaultRequest 5 | import io.ktor.client.features.json.Json 6 | import io.ktor.client.features.json.serializer.KotlinxSerializer 7 | import io.ktor.client.features.logging.DEFAULT 8 | import io.ktor.client.features.logging.LogLevel 9 | import io.ktor.client.features.logging.Logger 10 | import io.ktor.client.features.logging.Logging 11 | import io.ktor.client.request.get 12 | import io.ktor.http.URLProtocol 13 | import io.ktor.http.encodeURLPath 14 | import kotlinx.serialization.json.Json 15 | import org.example.kotlin.multiplatform.api.Api 16 | import org.example.kotlin.multiplatform.api.Api.V1.Paths.greeting 17 | import org.example.kotlin.multiplatform.data.responses.HelloResponse 18 | 19 | class NetworkDataSource(networkConfig: NetworkConfig = defaultNetworkConfig) { 20 | 21 | private val httpClient: HttpClient = 22 | makeHttpClient(networkConfig) 23 | 24 | suspend fun getGreeting(who: String): HelloResponse = 25 | httpClient.get( 26 | path = Api.path(greeting) 27 | ) { 28 | url { 29 | encodedPath += "/${who.encodeURLPath()}" 30 | } 31 | } 32 | } 33 | 34 | private val json = Json { isLenient = true } 35 | 36 | private fun makeHttpClient( 37 | networkConfig: NetworkConfig 38 | ): HttpClient = HttpClient { 39 | defaultRequest { 40 | url { 41 | host = networkConfig.host 42 | port = networkConfig.port 43 | protocol = if (networkConfig.secure) URLProtocol.HTTPS else URLProtocol.HTTP 44 | } 45 | } 46 | Json { 47 | serializer = KotlinxSerializer(json = json) 48 | } 49 | Logging { 50 | logger = Logger.DEFAULT 51 | level = LogLevel.INFO 52 | } 53 | } 54 | 55 | val defaultNetworkConfig = NetworkConfig() 56 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/org/example/kotlin/multiplatform/data/model/Greeting.kt: -------------------------------------------------------------------------------- 1 | package org.example.kotlin.multiplatform.data.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Greeting(val message: String) 7 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/org/example/kotlin/multiplatform/data/responses/ErrorResponse.kt: -------------------------------------------------------------------------------- 1 | package org.example.kotlin.multiplatform.data.responses 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class ErrorResponse(val message: String) 7 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/org/example/kotlin/multiplatform/data/responses/HelloResponse.kt: -------------------------------------------------------------------------------- 1 | package org.example.kotlin.multiplatform.data.responses 2 | 3 | import kotlinx.serialization.Serializable 4 | import org.example.kotlin.multiplatform.data.model.Greeting 5 | 6 | @Serializable 7 | data class HelloResponse(val greeting: Greeting) 8 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/org/example/kotlin/multiplatform/repository/NetworkRepository.kt: -------------------------------------------------------------------------------- 1 | package org.example.kotlin.multiplatform.repository 2 | 3 | import org.example.kotlin.multiplatform.data.NetworkDataSource 4 | import org.example.kotlin.multiplatform.repository.model.Greeting 5 | import org.example.kotlin.multiplatform.repository.model.toModel 6 | 7 | class NetworkRepository( 8 | private val dataSource: NetworkDataSource 9 | ) { 10 | 11 | suspend fun getGreeting(who: String): Greeting = 12 | dataSource.getGreeting(who) 13 | .greeting 14 | .toModel() 15 | } 16 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/org/example/kotlin/multiplatform/repository/model/Greeting.kt: -------------------------------------------------------------------------------- 1 | package org.example.kotlin.multiplatform.repository.model 2 | 3 | import org.example.kotlin.multiplatform.data.model.Greeting as DtoGreeting 4 | 5 | data class Greeting(val message: String) 6 | 7 | fun DtoGreeting.toModel() = Greeting(message = message) 8 | -------------------------------------------------------------------------------- /common/src/commonRelease/kotlin/org/example/kotlin/multiplatform/data/defaultNetworkConfig.kt: -------------------------------------------------------------------------------- 1 | package org.example.kotlin.multiplatform.data 2 | 3 | data class NetworkConfig( 4 | val host: String = "example.org", 5 | val port: Int = 0, 6 | val secure: Boolean = true 7 | ) 8 | -------------------------------------------------------------------------------- /detekt.yml: -------------------------------------------------------------------------------- 1 | build: 2 | maxIssues: 0 3 | weights: 4 | # complexity: 2 5 | # LongParameterList: 1 6 | # style: 1 7 | # comments: 1 8 | 9 | processors: 10 | active: true 11 | exclude: 12 | # - 'FunctionCountProcessor' 13 | # - 'PropertyCountProcessor' 14 | # - 'ClassCountProcessor' 15 | # - 'PackageCountProcessor' 16 | # - 'KtFileCountProcessor' 17 | 18 | console-reports: 19 | active: true 20 | exclude: 21 | # - 'ProjectStatisticsReport' 22 | # - 'ComplexityReport' 23 | # - 'NotificationReport' 24 | # - 'FindingsReport' 25 | # - 'BuildFailureReport' 26 | 27 | comments: 28 | active: true 29 | CommentOverPrivateFunction: 30 | active: false 31 | CommentOverPrivateProperty: 32 | active: false 33 | EndOfSentenceFormat: 34 | active: false 35 | endOfSentenceFormat: ([.?!][ \t\n\r\f<])|([.?!]$) 36 | UndocumentedPublicClass: 37 | active: false 38 | searchInNestedClass: true 39 | searchInInnerClass: true 40 | searchInInnerObject: true 41 | searchInInnerInterface: true 42 | UndocumentedPublicFunction: 43 | active: false 44 | 45 | complexity: 46 | active: false 47 | ComplexCondition: 48 | active: true 49 | threshold: 4 50 | ComplexInterface: 51 | active: false 52 | threshold: 10 53 | includeStaticDeclarations: false 54 | ComplexMethod: 55 | active: true 56 | threshold: 10 57 | ignoreSingleWhenExpression: false 58 | ignoreSimpleWhenEntries: false 59 | LabeledExpression: 60 | active: false 61 | ignoredLabels: "" 62 | LargeClass: 63 | active: true 64 | threshold: 150 65 | LongMethod: 66 | active: true 67 | threshold: 20 68 | LongParameterList: 69 | active: true 70 | threshold: 6 71 | ignoreDefaultParameters: false 72 | MethodOverloading: 73 | active: false 74 | threshold: 6 75 | NestedBlockDepth: 76 | active: true 77 | threshold: 4 78 | StringLiteralDuplication: 79 | active: false 80 | threshold: 3 81 | ignoreAnnotation: true 82 | excludeStringsWithLessThan5Characters: true 83 | ignoreStringsRegex: '$^' 84 | TooManyFunctions: 85 | active: true 86 | thresholdInFiles: 11 87 | thresholdInClasses: 11 88 | thresholdInInterfaces: 11 89 | thresholdInObjects: 11 90 | thresholdInEnums: 11 91 | ignoreDeprecated: false 92 | ignorePrivate: false 93 | 94 | empty-blocks: 95 | active: true 96 | EmptyCatchBlock: 97 | active: true 98 | allowedExceptionNameRegex: "^(_|(ignore|expected).*)" 99 | EmptyClassBlock: 100 | active: true 101 | EmptyDefaultConstructor: 102 | active: true 103 | EmptyDoWhileBlock: 104 | active: true 105 | EmptyElseBlock: 106 | active: true 107 | EmptyFinallyBlock: 108 | active: true 109 | EmptyForBlock: 110 | active: true 111 | EmptyFunctionBlock: 112 | active: true 113 | ignoreOverriddenFunctions: false 114 | EmptyIfBlock: 115 | active: true 116 | EmptyInitBlock: 117 | active: true 118 | EmptyKtFile: 119 | active: true 120 | EmptySecondaryConstructor: 121 | active: true 122 | EmptyWhenBlock: 123 | active: true 124 | EmptyWhileBlock: 125 | active: true 126 | 127 | exceptions: 128 | active: true 129 | ExceptionRaisedInUnexpectedLocation: 130 | active: true 131 | methodNames: 'toString,hashCode,equals,finalize' 132 | InstanceOfCheckForException: 133 | active: true 134 | NotImplementedDeclaration: 135 | active: true 136 | PrintStackTrace: 137 | active: true 138 | RethrowCaughtException: 139 | active: true 140 | ReturnFromFinally: 141 | active: true 142 | SwallowedException: 143 | active: true 144 | ignoredExceptionTypes: 'InterruptedException,NumberFormatException,ParseException,MalformedURLException' 145 | ThrowingExceptionFromFinally: 146 | active: true 147 | ThrowingExceptionInMain: 148 | active: true 149 | ThrowingExceptionsWithoutMessageOrCause: 150 | active: true 151 | exceptions: 'IllegalArgumentException,IllegalStateException,IOException' 152 | ThrowingNewInstanceOfSameException: 153 | active: false 154 | TooGenericExceptionCaught: 155 | active: true 156 | exceptionNames: 157 | - ArrayIndexOutOfBoundsException 158 | - Error 159 | - Exception 160 | - IllegalMonitorStateException 161 | - NullPointerException 162 | - IndexOutOfBoundsException 163 | - RuntimeException 164 | - Throwable 165 | allowedExceptionNameRegex: "^(_|(ignore|expected).*)" 166 | TooGenericExceptionThrown: 167 | active: true 168 | exceptionNames: 169 | - Error 170 | - Exception 171 | - Throwable 172 | - RuntimeException 173 | 174 | formatting: 175 | active: true 176 | android: false 177 | autoCorrect: true 178 | ChainWrapping: 179 | active: true 180 | autoCorrect: true 181 | CommentSpacing: 182 | active: true 183 | autoCorrect: true 184 | Filename: 185 | active: true 186 | FinalNewline: 187 | active: true 188 | autoCorrect: true 189 | ImportOrdering: 190 | active: false 191 | autoCorrect: false 192 | Indentation: 193 | active: true 194 | autoCorrect: true 195 | indentSize: 4 196 | continuationIndentSize: 4 197 | MaximumLineLength: 198 | active: true 199 | maxLineLength: 120 200 | ModifierOrdering: 201 | active: true 202 | autoCorrect: true 203 | NoBlankLineBeforeRbrace: 204 | active: true 205 | autoCorrect: true 206 | NoConsecutiveBlankLines: 207 | active: true 208 | autoCorrect: true 209 | NoEmptyClassBody: 210 | active: true 211 | autoCorrect: true 212 | NoItParamInMultilineLambda: 213 | active: true 214 | NoLineBreakAfterElse: 215 | active: true 216 | autoCorrect: true 217 | NoLineBreakBeforeAssignment: 218 | active: true 219 | autoCorrect: true 220 | NoMultipleSpaces: 221 | active: true 222 | autoCorrect: true 223 | NoSemicolons: 224 | active: true 225 | autoCorrect: true 226 | NoTrailingSpaces: 227 | active: true 228 | autoCorrect: true 229 | NoUnitReturn: 230 | active: true 231 | autoCorrect: true 232 | NoUnusedImports: 233 | active: true 234 | autoCorrect: true 235 | NoWildcardImports: 236 | active: true 237 | autoCorrect: true 238 | PackageName: 239 | active: true 240 | autoCorrect: true 241 | ParameterListWrapping: 242 | active: true 243 | autoCorrect: true 244 | indentSize: 4 245 | SpacingAroundColon: 246 | active: true 247 | autoCorrect: true 248 | SpacingAroundComma: 249 | active: true 250 | autoCorrect: true 251 | SpacingAroundCurly: 252 | active: true 253 | autoCorrect: true 254 | SpacingAroundKeyword: 255 | active: true 256 | autoCorrect: true 257 | SpacingAroundOperators: 258 | active: true 259 | autoCorrect: true 260 | SpacingAroundParens: 261 | active: true 262 | autoCorrect: true 263 | SpacingAroundRangeOperator: 264 | active: true 265 | autoCorrect: true 266 | StringTemplate: 267 | active: true 268 | autoCorrect: true 269 | 270 | naming: 271 | active: false 272 | ClassNaming: 273 | active: true 274 | classPattern: '[A-Z$][a-zA-Z0-9$]*' 275 | ConstructorParameterNaming: 276 | active: true 277 | parameterPattern: '[a-z][A-Za-z0-9]*' 278 | privateParameterPattern: '[a-z][A-Za-z0-9]*' 279 | excludeClassPattern: '$^' 280 | EnumNaming: 281 | active: true 282 | enumEntryPattern: '^[A-Z][_a-zA-Z0-9]*' 283 | ForbiddenClassName: 284 | active: false 285 | forbiddenName: '' 286 | FunctionMaxLength: 287 | active: false 288 | maximumFunctionNameLength: 30 289 | FunctionMinLength: 290 | active: false 291 | minimumFunctionNameLength: 3 292 | FunctionNaming: 293 | active: true 294 | functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$' 295 | excludeClassPattern: '$^' 296 | ignoreOverridden: true 297 | FunctionParameterNaming: 298 | active: true 299 | parameterPattern: '[a-z][A-Za-z0-9]*' 300 | excludeClassPattern: '$^' 301 | ignoreOverriddenFunctions: true 302 | MatchingDeclarationName: 303 | active: true 304 | MemberNameEqualsClassName: 305 | active: false 306 | ignoreOverriddenFunction: true 307 | ObjectPropertyNaming: 308 | active: true 309 | constantPattern: '[A-Za-z][_A-Za-z0-9]*' 310 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*' 311 | privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' 312 | PackageNaming: 313 | active: true 314 | packagePattern: '^[a-z]+(\.[a-z][a-z0-9]*)*$' 315 | TopLevelPropertyNaming: 316 | active: true 317 | constantPattern: '[A-Z][_A-Z0-9]*' 318 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*' 319 | privatePropertyPattern: '(_)?[A-Za-z][A-Za-z0-9]*' 320 | VariableMaxLength: 321 | active: false 322 | maximumVariableNameLength: 64 323 | VariableMinLength: 324 | active: false 325 | minimumVariableNameLength: 1 326 | VariableNaming: 327 | active: true 328 | variablePattern: '[a-z][A-Za-z0-9]*' 329 | privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' 330 | excludeClassPattern: '$^' 331 | ignoreOverridden: true 332 | 333 | performance: 334 | active: true 335 | ArrayPrimitive: 336 | active: true 337 | ForEachOnRange: 338 | active: true 339 | SpreadOperator: 340 | active: false 341 | UnnecessaryTemporaryInstantiation: 342 | active: true 343 | 344 | potential-bugs: 345 | active: true 346 | DuplicateCaseInWhenExpression: 347 | active: true 348 | EqualsAlwaysReturnsTrueOrFalse: 349 | active: false 350 | EqualsWithHashCodeExist: 351 | active: true 352 | ExplicitGarbageCollectionCall: 353 | active: true 354 | InvalidRange: 355 | active: false 356 | IteratorHasNextCallsNextMethod: 357 | active: false 358 | IteratorNotThrowingNoSuchElementException: 359 | active: false 360 | LateinitUsage: 361 | active: false 362 | excludeAnnotatedProperties: "" 363 | ignoreOnClassesPattern: "" 364 | UnconditionalJumpStatementInLoop: 365 | active: false 366 | UnreachableCode: 367 | active: true 368 | UnsafeCallOnNullableType: 369 | active: false 370 | UnsafeCast: 371 | active: false 372 | UselessPostfixExpression: 373 | active: false 374 | WrongEqualsTypeParameter: 375 | active: false 376 | 377 | style: 378 | active: true 379 | CollapsibleIfStatements: 380 | active: true 381 | DataClassContainsFunctions: 382 | active: true 383 | conversionFunctionPrefix: 'to' 384 | EqualsNullCall: 385 | active: true 386 | EqualsOnSignatureLine: 387 | active: true 388 | ExplicitItLambdaParameter: 389 | active: true 390 | ExpressionBodySyntax: 391 | active: true 392 | includeLineWrapping: true 393 | ForbiddenComment: 394 | active: true 395 | values: 'TODO:,FIXME:,STOPSHIP:' 396 | ForbiddenImport: 397 | active: false 398 | imports: '' 399 | ForbiddenVoid: 400 | active: true 401 | FunctionOnlyReturningConstant: 402 | active: false 403 | ignoreOverridableFunction: true 404 | excludedFunctions: 'describeContents' 405 | LoopWithTooManyJumpStatements: 406 | active: true 407 | maxJumpCount: 1 408 | MagicNumber: 409 | active: false 410 | ignoreNumbers: '-1,0,1,2' 411 | ignoreHashCodeFunction: true 412 | ignorePropertyDeclaration: false 413 | ignoreConstantDeclaration: true 414 | ignoreCompanionObjectPropertyDeclaration: true 415 | ignoreAnnotation: false 416 | ignoreNamedArgument: true 417 | ignoreEnums: false 418 | MandatoryBracesIfStatements: 419 | active: false 420 | MaxLineLength: 421 | active: true 422 | maxLineLength: 120 423 | excludePackageStatements: true 424 | excludeImportStatements: true 425 | excludeCommentStatements: false 426 | MayBeConst: 427 | active: true 428 | ModifierOrder: 429 | active: true 430 | NestedClassesVisibility: 431 | active: true 432 | NewLineAtEndOfFile: 433 | active: true 434 | NoTabs: 435 | active: true 436 | OptionalAbstractKeyword: 437 | active: true 438 | OptionalUnit: 439 | active: true 440 | OptionalWhenBraces: 441 | active: true 442 | PreferToOverPairSyntax: 443 | active: true 444 | ProtectedMemberInFinalClass: 445 | active: true 446 | RedundantVisibilityModifierRule: 447 | active: true 448 | ReturnCount: 449 | active: true 450 | max: 2 451 | excludedFunctions: "equals" 452 | excludeLabeled: false 453 | excludeReturnFromLambda: true 454 | SafeCast: 455 | active: true 456 | SerialVersionUIDInSerializableClass: 457 | active: false 458 | SpacingBetweenPackageAndImports: 459 | active: true 460 | ThrowsCount: 461 | active: true 462 | max: 2 463 | TrailingWhitespace: 464 | active: true 465 | UnnecessaryAbstractClass: 466 | active: false 467 | excludeAnnotatedClasses: "dagger.Module" 468 | UnnecessaryApply: 469 | active: true 470 | UnnecessaryInheritance: 471 | active: true 472 | UnnecessaryLet: 473 | active: true 474 | UnnecessaryParentheses: 475 | active: true 476 | UntilInsteadOfRangeTo: 477 | active: true 478 | UnusedImports: 479 | active: true 480 | UnusedPrivateClass: 481 | active: true 482 | UnusedPrivateMember: 483 | active: true 484 | allowedNames: "(_|ignored|expected|serialVersionUID)" 485 | UseDataClass: 486 | active: true 487 | excludeAnnotatedClasses: "" 488 | UtilityClassWithPublicConstructor: 489 | active: true 490 | VarCouldBeVal: 491 | active: false 492 | WildcardImport: 493 | active: true 494 | excludeImports: 'java.util.*,kotlinx.android.synthetic.*' 495 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | group=org.example.kotlin.multiplatform 2 | version=0.0.1-SNAPSHOT 3 | org.gradle.jvmargs=-Xmx1536M 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiyarmir/kotlin-multiplatform-frontend-template/04d5e21b4021bc49610bd21d1f42e92385e12bc5/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin, switch paths to Windows format before running java 129 | if $cygwin ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=$((i+1)) 158 | done 159 | case $i in 160 | (0) set -- ;; 161 | (1) set -- "$args0" ;; 162 | (2) set -- "$args0" "$args1" ;; 163 | (3) set -- "$args0" "$args1" "$args2" ;; 164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=$(save "$@") 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 185 | cd "$(dirname "$0")" 186 | fi 187 | 188 | exec "$JAVACMD" "$@" 189 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem http://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'kotlin-multiplatform-frontend-template' 2 | 3 | include 'common' 4 | include 'backend' 5 | include 'web' 6 | -------------------------------------------------------------------------------- /web/build.gradle: -------------------------------------------------------------------------------- 1 | import groovy.json.StringEscapeUtils 2 | 3 | plugins { 4 | id 'org.jetbrains.kotlin.js' 5 | } 6 | 7 | repositories { 8 | jcenter() 9 | } 10 | 11 | dependencies { 12 | implementation project(":common") 13 | 14 | testImplementation Libs.kotlinTestJs 15 | 16 | implementation Libs.kotlinxHtmlJs 17 | implementation Libs.kotlinxCoroutinesCore 18 | 19 | implementation Libs.kotlinExtensions 20 | implementation Libs.kotlinReact 21 | implementation Libs.kotlinReactDom 22 | 23 | implementation npm('react', Versions.react) 24 | implementation npm('react-dom', Versions.react) 25 | 26 | // https://github.com/JetBrains/kotlin-wrappers/issues/109 27 | implementation npm("core-js", "3") 28 | 29 | // https://github.com/ktorio/ktor/issues/961 30 | implementation npm("text-encoding", "*") 31 | 32 | implementation(npm("abort-controller", "3.0.0")) 33 | 34 | implementation devNpm("webpack", "4.42.1") 35 | implementation devNpm("babel-loader", "8") 36 | implementation devNpm("@babel/core", "7") 37 | implementation devNpm("@babel/preset-env", "7") 38 | implementation devNpm("css-loader", "*") 39 | implementation devNpm("style-loader", "*") 40 | implementation devNpm("html-loader", "*") 41 | implementation devNpm("source-map-loader", "*") 42 | implementation devNpm("karma", "*") 43 | } 44 | 45 | kotlin { 46 | target { 47 | browser { 48 | webpackTask { 49 | doFirst { 50 | def path = StringEscapeUtils.escapeJavaScript(file("src/main/resources").absolutePath) 51 | file("webpack.config.d/resources.js").text = "config.resolve.modules.push('$path');" 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | compileKotlinJs { 59 | kotlinOptions { 60 | metaInfo = true 61 | outputFile = "$project.buildDir.path/js/${project.name}.js" 62 | sourceMap = true 63 | sourceMapEmbedSources = "always" 64 | moduleKind = 'commonjs' 65 | main = "call" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /web/package.json.d/project.info.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Kotlin Multiplatform Template" 3 | } 4 | -------------------------------------------------------------------------------- /web/src/main/kotlin/org/example/kotlin/multiplatform/web/Application.kt: -------------------------------------------------------------------------------- 1 | package org.example.kotlin.multiplatform.web 2 | 3 | import kotlinx.coroutines.GlobalScope 4 | import kotlinx.coroutines.launch 5 | import kotlinx.html.js.onClickFunction 6 | import org.example.kotlin.multiplatform.repository.NetworkRepository 7 | import react.RBuilder 8 | import react.RComponent 9 | import react.RHandler 10 | import react.RProps 11 | import react.RState 12 | import react.child 13 | import react.dom.button 14 | import react.dom.div 15 | import react.dom.h3 16 | import react.dom.p 17 | import react.functionalComponent 18 | import react.useState 19 | 20 | class Application : RComponent() { 21 | 22 | override fun RBuilder.render() { 23 | div(classes = "container") { 24 | div(classes = "header clearfix") { 25 | h3 { +"Kotlin Multiplatform Example" } 26 | } 27 | child(hello(), props = props) 28 | } 29 | } 30 | } 31 | 32 | fun hello() = functionalComponent { props -> 33 | val (name, setName) = useState(null as String?) 34 | 35 | div { 36 | p { +if (name == null) "Hello" else "Hello, $name" } 37 | button(classes = "btn btn-primary") { 38 | +"Fetch" 39 | attrs { 40 | onClickFunction = { 41 | GlobalScope.launch { 42 | console.log(props) 43 | val name = props.networkRepository.getGreeting("web") 44 | setName(name.message) 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | 52 | external interface ApplicationProps : RProps { 53 | var networkRepository: NetworkRepository 54 | } 55 | 56 | fun RBuilder.app(handler: RHandler) = child(Application::class, handler) 57 | -------------------------------------------------------------------------------- /web/src/main/kotlin/org/example/kotlin/multiplatform/web/main.kt: -------------------------------------------------------------------------------- 1 | package org.example.kotlin.multiplatform.web 2 | 3 | import kotlinext.js.require 4 | import org.example.kotlin.multiplatform.data.NetworkDataSource 5 | import org.example.kotlin.multiplatform.repository.NetworkRepository 6 | import react.dom.render 7 | import kotlin.browser.document 8 | import kotlin.browser.window 9 | 10 | fun main() { 11 | val globalState = object { 12 | val networkRepository = makeRepository() 13 | } 14 | 15 | require("main.css") 16 | 17 | window.onload = { 18 | render(document.getElementById("content")) { 19 | app { 20 | attrs { 21 | networkRepository = globalState.networkRepository 22 | } 23 | } 24 | } 25 | } 26 | } 27 | 28 | private fun makeRepository(): NetworkRepository = 29 | NetworkRepository( 30 | dataSource = NetworkDataSource() 31 | ) 32 | -------------------------------------------------------------------------------- /web/src/main/kotlin/org/example/kotlin/multiplatform/web/repository/WithNetworkRepository.kt: -------------------------------------------------------------------------------- 1 | package org.example.kotlin.multiplatform.web.repository 2 | 3 | import org.example.kotlin.multiplatform.repository.NetworkRepository 4 | import react.RProps 5 | 6 | external interface WithNetworkRepository : RProps { 7 | var networkRepository: NetworkRepository 8 | } 9 | -------------------------------------------------------------------------------- /web/src/main/resources/main.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiyarmir/kotlin-multiplatform-frontend-template/04d5e21b4021bc49610bd21d1f42e92385e12bc5/web/src/main/resources/main.css -------------------------------------------------------------------------------- /web/webpack.config.d/.gitignore: -------------------------------------------------------------------------------- 1 | resources.js 2 | -------------------------------------------------------------------------------- /web/webpack.config.d/babel.js: -------------------------------------------------------------------------------- 1 | config.module.rules.push({ 2 | test: /\.jsx?$/, 3 | exclude: /node_modules/, 4 | use: { 5 | loader: 'babel-loader', 6 | options: { 7 | cacheDirectory: true, 8 | } 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /web/webpack.config.d/bundle.js: -------------------------------------------------------------------------------- 1 | config.output.publicPath = '/frontend/'; 2 | config.devServer = { 3 | proxy: { 4 | '/': 'http://localhost:9090/', 5 | }, 6 | }; 7 | config.stats = 'errors-only'; 8 | 9 | -------------------------------------------------------------------------------- /web/webpack.config.d/css.js: -------------------------------------------------------------------------------- 1 | config.module.rules.push({ 2 | test: /\.css$/i, 3 | use: ['style-loader', 'css-loader'], 4 | }); 5 | 6 | -------------------------------------------------------------------------------- /web/webpack.config.d/html.js: -------------------------------------------------------------------------------- 1 | config.module.rules.push({ 2 | test: /\.html$/, 3 | exclude: /node_modules/, 4 | loader: 'html-loader' 5 | }); 6 | -------------------------------------------------------------------------------- /web/webpack.config.d/minify.js: -------------------------------------------------------------------------------- 1 | config.optimization === undefined 2 | ? config.optimization = {minimize: true} 3 | : config.optimization.minimize = true; 4 | 5 | -------------------------------------------------------------------------------- /web/webpack.config.d/sourcemap.js: -------------------------------------------------------------------------------- 1 | config.module.rules.push({ 2 | enforce: 'pre', 3 | test: /\.js$/, 4 | loader: 'source-map-loader' 5 | }); 6 | config.devtool = "eval-source-map"; 7 | --------------------------------------------------------------------------------