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;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With