Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kotlin DSL: Can't find imports #98

Closed
mraible opened this issue Apr 29, 2020 · 12 comments
Closed

Kotlin DSL: Can't find imports #98

mraible opened this issue Apr 29, 2020 · 12 comments

Comments

@mraible
Copy link

mraible commented Apr 29, 2020

I'm trying to use this plugin in my Spring Boot project that uses Kotlin for its Gradle file.

I added the imports and plugin as shown in this file.

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import com.github.gradle.node.npm.task.NpmInstallTask
import com.github.gradle.node.npm.task.NpmTask
import com.github.gradle.node.npm.task.NpxTask
import com.github.gradle.node.task.NodeTask

plugins {
    id("se.patrikerdes.use-latest-versions") version "0.2.13"
    id("com.github.ben-manes.versions") version "0.28.0"
    id("org.springframework.boot") version "2.2.6.RELEASE"
    id("io.spring.dependency-management") version "1.0.9.RELEASE"
    id("com.github.node-gradle.node") version "2.2.3"
    kotlin("jvm") version "1.3.61"
    kotlin("plugin.spring") version "1.3.61"
    kotlin("plugin.jpa") version "1.3.61"
}

I also added a buildWeb task and made bootJar depend on it.

val buildWeb = tasks.register<NpmTask>("buildNpm") {
    dependsOn(npmInstallTask)
    npmCommand.set(listOf("run", "build"))
    args.set(listOf("--", "--prod"))
    inputs.dir("../notes")
    outputs.dir("${buildDir}/resources/static")
}

val profile = if (project.hasProperty("prod")) "prod" else "dev"

tasks.bootRun {
    args("--spring.profiles.active=${profile}")
}

tasks.bootJar {
    rename("application-${profile}.properties", "application.properties")
    if (project.hasProperty("prod")) {
        from(buildWeb)
    }
}

However, when I try to run it, I get the following error:

$ ./gradlew bootJar -Pprod

...

Script compilation errors:

  Line 02: import com.github.gradle.node.npm.task.NpmInstallTask
                             ^ Unresolved reference: gradle

  Line 03: import com.github.gradle.node.npm.task.NpmTask
                             ^ Unresolved reference: gradle

  Line 04: import com.github.gradle.node.npm.task.NpxTask
                             ^ Unresolved reference: gradle

  Line 05: import com.github.gradle.node.task.NodeTask
                             ^ Unresolved reference: gradle

  Line 59: val buildWeb = tasks.register<NpmTask>("buildNpm") {
                                         ^ Unresolved reference: NpmTask

  Line 59: val buildWeb = tasks.register<NpmTask>("buildNpm") {
                                                              ^ Type mismatch: inferred type is () -> [ERROR : Cannot infer type variable TypeVariable(_L)] but Class<TypeVariable(T)!>! was expected

  Line 60:     dependsOn(npmInstallTask)
               ^ Unresolved reference: dependsOn

  Line 60:     dependsOn(npmInstallTask)
                         ^ Unresolved reference: npmInstallTask

  Line 61:     npmCommand.set(listOf("run", "build"))
               ^ Unresolved reference: npmCommand

  Line 62:     args.set(listOf("--", "--prod"))
               ^ Unresolved reference: args

  Line 63:     inputs.dir("../notes")
               ^ Unresolved reference: inputs

  Line 64:     outputs.dir("${buildDir}/resources/static")
               ^ Unresolved reference: outputs

I tried running the following, but it doesn't seem to help:

rm -rf $HOME/.gradle/caches/

Any idea of what I'm doing wrong?

@bsautel
Copy link
Contributor

bsautel commented Apr 29, 2020

This is because we did a refactoring in the version 3.0 which (will be released soon) in which we broke backward compatibility:

  • Kotlin rewrite of the whole plugin code
  • Base package renaming
  • See here to see the whole changelog

The documentation you see is the one of the 3.0 branch which is not yet released. This is because the development branch is master which is also the default branch, and this is not good for users during this transition phase.

@deepy, maybe we should try to find a way to avoid that, for instance setting the default branch to 2.x until we release the 3.0 version?

This explains why the documentation does not correspond to the binaries you used and why you got these errors, really sorry for that.

We did not have any Kotlin example for the 2.x version. It is written in Groovy and not easily usable in Kotlin. However, I am going to try to adapt the example file (which is used in integration tests to ensure it is valid) for the 2.x version in order to help you solve this issue.

Another way to solve your issue would be to publish a 3.0 beta version because it sounds like we already did all backward compatibility breaking changes and we have quite a good test coverage so its quality is already quite good. @deepy what do you think about that?

I cannot publish a beta version right now (don't have the permission), so I am trying to adapt the Kotlin example to the 2.x and let you know quickly if it succeeded.

@bsautel
Copy link
Contributor

bsautel commented Apr 29, 2020

I could successfully convert this file to the version 2.2.3 of the plugin. It is visible here.

As you can see, it is less beautiful than the next version, but it works.

Hope it will help you and again, sorry for this confusing difference between the project's home page and the latest published binaries.

@mraible
Copy link
Author

mraible commented Apr 29, 2020

Thanks @bsautel, this got me a bit further.

I've fixed the imports and now I have the following:

val npmInstallTask = tasks.named<NpmInstallTask>("npmInstall")

val buildWeb = tasks.register<NpmTask>("buildNpm") {
    dependsOn(npmInstallTask)
    setNpmCommand("run", "build")
    setArgs(listOf("--prod"))
    setWorkingDir(file("${projectDir}/../notes"))
    outputs.dir("${buildDir}/resources/static")
    outputs.upToDateWhen {
        true
    }
}

However, I get the following error:

* What went wrong:
A problem was found with the configuration of task ':npmInstall' (type 'NpmInstallTask').
> No value has been specified for property 'packageJsonFile'.

I only have the Node.js version configured:

node {
    version = "12.16.2"
}

If I specify more properties, it still fails with the same error:

node {
    version = "12.16.2"
    npmVersion = ""
    npmInstallCommand = "install"
    distBaseUrl = "https://nodejs.org/dist"
    download = false
    workDir = file("${buildDir}/node")
    npmWorkDir = file("${buildDir}/npm")
}

A few questions:

  1. Do I have to specify all these values or are there defaults that will be used if I don't?
  2. What behavior does the download property change?
  3. Do you know when 3.0 will be released? I'm hoping to publish a blog post using this code in early May.

@bsautel
Copy link
Contributor

bsautel commented Apr 29, 2020

You get this error because there is no package.json file in the project directory.

As I see, you changed the working directory to ../notes, so I guess your Node.js project (including the package.json and the node_modules directory that will be created by npm install) are inside this directory.

If this is the case, you should use the nodeModulesDir property of the node configuration, this is the directory that contains the package.json and the node_modules directory.

Something like:

node {
    // Other properties if needed
    nodeModulesDir = file("${project.projectDir}/../notes")
}

You also should remove the overridden workingDirectory property usage in the NpmTask, this is used in very rare use cases. This make me think that we should document it better.

Here is the answer to your questions:

  1. This file is used to ensure the Kotlin DSL syntax is valid, so it uses all the existing configuration properties, and most of them correspond to the default values. In your use case, you need to set only a few of them.
  2. By default (download is false), the plugin will use the Node.js which is globally installed, it won't download and install Node.js. If you want to install it (because it is not already installed or because you want to use a specific version), you set download to trueand it will install and use the version of Node.js specified in theversion` property.
  3. For me 3.0 can be released really soon. It remains only one open issue in the milestone that can be solved quickly (we have to take a decision). @deepy , what do you think about that?

Hope this will help you!

@mraible
Copy link
Author

mraible commented Apr 29, 2020

This helped greatly, thanks @bsautel! I have everything compiling now. I found I had to add an extra "--" argument to get a parameter passed to the npm task.

val buildWeb = tasks.register<NpmTask>("buildNpm") {
    dependsOn(npmInstallTask)
    setNpmCommand("run", "build")
    setArgs(listOf("--", "--prod"))
    outputs.dir("${buildDir}/resources/static")
    outputs.upToDateWhen {
        true
    }
}

The last issue seems to be the outputs dir. A build/resources/static file is created, but nothing is in it when the build completes.

Also, is this code needed? If I don't change any Angular files between builds, it seems to skip the build regardless of whether I have this code.

outputs.upToDateWhen {
    true
}

@bsautel
Copy link
Contributor

bsautel commented Apr 29, 2020

Yes, using package.json's scripts you have to add an extra -- parameter to get --* parameters work, don't know really why. To avoid this issue, I would advice you to use the NpxTask. This way, you do not need to define scripts in the package.json file.

For instance, instead of using npm build and defining "build": "webpack build" in the package.json file, you define a NpxTask which webpack as command and listOf("build") as args.

And I confirm that you should remove the upToDateWhen if your task has outputs. By default, Gradle will consider that the task is up-to-date if the inputs did not change and the output is still here.
upToDateWhen { true} tells Gradle to ignore output when checking whether it has to run or not and it is probably not what you want (since you defined an output). You should also define some inputs to get up-to-date detection work as expected. See this to know more about Gradle inputs and outputs mechanism.

@mraible
Copy link
Author

mraible commented Apr 29, 2020

I'm developing an Angular app and most of them have the scripts by default, so there's no additional work to add the scripts.

"scripts": {
  "ng": "ng",
  "start": "ng serve",
  "build": "ng build",
  "test": "ng test",
  "lint": "ng lint",
  "e2e": "ng e2e"
},

I still have a question about the outputs.dir.

val buildWeb = tasks.register<NpmTask>("buildNpm") {
    dependsOn(npmInstallTask)
    setNpmCommand("run", "build")
    setArgs(listOf("--", "--prod"))
    outputs.dir("${buildDir}/resources/static")
}

It creates build/resources/static, but doesn't put any files in it. Do I need to add lines like the following to copy the compiled files into it?

from("${projectDir}/../notes/dist/notes")
into("${buildDir}/resources/static")

@bsautel
Copy link
Contributor

bsautel commented Apr 29, 2020

Ok, I see what you mean for npm. I am also using Angular and I use it via npx, but both solutions work. There is an example of an Angular build task here (using the Groovy DSL).

What is the output directory of your Angular build (in angular.json)? This is where the output is created and is by default dist/yourProjectName.

outputs.dir won't change the Angular build's behavior. It is just here to tell what are the inputs and outputs of the task to Gradle so that it can know whether it is necessary to run the task or not.

So the output dir you set in the Gradle task must be the same as the one configured in the Angular build.

@mraible
Copy link
Author

mraible commented Apr 30, 2020

I got everything working where my projects are side-by-side:

notes // angular app
notes-api // spring-boot app

Here's the contents of my build file:

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import com.moowork.gradle.node.npm.NpmInstallTask
import com.moowork.gradle.node.npm.NpmTask

plugins {
    id("se.patrikerdes.use-latest-versions") version "0.2.13"
    id("com.github.ben-manes.versions") version "0.28.0"
    id("org.springframework.boot") version "2.2.6.RELEASE"
    id("io.spring.dependency-management") version "1.0.9.RELEASE"
    id("com.github.node-gradle.node") version "2.2.3"
    kotlin("jvm") version "1.3.61"
    kotlin("plugin.spring") version "1.3.61"
    kotlin("plugin.jpa") version "1.3.61"
}

val spa = "${projectDir}/../notes";

node {
    version = "12.16.2"
    nodeModulesDir = file(spa)
}

group = "com.okta.developer"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-data-rest")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("com.okta.spring:okta-spring-boot-starter:1.4.0")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    if (project.hasProperty("prod")) {
        runtimeOnly("org.postgresql:postgresql")
    } else {
        runtimeOnly("com.h2database:h2")
    }
    testImplementation("org.springframework.boot:spring-boot-starter-test") {
        exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs = listOf("-Xjsr305=strict")
        jvmTarget = "1.8"
    }
}

val npmInstallTask = tasks.named<NpmInstallTask>("npmInstall")

val buildWeb = tasks.register<NpmTask>("buildNpm") {
    dependsOn(npmInstallTask)
    setNpmCommand("run", "build")
    setArgs(listOf("--", "--prod"))
}

val profile = if (project.hasProperty("prod")) "prod" else "dev"

tasks.bootRun {
    args("--spring.profiles.active=${profile}")
}

tasks.bootJar {
    rename("application-${profile}.properties", "application.properties")
    if (project.hasProperty("prod")) {
        from(buildWeb)
        from("${spa}/dist/notes") {
            into("static")
        }
    }
}

The issue I'm experiencing now is ./gradlew bootJar -Pprod takes ~30 seconds to run every time, even when no files have changed. Shouldn't the buildNpm task be skipped if nothing has changed?

➜  notes-api git:(jar) ✗ ./gradlew bootJar -Pprod

> Task :buildNpm

> [email protected] build /Users/mraible/okta-angular-deployment-example/notes
> ng build "--prod"

Generating ES5 bundles for differential loading...
ES5 bundle generation complete.

chunk {2} polyfills-es2015.7c2cfccf3ec8a9795bc6.js (polyfills) 36.8 kB [initial] [rendered]
chunk {3} polyfills-es5.2c3c254746d12e2fc302.js (polyfills-es5) 130 kB [initial] [rendered]
chunk {0} runtime-es2015.1eba213af0b233498d9d.js (runtime) 1.45 kB [entry] [rendered]
chunk {0} runtime-es5.1eba213af0b233498d9d.js (runtime) 1.45 kB [entry] [rendered]
chunk {1} main-es2015.f702d3031481ba47a012.js (main) 328 kB [initial] [rendered]
chunk {1} main-es5.f702d3031481ba47a012.js (main) 397 kB [initial] [rendered]
chunk {4} styles.af3772bed2638673aaae.css (styles) 143 kB [initial] [rendered]
Date: 2020-04-30T21:34:48.424Z - Hash: fbb16d5705207fbd7a90 - Time: 20101ms



BUILD SUCCESSFUL in 29s
5 actionable tasks: 1 executed, 4 up-to-date
Execution time: 30 s.                                                                                                                                                                                            
➜  notes-api git:(jar) ✗ ./gradlew bootJar -Pprod

> Task :buildNpm

> [email protected] build /Users/mraible/okta-angular-deployment-example/notes
> ng build "--prod"

Generating ES5 bundles for differential loading...
ES5 bundle generation complete.

chunk {2} polyfills-es2015.7c2cfccf3ec8a9795bc6.js (polyfills) 36.8 kB [initial] [rendered]
chunk {3} polyfills-es5.2c3c254746d12e2fc302.js (polyfills-es5) 130 kB [initial] [rendered]
chunk {0} runtime-es2015.1eba213af0b233498d9d.js (runtime) 1.45 kB [entry] [rendered]
chunk {0} runtime-es5.1eba213af0b233498d9d.js (runtime) 1.45 kB [entry] [rendered]
chunk {1} main-es2015.f702d3031481ba47a012.js (main) 328 kB [initial] [rendered]
chunk {1} main-es5.f702d3031481ba47a012.js (main) 397 kB [initial] [rendered]
chunk {4} styles.af3772bed2638673aaae.css (styles) 143 kB [initial] [rendered]
Date: 2020-04-30T21:35:17.429Z - Hash: fbb16d5705207fbd7a90 - Time: 17036ms



BUILD SUCCESSFUL in 23s
5 actionable tasks: 1 executed, 4 up-to-date
Execution time: 24 s.                                                        

@bsautel
Copy link
Contributor

bsautel commented May 1, 2020

Ok, nice!

And sure you can avoid the task from running when nothing changed! For that you have to declare its inputs and outputs as explained in the Gradle docs. Gradle will be able to check whether inputs and outputs changed or not and will run the task accordingly to that.

Have a look to this Angular build example (using the Groovy DSL), it defines the inputs and outputs of the ng build task. You have to do nearly the same thing, except that the paths are not the same (prefix with the spa variable). Ensure also that those files exist and are at this place (it sometimes changes according to Angular versions).

Note that the inputs and outputs of the npmInstall task are configured by the plugin. So you just have to do that to the buildNpm task.

To know why Gradle considers a task to be up-to-date or not, use the --info parameters.

@mraible
Copy link
Author

mraible commented May 1, 2020

Saweeet! You da man, @bsautel!! Here's my final buildWeb task:

val buildWeb = tasks.register<NpmTask>("buildNpm") {
    dependsOn("npmInstall")
    setNpmCommand("run", "build")
    setArgs(listOf("--", "--prod"))
    inputs.dir("${spa}/src")
    inputs.dir(fileTree("${spa}/node_modules").exclude("${spa}/.cache"))
    outputs.dir("${spa}/dist")
}

Results when no files change:

./gradlew bootJar -Pprod

BUILD SUCCESSFUL in 3s
5 actionable tasks: 5 up-to-date
Execution time: 4 s.    

@bsautel
Copy link
Contributor

bsautel commented May 8, 2020

FYI @mraible , I added a Spring Boot Angular example project.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants