I'm creating a web app with the intent of using Angular for the client and Google's Cloud-Run for the backend. I tried a different approach (asked previously) but it ended up not being up to the task of also managing backend code.
Using IntelliJ, I've created a Kotlin Multiplatform "Full-Stack Web Application" and written some commonMain
code to be used by both ends. It builds and passes tests.
It appears that the intent is to write the backend code inside jvmMain
and the client code inside jsMain
. This seems reasonable. The backend side looks pretty easy as everything is added by hand when setting that up.
On the client side, though... Angular has a CLI program to set it up and what is created doesn't match the existing src
directory structure plus needs to be built via ng build
(or npm build
) which isn't called by the Gradle config created for the project as a whole.
Another option is to have IntelliJ create a new "Angular/CLI" module of the project that somehow references the code at the main/project level. I would of course then have (for my OCD's sake) to move the backend into its own, parallel module. But this seems to go against the intent to have them in jsMain
and jvmMain
, respectively.
What is the best way to add Angular to a Kotlin "Full-Stack Web Application" and have it able to access the commonMain
code?
Definitely non-trivial. Here's how I did it:
cd /tmp/
npm update
ng new myapp
cd /path/to/kmp/project
mkdir -p src/jsMain/typescript/com/example/myapp
mv /tmp/myapp/src/app src/jsMain/typescript/com/example/myapp/
mv /tmp/myapp/{angular.json,*.ts} ./
mv /tmp/{node_modules,package.json,package-lock.json} ./
/typescript/
directory.mv src/jsMain/typescript/com/example/myapp/{*.ico,*.html,*.css} src/jsMain/
/tmp/myapp/.gitignore
and merge it with the existing .gitignore
of your project. The output of git status
will be helpful to look for things that shouldn't be committed.angular.json
file with the following changes. Note that it will cause everything under the standard src/resources/
subdirectory to be available as assets/
in the built application.{
"projects": {
"myapp": {
"architect": {
"build": {
"options": {
"outputPath": "build/install/webapp/myapp",
"index": "src/jsMain/index.html",
"main": "src/jsMain/typescript/com/example/myapp/main.ts",
"tsConfig": "tsconfig.app.json",
"assets": [
{ "input":"src/jsMain/resources/", "glob":"**/*", "output": "/assets/" }
],
"styles": [
"src/jsMain/styles.css"
],
},
},
"test": {
"options": {
"tsConfig": "tsconfig.spec.json",
"assets": [
{ "input":"src/jsMain/resources/", "glob":"**/*", "output":"/assets/" }
],
"styles": [
"src/jsMain/styles.css"
],
}
}
}
}
}
}
tsconfig.app.json:
"files": [
"src/jsMain/typescript/com/example/myapp/main.ts"
],
"include": [
"src/jsMain/typescript/**/*.d.ts"
]
tsconfig.spec.json:
"include": [
"src/jsMain/typescript/**/*.spec.ts",
"src/jsMain/typescript/**/*.d.ts"
]
tsconfig.json:
{
"compilerOptions": {
"paths": {
// Create an alias "common" for the entire commonMain.
// By default, it's built with the name of the top-level project.
"common": [ "build/js/packages/myproject/kotlin/myproject" ]
},
}
}
build.gradle.kts:
import com.github.gradle.node.npm.task.NpxTask
plugins {
kotlin("multiplatform") version "1.8.+"
kotlin("plugin.serialization") version "1.8.+"
id("com.github.node-gradle.node") version "3.5.+"
}
kotlin {
sourceSets.all {
languageSettings.apply {
optIn("kotlin.js.ExperimentalJsExport")
}
}
js(IR) {
binaries.executable()
}
}
val ngBuild = tasks.register<NpxTask>("buildWebapp") {
command.set("ng")
args.set(listOf("build", "--configuration=production"))
dependsOn(tasks.npmInstall)
inputs.dir(project.fileTree("src/jsMain").exclude("**/*.spec.ts"))
inputs.dir("node_modules")
inputs.files("angular.json", ".browserslistrc", "tsconfig.json", "tsconfig.app.json")
outputs.dir("${project.buildDir}/install/webapp")
}
val ngTest = tasks.register<NpxTask>("testWebapp") {
command.set("ng")
args.set(listOf("test", "--watch=false"))
dependsOn(tasks.npmInstall)
inputs.dir("src/jsTest")
inputs.dir("node_modules")
inputs.files("angular.json", ".browserslistrc", "tsconfig.json", "tsconfig.spec.json", "karma.conf.js")
outputs.upToDateWhen { true }
}
sourceSets {
java {
main {
resources {
// This makes the processResources task automatically depend on the buildWebapp one
srcDir(ngBuild)
}
}
}
}
tasks.test {
dependsOn(ngTest)
}
I believe that's the end. I created this answer by going through my command-line history while looking at a git diff
against before I started but it's possible I missed something. I'll try to update this answer if people have difficulties.
You should be able to ng build
and ng serve
the Angular sample application. Once that is working, it can be changed to create something new.
To access the commonMain
classes annotated with @JsExport
from within typescript, the generated library has to be imported. However, only the top-level "com" namespace can be imported directly so some alias trickery is needed to make it (a) more manageable and (b) not conflict with another library also starting with "com".
import { Component } from '@angular/core';
import * as common_top from "common";
import common = common_top.com.example.whatever;