feat(java): add gradle kotlin plugin (#29464)

- [x] change init to create `createNodes` instead
- [x] unit tests
- [x] test-ci
- [x] test on windows
- [x] help metadata
- [x] external nodes

TODO:
- add publish executor?
- publish to maven central?

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->
currently, it uses [project report
plugin](https://docs.gradle.org/current/userguide/project_report_plugin.html).
- pro: no need to maintain this plugin
- con: this plugin gives limited information

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
change the project report plugin to @nxn/gradle/plugin-v1
now the @nx/gradle plugin will use project graph plugin
(dev.nx.gradle.project-graph) created in this pr.
this plugin will create json file that is exactly what nx project grpah
expected.

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
This commit is contained in:
Emily Xiong 2025-04-23 13:13:25 -04:00 committed by GitHub
parent 296f326b94
commit b377c96d99
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
94 changed files with 6123 additions and 1846 deletions

View File

@ -65,6 +65,12 @@ jobs:
- name: Install Rust - name: Install Rust
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
- name: Check Documentation - name: Check Documentation
run: pnpm nx documentation run: pnpm nx documentation
timeout-minutes: 20 timeout-minutes: 20

5
.gitignore vendored
View File

@ -67,5 +67,8 @@ target
vite.config.*.timestamp* vite.config.*.timestamp*
storybook-static storybook-static
# Ignore Gradle project-specific cache directory
.gradle
.kotlin

View File

@ -64,6 +64,13 @@ launch-templates:
- name: Load Cargo Env - name: Load Cargo Env
script: echo "PATH=$HOME/.cargo/bin:$PATH" >> $NX_CLOUD_ENV script: echo "PATH=$HOME/.cargo/bin:$PATH" >> $NX_CLOUD_ENV
- name: Setup Java 21
script: |
sudo apt update
sudo apt install -y openjdk-21-jdk
sudo update-alternatives --set java /usr/lib/jvm/java-21-openjdk-amd64/bin/java
java -version
linux-extra-large: linux-extra-large:
resource-class: 'docker_linux_amd64/extra_large' resource-class: 'docker_linux_amd64/extra_large'
image: 'ubuntu22.04-node20.11-v10' image: 'ubuntu22.04-node20.11-v10'
@ -128,3 +135,10 @@ launch-templates:
- name: Load Cargo Env - name: Load Cargo Env
script: echo "PATH=$HOME/.cargo/bin:$PATH" >> $NX_CLOUD_ENV script: echo "PATH=$HOME/.cargo/bin:$PATH" >> $NX_CLOUD_ENV
- name: Setup Java 21
script: |
sudo apt update
sudo apt install -y openjdk-21-jdk
sudo update-alternatives --set java /usr/lib/jvm/java-21-openjdk-amd64/bin/java
java -version

View File

@ -13,6 +13,7 @@ packages/express/src/schematics/**/files/**/*.json
packages/nest/src/schematics/**/files/**/*.json packages/nest/src/schematics/**/files/**/*.json
packages/react/src/schematics/**/files/**/*.json packages/react/src/schematics/**/files/**/*.json
packages/jest/src/schematics/**/files/**/*.json packages/jest/src/schematics/**/files/**/*.json
packages/gradle/project-graph/build/**/*.*
packages/nx/src/plugins/js/lock-file/__fixtures__/**/*.* packages/nx/src/plugins/js/lock-file/__fixtures__/**/*.*
packages/**/schematics/**/files/**/*.html packages/**/schematics/**/files/**/*.html
packages/**/generators/**/files/**/*.html packages/**/generators/**/files/**/*.html

6
build.gradle.kts Normal file
View File

@ -0,0 +1,6 @@
plugins {
id("dev.nx.gradle.project-graph") version("0.0.2")
id("com.ncorti.ktfmt.gradle") version("+")
}
group = "dev.nx"

View File

@ -2325,6 +2325,16 @@
} }
}, },
"migrations": { "migrations": {
"/nx-api/gradle/migrations/change-plugin-to-v1": {
"description": "Change @nx/gradle plugin to version 1",
"file": "generated/packages/gradle/migrations/change-plugin-to-v1.json",
"hidden": false,
"name": "change-plugin-to-v1",
"version": "21.0.0-beta.5",
"originalFilePath": "/packages/gradle",
"path": "/nx-api/gradle/migrations/change-plugin-to-v1",
"type": "migration"
},
"/nx-api/gradle/migrations/add-include-subprojects-tasks": { "/nx-api/gradle/migrations/add-include-subprojects-tasks": {
"description": "Add includeSubprojectsTasks to build.gradle file", "description": "Add includeSubprojectsTasks to build.gradle file",
"file": "generated/packages/gradle/migrations/add-include-subprojects-tasks.json", "file": "generated/packages/gradle/migrations/add-include-subprojects-tasks.json",

View File

@ -2309,6 +2309,16 @@
} }
], ],
"migrations": [ "migrations": [
{
"description": "Change @nx/gradle plugin to version 1",
"file": "generated/packages/gradle/migrations/change-plugin-to-v1.json",
"hidden": false,
"name": "change-plugin-to-v1",
"version": "21.0.0-beta.5",
"originalFilePath": "/packages/gradle",
"path": "gradle/migrations/change-plugin-to-v1",
"type": "migration"
},
{ {
"description": "Add includeSubprojectsTasks to build.gradle file", "description": "Add includeSubprojectsTasks to build.gradle file",
"file": "generated/packages/gradle/migrations/add-include-subprojects-tasks.json", "file": "generated/packages/gradle/migrations/add-include-subprojects-tasks.json",

View File

@ -10,5 +10,5 @@
"path": "/packages/gradle", "path": "/packages/gradle",
"schema": null, "schema": null,
"type": "migration", "type": "migration",
"examplesFile": "#### Add includeSubprojectsTasks to build.gradle File\n\nAdd includeSubprojectsTasks to build.gradle file\n\n#### Sample Code Changes\n\nUpdate import paths for `withModuleFederation` and `withModuleFederationForSSR`.\n\n{% tabs %}\n{% tab label=\"Before\" %}\n\n```json {% fileName=\"nx.json\" %}\n{\n \"plugins\": [\"@nx/gradle\"]\n}\n```\n\n{% /tab %}\n{% tab label=\"After\" %}\n\n```json {% highlightLines=[5] fileName=\"nx.json\" %}\n{\n \"plugins\": [\n {\n \"options\": {\n \"includeSubprojectsTasks\": true\n },\n \"plugin\": \"@nx/gradle\"\n }\n ]\n}\n```\n\n{% /tab %}\n{% /tabs %}\n" "examplesFile": "#### Add includeSubprojectsTasks to @nx/gradle Plugin Options\n\nAdd includeSubprojectsTasks to @nx/gradle plugin options in nx.json file\n\n#### Sample Code Changes\n\n{% tabs %}\n{% tab label=\"Before\" %}\n\n```json {% fileName=\"nx.json\" %}\n{\n \"plugins\": [\"@nx/gradle\"]\n}\n```\n\n{% /tab %}\n{% tab label=\"After\" %}\n\n```json {% highlightLines=[5] fileName=\"nx.json\" %}\n{\n \"plugins\": [\n {\n \"options\": {\n \"includeSubprojectsTasks\": true\n },\n \"plugin\": \"@nx/gradle\"\n }\n ]\n}\n```\n\n{% /tab %}\n{% /tabs %}\n"
} }

View File

@ -0,0 +1,14 @@
{
"name": "change-plugin-to-v1",
"version": "21.0.0-beta.5",
"cli": "nx",
"description": "Change @nx/gradle plugin to version 1",
"factory": "./src/migrations/21-0-0/change-plugin-to-v1",
"implementation": "/packages/gradle/src/migrations/21-0-0/change-plugin-to-v1.ts",
"aliases": [],
"hidden": false,
"path": "/packages/gradle",
"schema": null,
"type": "migration",
"examplesFile": "#### Change @nx/gradle plugin to @nx/gradle/plugin-v1\n\nChange @nx/gradle plugin to version 1 in nx.json\n\n#### Sample Code Changes\n\n{% tabs %}\n{% tab label=\"Before\" %}\n\n```json {% fileName=\"nx.json\" %}\n{\n \"plugins\": [\"@nx/gradle\"]\n}\n```\n\n{% /tab %}\n{% tab label=\"After\" %}\n\n```json {% highlightLines=[5] fileName=\"nx.json\" %}\n{\n \"plugins\": [\"@nx/gradle/plugin-v1\"]\n}\n```\n\n{% /tab %}\n{% /tabs %}\n"
}

View File

@ -375,10 +375,10 @@ jobs:
# Uncomment this line to enable task distribution # Uncomment this line to enable task distribution
# - run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-jvm" --stop-agents-after="build" # - run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-jvm" --stop-agents-after="build"
- name: Set up JDK 17 for x64 - name: Set up JDK 21 for x64
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
java-version: '17' java-version: '21'
distribution: 'temurin' distribution: 'temurin'
architecture: x64 architecture: x64

View File

@ -1,2 +0,0 @@
# This file was generated by the Gradle 'init' task.
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format

Binary file not shown.

View File

@ -17,10 +17,9 @@ import { createGradleProject } from './utils/create-gradle-project';
import { createFileSync } from 'fs-extra'; import { createFileSync } from 'fs-extra';
describe('Nx Import Gradle', () => { describe('Nx Import Gradle', () => {
let proj: string;
const tempImportE2ERoot = join(e2eCwd, 'nx-import'); const tempImportE2ERoot = join(e2eCwd, 'nx-import');
beforeAll(() => { beforeAll(() => {
proj = newProject({ newProject({
packages: ['@nx/js'], packages: ['@nx/js'],
}); });
@ -66,30 +65,7 @@ describe('Nx Import Gradle', () => {
'gradleProjectKotlin', 'gradleProjectKotlin',
'kotlin-' 'kotlin-'
); );
// Add project.json files to the gradle project to avoid duplicate project names setupGradleProjectGit(tempGraldeProjectPath, tempGradleProjectName);
createFileSync(join(tempGraldeProjectPath, 'project.json'));
writeFileSync(
join(tempGraldeProjectPath, 'project.json'),
`{"name": "${tempGradleProjectName}"}`
);
execSync(`git init`, {
cwd: tempGraldeProjectPath,
});
execSync(`git add .`, {
cwd: tempGraldeProjectPath,
});
execSync(`git commit -am "initial commit"`, {
cwd: tempGraldeProjectPath,
});
try {
execSync(`git checkout -b main`, {
cwd: tempGraldeProjectPath,
});
} catch {
// This fails if git is already configured to have `main` branch, but that's OK
}
const remote = tempGraldeProjectPath; const remote = tempGraldeProjectPath;
const ref = 'main'; const ref = 'main';
@ -140,30 +116,7 @@ describe('Nx Import Gradle', () => {
'gradleProjectGroovy', 'gradleProjectGroovy',
'groovy-' 'groovy-'
); );
// Add project.json files to the gradle project to avoid duplicate project names setupGradleProjectGit(tempGraldeProjectPath, tempGradleProjectName);
createFileSync(join(tempGraldeProjectPath, 'project.json'));
writeFileSync(
join(tempGraldeProjectPath, 'project.json'),
`{"name": "${tempGradleProjectName}"}`
);
execSync(`git init`, {
cwd: tempGraldeProjectPath,
});
execSync(`git add .`, {
cwd: tempGraldeProjectPath,
});
execSync(`git commit -am "initial commit"`, {
cwd: tempGraldeProjectPath,
});
try {
execSync(`git checkout -b main`, {
cwd: tempGraldeProjectPath,
});
} catch {
// This fails if git is already configured to have `main` branch, but that's OK
}
const remote = tempGraldeProjectPath; const remote = tempGraldeProjectPath;
const ref = 'main'; const ref = 'main';
@ -199,3 +152,40 @@ describe('Nx Import Gradle', () => {
runCommand(`git commit -am 'import groovy project'`); runCommand(`git commit -am 'import groovy project'`);
}); });
}); });
function setupGradleProjectGit(
tempGraldeProjectPath: string,
tempGradleProjectName: string
) {
// Add project.json files to the gradle project to avoid duplicate project names
createFileSync(join(tempGraldeProjectPath, 'project.json'));
writeFileSync(
join(tempGraldeProjectPath, 'project.json'),
`{"name": "${tempGradleProjectName}"}`
);
execSync(`./gradlew --stop`, {
cwd: tempGraldeProjectPath,
});
execSync(`./gradlew clean`, {
cwd: tempGraldeProjectPath,
});
execSync(`git init`, {
cwd: tempGraldeProjectPath,
});
execSync(`git add .`, {
cwd: tempGraldeProjectPath,
});
execSync(`git commit -am "initial commit"`, {
cwd: tempGraldeProjectPath,
});
try {
execSync(`git checkout -b main`, {
cwd: tempGraldeProjectPath,
});
} catch {
// This fails if git is already configured to have `main` branch, but that's OK
}
}

View File

@ -0,0 +1,177 @@
import {
checkFilesExist,
cleanupProject,
createFile,
fileExists,
newProject,
readFile,
runCLI,
uniq,
updateFile,
updateJson,
} from '@nx/e2e/utils';
import { basename, dirname, join } from 'path';
import { createGradleProject } from './utils/create-gradle-project';
describe('Gradle Plugin V1', () => {
describe.each([{ type: 'kotlin' }, { type: 'groovy' }])(
'$type',
({ type }: { type: 'kotlin' | 'groovy' }) => {
let gradleProjectName = uniq('my-gradle-project');
beforeAll(() => {
newProject();
createGradleProject(gradleProjectName, type);
runCLI(`add @nx/gradle`);
updateJson('nx.json', (json) => {
json.plugins.find((p) => p.plugin === '@nx/gradle').plugin =
'@nx/gradle/plugin-v1';
return json;
});
addProjectReportToBuildGradle(
`settings.gradle${type === 'kotlin' ? '.kts' : ''}`
);
});
afterAll(() => cleanupProject());
it('should build', () => {
const projects = runCLI(`show projects`);
expect(projects).toContain('app');
expect(projects).toContain('list');
expect(projects).toContain('utilities');
expect(projects).toContain(gradleProjectName);
const buildOutput = runCLI('build app', { verbose: true });
expect(buildOutput).toContain('nx run list:build');
expect(buildOutput).toContain(':list:classes');
expect(buildOutput).toContain('nx run utilities:build');
expect(buildOutput).toContain(':utilities:classes');
checkFilesExist(
`app/build/libs/app.jar`,
`list/build/libs/list.jar`,
`utilities/build/libs/utilities.jar`
);
});
it('should track dependencies for new app', () => {
if (type === 'groovy') {
createFile(
`app2/build.gradle`,
`plugins {
id 'buildlogic.groovy-application-conventions'
}
dependencies {
implementation project(':app')
}`
);
} else {
createFile(
`app2/build.gradle.kts`,
`plugins {
id("buildlogic.kotlin-application-conventions")
}
dependencies {
implementation(project(":app"))
}`
);
updateFile(`app/build.gradle.kts`, (content) => {
content += `\r\ntasks.register("task1"){
println("REGISTER TASK1: This is executed during the configuration phase")
}`;
return content;
});
}
updateFile(
`settings.gradle${type === 'kotlin' ? '.kts' : ''}`,
(content) => {
content += `\r\ninclude("app2")`;
return content;
}
);
let buildOutput = runCLI('build app2', { verbose: true });
// app2 depends on app
expect(buildOutput).toContain('nx run app:build');
expect(buildOutput).toContain(':app:classes');
expect(buildOutput).toContain('nx run list:build');
expect(buildOutput).toContain(':list:classes');
expect(buildOutput).toContain('nx run utilities:build');
expect(buildOutput).toContain(':utilities:classes');
checkFilesExist(
`app2/build/libs/app2.jar`,
`app/build/libs/app.jar`,
`list/build/libs/list.jar`,
`utilities/build/libs/utilities.jar`
);
});
it('should run atomized test target', () => {
updateJson('nx.json', (json) => {
json.plugins.find((p) => p.plugin === '@nx/gradle/plugin-v1').options[
'ciTargetName'
] = 'test-ci';
return json;
});
expect(() => {
runCLI('run app:test-ci--MessageUtilsTest', { verbose: true });
runCLI('run list:test-ci--LinkedListTest', { verbose: true });
}).not.toThrow();
});
}
);
});
function addProjectReportToBuildGradle(settingsGradleFile: string) {
const filename = basename(settingsGradleFile);
let gradleFilePath = 'build.gradle';
if (filename.endsWith('.kts')) {
gradleFilePath = 'build.gradle.kts';
}
gradleFilePath = join(dirname(settingsGradleFile), gradleFilePath);
let buildGradleContent = '';
if (!fileExists(gradleFilePath)) {
createFile(gradleFilePath, buildGradleContent); // create a build.gradle file near settings.gradle file if it does not exist
} else {
buildGradleContent = readFile(gradleFilePath).toString();
}
buildGradleContent += `\n\rallprojects {
apply {
plugin("project-report")
}
}`;
if (gradleFilePath.endsWith('.kts')) {
buildGradleContent += `\n\rtasks.register("projectReportAll") {
// All project reports of subprojects
allprojects.forEach {
dependsOn(it.tasks.get("projectReport"))
}
// All projectReportAll of included builds
gradle.includedBuilds.forEach {
dependsOn(it.task(":projectReportAll"))
}
}`;
} else {
buildGradleContent += `\n\rtasks.register("projectReportAll") {
// All project reports of subprojects
allprojects.forEach {
dependsOn(it.tasks.getAt("projectReport"))
}
// All projectReportAll of included builds
gradle.includedBuilds.forEach {
dependsOn(it.task(":projectReportAll"))
}
}`;
}
if (buildGradleContent) {
updateFile(gradleFilePath, buildGradleContent);
}
}

View File

@ -6,6 +6,7 @@ import {
runCLI, runCLI,
uniq, uniq,
updateFile, updateFile,
updateJson,
} from '@nx/e2e/utils'; } from '@nx/e2e/utils';
import { createGradleProject } from './utils/create-gradle-project'; import { createGradleProject } from './utils/create-gradle-project';
@ -30,9 +31,9 @@ describe('Gradle', () => {
expect(projects).toContain(gradleProjectName); expect(projects).toContain(gradleProjectName);
const buildOutput = runCLI('build app', { verbose: true }); const buildOutput = runCLI('build app', { verbose: true });
expect(buildOutput).toContain('nx run list:build'); expect(buildOutput).toContain('nx run list:');
expect(buildOutput).toContain(':list:classes'); expect(buildOutput).toContain(':list:classes');
expect(buildOutput).toContain('nx run utilities:build'); expect(buildOutput).toContain('nx run utilities:');
expect(buildOutput).toContain(':utilities:classes'); expect(buildOutput).toContain(':utilities:classes');
checkFilesExist( checkFilesExist(
@ -82,8 +83,28 @@ dependencies {
let buildOutput = runCLI('build app2', { verbose: true }); let buildOutput = runCLI('build app2', { verbose: true });
// app2 depends on app // app2 depends on app
expect(buildOutput).toContain('nx run app:build'); expect(buildOutput).toContain('nx run app:');
expect(buildOutput).toContain(':app:classes'); expect(buildOutput).toContain(':app:classes');
expect(buildOutput).toContain('nx run list:');
expect(buildOutput).toContain(':list:classes');
expect(buildOutput).toContain('nx run utilities:');
expect(buildOutput).toContain(':utilities:classes');
checkFilesExist(`app2/build/libs/app2.jar`);
});
it('should run atomized test target', () => {
updateJson('nx.json', (json) => {
json.plugins.find((p) => p.plugin === '@nx/gradle').options[
'ciTargetName'
] = 'test-ci';
return json;
});
expect(() => {
runCLI('run app:test-ci--MessageUtilsTest', { verbose: true });
runCLI('run list:test-ci--LinkedListTest', { verbose: true });
}).not.toThrow();
}); });
} }
); );

View File

@ -5,6 +5,7 @@ import {
tmpProjPath, tmpProjPath,
} from '@nx/e2e/utils'; } from '@nx/e2e/utils';
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import { readFileSync } from 'fs';
import { createFileSync, writeFileSync } from 'fs-extra'; import { createFileSync, writeFileSync } from 'fs-extra';
import { join, resolve } from 'path'; import { join, resolve } from 'path';
@ -15,14 +16,10 @@ export function createGradleProject(
packageName: string = 'gradleProject', packageName: string = 'gradleProject',
addProjectJsonNamePrefix: string = '' addProjectJsonNamePrefix: string = ''
) { ) {
e2eConsoleLogger( e2eConsoleLogger(`Using java version: ${execSync('java -version')}`);
`Using java version: ${execSync('java -version')} ${execSync(
'echo $JAVA_HOME'
)}`
);
const gradleCommand = isWindows() const gradleCommand = isWindows()
? resolve(`${__dirname}/../../gradlew.bat`) ? resolve(`${__dirname}/../../../../gradlew.bat`)
: resolve(`${__dirname}/../../gradlew`); : resolve(`${__dirname}/../../../../gradlew`);
e2eConsoleLogger( e2eConsoleLogger(
'Using gradle version: ' + 'Using gradle version: ' +
execSync(`${gradleCommand} --version`, { execSync(`${gradleCommand} --version`, {
@ -36,13 +33,26 @@ export function createGradleProject(
); );
e2eConsoleLogger( e2eConsoleLogger(
runCommand( runCommand(
`${gradleCommand} init --type ${type}-application --dsl ${type} --project-name ${projectName} --package ${packageName} --no-incubating --split-project`, `${gradleCommand} init --type ${type}-application --dsl ${type} --project-name ${projectName} --package ${packageName} --no-incubating --split-project --overwrite`,
{ {
cwd, cwd,
} }
) )
); );
try {
e2eConsoleLogger(
runCommand(`${gradleCommand} --stop`, {
cwd,
})
);
e2eConsoleLogger(
runCommand(`${gradleCommand} clean`, {
cwd,
})
);
} catch (e) {}
if (addProjectJsonNamePrefix) { if (addProjectJsonNamePrefix) {
createFileSync(join(cwd, 'app/project.json')); createFileSync(join(cwd, 'app/project.json'));
writeFileSync( writeFileSync(
@ -60,4 +70,35 @@ export function createGradleProject(
`{"name": "${addProjectJsonNamePrefix}utilities"}` `{"name": "${addProjectJsonNamePrefix}utilities"}`
); );
} }
addLocalPluginManagement(
join(cwd, `settings.gradle${type === 'kotlin' ? '.kts' : ''}`)
);
addLocalPluginManagement(
join(cwd, `buildSrc/settings.gradle${type === 'kotlin' ? '.kts' : ''}`)
);
e2eConsoleLogger(
execSync(
`${gradleCommand} :project-graph:publishToMavenLocal -x :project-graph:signNxProjectGraphPluginPluginMarkerMavenPublication -x :project-graph:signPluginMavenPublication -x :project-graph:publishNxProjectGraphPluginPluginMarkerMavenPublicationToMavenLocal -x :project-graph:publishPluginMavenPublicationToMavenLocal`,
{
cwd: `${__dirname}/../../../..`,
}
).toString()
);
}
function addLocalPluginManagement(filePath: string) {
let content = readFileSync(filePath).toString();
content =
`pluginManagement {
repositories {
mavenLocal()
gradlePluginPortal()
mavenCentral()
// Add other repositories if needed
}
}
` + content;
writeFileSync(filePath, content);
} }

8
gradle.properties Normal file
View File

@ -0,0 +1,8 @@
# This file was generated by the Gradle 'init' task.
# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties
org.gradle.parallel=true
org.gradle.caching=true
# disable the configuration cache for this project https://docs.gradle.org/current/userguide/configuration_cache.html#config_cache:requirements:disallowed_types
# nxProjectGraph is not supported by the configuration cache
org.gradle.configuration-cache=false

11
gradle/libs.versions.toml Normal file
View File

@ -0,0 +1,11 @@
# This file was generated by the Gradle 'init' task.
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format
[plugins]
jvm = { id = "org.jetbrains.kotlin.jvm", version = "1.9.20" }
[versions]
kotlin-gradle-plugin = "2.0.21"
[libraries]
kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin-gradle-plugin" }

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
validateDistributionUrl=false networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# SPDX-License-Identifier: Apache-2.0
#
############################################################################## ##############################################################################
# #
@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@ -84,7 +86,7 @@ done
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@ -203,7 +205,7 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command: # Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped. # and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line. # treated as '${Hostname}' itself on the command line.

View File

@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and @rem See the License for the specific language governing permissions and
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail
@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail

View File

@ -369,6 +369,7 @@
"core-js": "3.36.1", "core-js": "3.36.1",
"enquirer": "~2.3.6", "enquirer": "~2.3.6",
"fast-glob": "3.3.3", "fast-glob": "3.3.3",
"form-data": "^4.0.2",
"framer-motion": "^11.3.0", "framer-motion": "^11.3.0",
"front-matter": "^4.0.2", "front-matter": "^4.0.2",
"glob": "7.1.4", "glob": "7.1.4",

View File

@ -27,7 +27,12 @@
} }
}, },
{ {
"files": ["./package.json"], "files": [
"./package.json",
"./generators.json",
"./executors.json",
"./migrations.json"
],
"parser": "jsonc-eslint-parser", "parser": "jsonc-eslint-parser",
"rules": { "rules": {
"@nx/nx-plugin-checks": "error" "@nx/nx-plugin-checks": "error"

View File

@ -0,0 +1,3 @@
{
"executors": {}
}

View File

@ -17,6 +17,12 @@
"cli": "nx", "cli": "nx",
"description": "Add includeSubprojectsTasks to build.gradle file", "description": "Add includeSubprojectsTasks to build.gradle file",
"factory": "./src/migrations/20-2-0/add-include-subprojects-tasks" "factory": "./src/migrations/20-2-0/add-include-subprojects-tasks"
},
"change-plugin-to-v1": {
"version": "21.0.0-beta.5",
"cli": "nx",
"description": "Change @nx/gradle plugin to version 1",
"factory": "./src/migrations/21-0-0/change-plugin-to-v1"
} }
}, },
"packageJsonUpdates": {} "packageJsonUpdates": {}

View File

@ -26,6 +26,7 @@
"generators": "./generators.json", "generators": "./generators.json",
"exports": { "exports": {
".": "./index.js", ".": "./index.js",
"./plugin-v1": "./plugin-v1.js",
"./package.json": "./package.json", "./package.json": "./package.json",
"./migrations.json": "./migrations.json", "./migrations.json": "./migrations.json",
"./generators.json": "./generators.json" "./generators.json": "./generators.json"
@ -38,5 +39,6 @@
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
} },
"executors": "./executors.json"
} }

View File

@ -0,0 +1,131 @@
import { CreateNodesContext } from '@nx/devkit';
import { TempFs } from '@nx/devkit/internal-testing-utils';
import { createNodesV2 } from './plugin-v1';
import { type GradleReport } from './src/plugin-v1/utils/get-gradle-report';
let gradleReport: GradleReport;
jest.mock('./src/plugin-v1/utils/get-gradle-report', () => {
return {
GRADLE_BUILD_FILES: new Set(['build.gradle', 'build.gradle.kts']),
populateGradleReport: jest.fn().mockImplementation(() => void 0),
getCurrentGradleReport: jest.fn().mockImplementation(() => gradleReport),
};
});
describe('@nx/gradle/plugin-v1', () => {
let createNodesFunction = createNodesV2[1];
let context: CreateNodesContext;
let tempFs: TempFs;
beforeEach(async () => {
tempFs = new TempFs('gradle-plugin');
gradleReport = {
gradleFileToGradleProjectMap: new Map<string, string>([
['proj/build.gradle', 'proj'],
]),
gradleProjectToDepsMap: new Map<string, Set<string>>(),
gradleFileToOutputDirsMap: new Map<string, Map<string, string>>([
['proj/build.gradle', new Map([['build', 'build']])],
]),
gradleProjectToTasksMap: new Map<string, Set<string>>([
['proj', new Set(['test'])],
]),
gradleProjectToTasksTypeMap: new Map<string, Map<string, string>>([
['proj', new Map([['test', 'Verification']])],
]),
gradleProjectToProjectName: new Map<string, string>([['proj', 'proj']]),
gradleProjectNameToProjectRootMap: new Map<string, string>([
['proj', 'proj'],
]),
gradleProjectToChildProjects: new Map<string, string[]>(),
};
context = {
nxJsonConfiguration: {
namedInputs: {
default: ['{projectRoot}/**/*'],
production: ['!{projectRoot}/**/*.spec.ts'],
},
},
workspaceRoot: tempFs.tempDir,
configFiles: [],
};
tempFs.createFileSync('package.json', JSON.stringify({ name: 'repo' }));
tempFs.createFileSync(
'my-app/project.json',
JSON.stringify({ name: 'my-app' })
);
});
afterEach(() => {
jest.resetModules();
tempFs.cleanup();
tempFs = null;
});
it('should create nodes', async () => {
tempFs.createFileSync('gradlew', '');
const nodes = await createNodesFunction(
['gradlew', 'proj/build.gradle'],
undefined,
context
);
expect(nodes).toMatchInlineSnapshot(`
[
[
"proj/build.gradle",
{
"projects": {
"proj": {
"metadata": {
"targetGroups": {
"Verification": [
"test",
],
},
"technologies": [
"gradle",
],
},
"name": "proj",
"projectType": "application",
"targets": {
"test": {
"cache": true,
"command": "./gradlew proj:test",
"dependsOn": [
"testClasses",
],
"inputs": [
"default",
"^production",
],
"metadata": {
"help": {
"command": "./gradlew help --task proj:test",
"example": {
"options": {
"args": [
"--rerun",
],
},
},
},
"technologies": [
"gradle",
],
},
"options": {
"cwd": ".",
},
},
},
},
},
},
],
]
`);
});
});

View File

@ -0,0 +1,2 @@
export { createDependencies } from './src/plugin-v1/dependencies';
export { createNodes, createNodesV2 } from './src/plugin-v1/nodes';

View File

@ -1,14 +1,17 @@
import { CreateNodesContext } from '@nx/devkit'; import { CreateNodesContext, readJsonFile } from '@nx/devkit';
import { TempFs } from '@nx/devkit/internal-testing-utils'; import { TempFs } from '@nx/devkit/internal-testing-utils';
import { createNodesV2 } from './plugin'; import { createNodesV2 } from './plugin';
import { type GradleReport } from './src/utils/get-gradle-report'; import { type ProjectGraphReport } from './src/plugin/utils/get-project-graph-from-gradle-plugin';
import { join } from 'path';
let gradleReport: GradleReport; let gradleReport: ProjectGraphReport;
jest.mock('./src/utils/get-gradle-report', () => { jest.mock('./src/plugin/utils/get-project-graph-from-gradle-plugin', () => {
return { return {
GRADLE_BUILD_FILES: new Set(['build.gradle', 'build.gradle.kts']), GRADLE_BUILD_FILES: new Set(['build.gradle', 'build.gradle.kts']),
populateGradleReport: jest.fn().mockImplementation(() => void 0), populateProjectGraph: jest.fn().mockImplementation(() => void 0),
getCurrentGradleReport: jest.fn().mockImplementation(() => gradleReport), getCurrentProjectGraphReport: jest
.fn()
.mockImplementation(() => gradleReport),
}; };
}); });
@ -19,26 +22,9 @@ describe('@nx/gradle/plugin', () => {
beforeEach(async () => { beforeEach(async () => {
tempFs = new TempFs('gradle-plugin'); tempFs = new TempFs('gradle-plugin');
gradleReport = { gradleReport = readJsonFile(
gradleFileToGradleProjectMap: new Map<string, string>([ join(__dirname, 'src/plugin/utils/__mocks__/gradle_tutorial.json')
['proj/build.gradle', 'proj'], );
]),
buildFileToDepsMap: new Map<string, Set<string>>(),
gradleFileToOutputDirsMap: new Map<string, Map<string, string>>([
['proj/build.gradle', new Map([['build', 'build']])],
]),
gradleProjectToTasksMap: new Map<string, Set<string>>([
['proj', new Set(['test'])],
]),
gradleProjectToTasksTypeMap: new Map<string, Map<string, string>>([
['proj', new Map([['test', 'Verification']])],
]),
gradleProjectToProjectName: new Map<string, string>([['proj', 'proj']]),
gradleProjectNameToProjectRootMap: new Map<string, string>([
['proj', 'proj'],
]),
gradleProjectToChildProjects: new Map<string, string[]>(),
};
context = { context = {
nxJsonConfiguration: { nxJsonConfiguration: {
namedInputs: { namedInputs: {
@ -76,48 +62,33 @@ describe('@nx/gradle/plugin', () => {
[ [
"proj/build.gradle", "proj/build.gradle",
{ {
"externalNodes": {},
"projects": { "projects": {
"proj": { "proj": {
"metadata": { "metadata": {
"targetGroups": { "targetGroups": {
"Verification": [ "help": [
"test", "buildEnvironment",
], ],
}, },
"technologies": [ "technologies": [
"gradle", "gradle",
], ],
}, },
"name": "proj", "name": "gradle-tutorial",
"projectType": "application", "root": "proj",
"targets": { "targets": {
"test": { "buildEnvironment": {
"cache": true, "cache": true,
"command": "./gradlew proj:test", "command": "./gradlew :buildEnvironment",
"dependsOn": [
"testClasses",
],
"inputs": [
"default",
"^production",
],
"metadata": { "metadata": {
"help": { "description": "Displays all buildscript dependencies declared in root project 'gradle-tutorial'.",
"command": "./gradlew help --task proj:test",
"example": {
"options": {
"args": [
"--rerun",
],
},
},
},
"technologies": [ "technologies": [
"gradle", "gradle",
], ],
}, },
"options": { "options": {
"cwd": ".", "cwd": "proj",
}, },
}, },
}, },

View File

@ -1,2 +1,2 @@
export { createDependencies } from './src/plugin/dependencies'; export { createDependencies } from './src/plugin/dependencies';
export { createNodes, createNodesV2 } from './src/plugin/nodes'; export { createNodesV2 } from './src/plugin/nodes';

View File

@ -0,0 +1,3 @@
# Ignore Gradle project-specific cache directory
bin
build

View File

@ -0,0 +1,56 @@
# dev.nx.gradle.project-graph
This gradle plugin contains
## Installation
Kotlin
build.gradle.kts
```
plugins {
id("dev.nx.gradle.project-graph") version("+")
}
```
Groovy
build.gradle
```
plugins {
id "dev.nx.gradle.project-graph" version "+"
}
```
## Usage
```bash
./gradlew nxProjectGraph
```
In terminal, it should output something like:
```
> Task :nxProjectGraph
< your workspace >/build/nx/add-nx-to-gradle.json
```
To pass in a hash parameter:
```bash
./gradlew nxProjectGraph -Phash=12345
```
It generates a json file to be consumed by nx:
```json
{
"nodes": {
"app": {
"targets": {}
}
},
"dependencies": [],
"externalNodes": {}
}
```

View File

@ -0,0 +1,120 @@
plugins {
`java-gradle-plugin`
`maven-publish`
signing
id("com.ncorti.ktfmt.gradle") version "+"
id("dev.nx.gradle.project-graph") version "0.0.2"
id("org.jetbrains.kotlin.jvm") version "2.1.10"
id("com.gradle.plugin-publish") version "1.2.1"
}
group = "dev.nx.gradle"
version = "0.0.2"
repositories { mavenCentral() }
dependencies {
implementation("com.google.code.gson:gson:2.10.1")
testImplementation(kotlin("test"))
testImplementation("org.mockito:mockito-core:5.8.0")
testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.1")
}
java {
withSourcesJar()
withJavadocJar()
}
gradlePlugin {
website = "https://nx.dev/"
vcsUrl = "https://github.com/nrwl/nx"
plugins {
create("nxProjectGraphPlugin") {
id = "dev.nx.gradle.project-graph"
implementationClass = "dev.nx.gradle.NxProjectGraphReportPlugin"
displayName = "The Nx Plugin for Gradle to generate Nx project graph"
description = "Generates a JSON file with nodes, dependencies, and external nodes for Nx"
tags = listOf("nx", "monorepo", "javascript", "typescript")
}
}
}
afterEvaluate {
publishing {
publications.named("pluginMaven", MavenPublication::class) {
pom {
name.set("Nx Gradle Project Graph Plugin")
description.set(
"A plugin to generate a JSON file with nodes, dependencies, and external nodes for Nx")
url.set("https://github.com/nrwl/nx")
licenses { license { name.set("MIT") } }
developers {
developer {
id.set("nx")
name.set("Nx")
email.set("java-services@nrwl.io")
}
}
scm {
connection.set("scm:git:git://github.com/nrwl/nx.git")
developerConnection.set("scm:git:ssh://github.com/nrwl/nx.git")
url.set("https://github.com/nrwl/nx")
}
}
}
repositories {
maven {
name = "localStaging"
url = uri(layout.buildDirectory.dir("staging"))
}
}
}
publishing {
publications.named("nxProjectGraphPluginPluginMarkerMaven", MavenPublication::class) {
pom {
name.set("Nx Gradle Project Graph Plugin")
description.set(
"A plugin to generate a JSON file with nodes, dependencies, and external nodes for Nx")
url.set("https://github.com/nrwl/nx")
licenses { license { name.set("MIT") } }
developers {
developer {
id.set("nx")
name.set("Nx")
email.set("java-services@nrwl.io")
}
}
scm {
connection.set("scm:git:git://github.com/nrwl/nx.git")
developerConnection.set("scm:git:ssh://github.com/nrwl/nx.git")
url.set("https://github.com/nrwl/nx")
}
repositories {
maven {
name = "localStaging"
url = uri(layout.buildDirectory.dir("staging"))
}
}
}
}
}
}
signing {
afterEvaluate {
sign(publishing.publications["pluginMaven"])
sign(publishing.publications["nxProjectGraphPluginPluginMarkerMaven"])
}
}
tasks.test { useJUnitPlatform() }

View File

@ -0,0 +1,43 @@
{
"name": "gradle-project-graph",
"$schema": "node_modules/nx/schemas/project-schema.json",
"targets": {
"test": {
"command": "./gradlew :project-graph:test",
"options": {
"args": []
},
"cache": true
},
"lint": {
"command": "./gradlew :project-graph:ktfmtCheck",
"cache": true
},
"format": {
"command": "./gradlew :project-graph:ktfmtFormat",
"cache": true
},
"publish-staging": {
"command": "./gradlew :project-graph:publish",
"cache": true,
"outputs": ["{projectRoot}/build/staging"]
},
"zip-staging": {
"command": "zip -r ../deployment.zip .",
"options": {
"cwd": "{projectRoot}/build/staging"
},
"inputs": ["{projectRoot}/build/staging"],
"outputs": ["{projectRoot}/build/deployment.zip"],
"dependsOn": ["publish-staging"]
},
"maven": {
"command": "npx ts-node publish-maven.ts --deploymentZipPath=build/deployment.zip",
"options": {
"cwd": "{projectRoot}"
},
"inputs": ["{projectRoot}/build/deployment.zip"],
"dependsOn": ["zip-staging"]
}
}
}

View File

@ -0,0 +1,133 @@
import axios from 'axios';
import * as fs from 'fs';
import * as FormData from 'form-data';
function parseArgs() {
const args = process.argv.slice(2);
const result: Record<string, string> = {};
args.forEach((arg) => {
const [key, value] = arg.replace(/^--/, '').split('=');
result[key] = value;
});
return result;
}
async function publishToMavenApi(
username: string,
password: string,
deploymentZipPath = 'deployment.zip'
) {
const token = Buffer.from(`${username}:${password}`).toString('base64');
console.log(`📦 Publishing to Maven Central...`);
const url = 'https://central.sonatype.com/api/v1/publisher/upload';
const form = new FormData();
form.append('bundle', fs.createReadStream(deploymentZipPath));
let uploadId: string;
try {
const response = await axios.post(url, form, {
headers: {
Authorization: `Basic ${token}`,
...form.getHeaders(),
},
});
uploadId = response.data.toString().trim();
console.log(`✅ Upload ID: ${uploadId}`);
} catch (err: any) {
console.error('🚫 Upload failed:', err.response?.data || err.message);
process.exit(1);
}
let currentStatus = await getUploadStatus(uploadId, token);
if (['PENDING', 'VALIDATING', 'PUBLISHING'].includes(currentStatus)) {
currentStatus = await retryUntilValidatedOrPublished(
currentStatus,
uploadId,
token
);
}
if (!['VALIDATED', 'PUBLISHED'].includes(currentStatus)) {
console.error(`🚫 Upload failed with final status: ${currentStatus}`);
process.exit(1);
}
console.log(`📦 Upload is ${currentStatus}, proceeding to deploy...`);
if (currentStatus === 'PUBLISHED') {
console.log('✅ Already published, skipping deployment.');
return;
}
const deployUrl = `https://central.sonatype.com/api/v1/publisher/deployment/${uploadId}`;
try {
const deployRes = await axios.post(deployUrl, null, {
headers: { Authorization: `Basic ${token}` },
});
console.log(`🚀 Deployment response: ${deployRes.data}`);
} catch (err: any) {
console.error('🚫 Deployment failed:', err.response?.data || err.message);
process.exit(1);
}
}
async function getUploadStatus(
uploadId: string,
token: string
): Promise<string> {
const url = `https://central.sonatype.com/api/v1/publisher/status?id=${uploadId}`;
try {
const response = await axios.post(url, null, {
headers: { Authorization: `Basic ${token}` },
});
const state = response.data.deploymentState;
console.log(`📡 Current deployment state: ${state}`);
return state;
} catch (err: any) {
console.error(
'🚫 Failed to get status:',
err.response?.data || err.message
);
return 'FAILED';
}
}
async function retryUntilValidatedOrPublished(
currentStatus: string,
uploadId: string,
token: string,
retries = 10,
delay = 10_000
): Promise<string> {
for (let i = 0; i < retries; i++) {
console.log(`🔁 Checking status (attempt ${i + 1}/${retries})...`);
await sleep(delay);
currentStatus = await getUploadStatus(uploadId, token);
if (['VALIDATED', 'PUBLISHED', 'FAILED'].includes(currentStatus)) break;
}
return currentStatus;
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// Entry
(async function main() {
let { username, password, deploymentZipPath } = parseArgs();
username = username || process.env.MAVEN_USERNAME;
password = password || process.env.MAVEN_PASSWORD;
if (!username || !password) {
console.error('❌ Missing MAVEN_USERNAME or MAVEN_PASSWORD');
process.exit(1);
}
if (!deploymentZipPath) {
console.error('❌ Missing required --deploymentZipPath argument');
process.exit(1);
}
await publishToMavenApi(username, password, deploymentZipPath);
})();

View File

@ -0,0 +1,16 @@
/*
* This file was generated by the Gradle 'init' task.
*
* The settings file is used to specify which projects to include in your build.
* For more detailed information on multi-project builds, please refer to https://docs.gradle.org/8.5/userguide/building_swift_projects.html in the Gradle documentation.
*/
pluginManagement {
repositories {
mavenLocal()
mavenCentral()
gradlePluginPortal()
}
}
rootProject.name = "project-graph"

View File

@ -0,0 +1,94 @@
package dev.nx.gradle
import java.util.*
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.TaskProvider
class NxProjectGraphReportPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.logger.info("${Date()} Applying NxProjectGraphReportPlugin to ${project.name}")
val nxProjectReportTask: TaskProvider<NxProjectReportTask> =
project.tasks.register("nxProjectReport", NxProjectReportTask::class.java) { task ->
val hashProperty =
project.findProperty("hash")?.toString()
?: run {
project.logger.warn(
"No 'hash' property was provided for $project. Using default hash value: 'default-hash'")
"default-hash"
}
val cwdProperty =
project.findProperty("cwd")?.toString()
?: run {
project.logger.warn(
"No 'cwd' property was provided for $project. Using default hash value: ${System.getProperty("user.dir")}")
System.getProperty("user.dir")
}
val workspaceRootProperty =
project.findProperty("workspaceRoot")?.toString()
?: run {
project.logger.warn(
"No 'workspaceRoot' property was provided for $project. Using default hash value: ${System.getProperty("user.dir")}")
System.getProperty("user.dir")
}
val targetNameOverrides: Map<String, String> =
project.properties
.filterKeys { it.endsWith("TargetName") }
.mapValues { it.value.toString() }
task.projectName.set(project.name)
task.projectRef.set(project)
task.hash.set(hashProperty)
task.targetNameOverrides.set(targetNameOverrides)
task.cwd.set(cwdProperty)
task.workspaceRoot.set(workspaceRootProperty)
task.description = "Create Nx project report for ${project.name}"
task.group = "Reporting"
task.doFirst { it.logger.info("${Date()} Running nxProjectReport for ${project.name}") }
}
// Ensure all included builds are processed only once using lazy evaluation
project.gradle.includedBuilds.distinct().forEach { includedBuild ->
nxProjectReportTask.configure { it.dependsOn(includedBuild.task(":nxProjectReport")) }
}
// Ensure all subprojects are processed only once using lazy evaluation
project.subprojects.distinct().forEach { subProject ->
// Add a dependency on each subproject's nxProjectReport task
nxProjectReportTask.configure {
it.dependsOn(subProject.tasks.matching { it.name == "nxProjectReport" })
}
}
project.tasks.register("nxProjectGraph").configure { task ->
task.dependsOn(nxProjectReportTask)
task.description = "Create Nx project graph for ${project.name}"
task.group = "Reporting"
val outputFileProvider = nxProjectReportTask.map { it.outputFile }
task.doFirst { it.logger.info("${Date()} Running nxProjectGraph for ${project.name}") }
task.doLast { println(outputFileProvider.get().path) }
}
// Ensure all included builds are processed only once using lazy evaluation
project.gradle.includedBuilds.distinct().forEach { includedBuild ->
project.tasks.named("nxProjectGraph").configure {
it.dependsOn(includedBuild.task(":nxProjectGraph"))
}
}
// Ensure all subprojects are processed only once using lazy evaluation
project.subprojects.distinct().forEach { subProject ->
project.tasks.named("nxProjectGraph").configure {
it.dependsOn(subProject.tasks.matching { it.name == "nxProjectGraph" })
}
}
}
}

View File

@ -0,0 +1,63 @@
package dev.nx.gradle
import com.google.gson.Gson
import dev.nx.gradle.utils.createNodeForProject
import java.io.File
import java.util.*
import javax.inject.Inject
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.file.ProjectLayout
import org.gradle.api.provider.MapProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
@CacheableTask
abstract class NxProjectReportTask @Inject constructor(private val projectLayout: ProjectLayout) :
DefaultTask() {
companion object {
private val gson = Gson()
}
@get:Input abstract val projectName: Property<String>
@get:Input abstract val hash: Property<String>
@get:Input abstract val cwd: Property<String>
@get:Input abstract val workspaceRoot: Property<String>
@get:Input abstract val targetNameOverrides: MapProperty<String, String>
// Don't compute report at configuration time, move it to execution time
@get:Internal // Prevent Gradle from caching this reference
abstract val projectRef: Property<Project>
@get:OutputFile
val outputFile: File
get() = projectLayout.buildDirectory.file("nx/${projectName.get()}.json").get().asFile
@TaskAction
fun action() {
logger.info("${Date()} Apply task action NxProjectReportTask for ${projectName.get()}")
logger.info("${Date()} Hash input: ${hash.get()}")
logger.info("${Date()} Target Name Overrides ${targetNameOverrides.get()}")
val project = projectRef.get() // Get project reference at execution time
val report =
createNodeForProject(
project,
targetNameOverrides.get(),
workspaceRoot.get(),
cwd.get()) // Compute report at execution time
val reportJson = gson.toJson(report)
if (outputFile.exists() && outputFile.readText() == reportJson) {
logger.info("${Date()} No change in the node report for ${projectName.get()}")
return
}
logger.info("${Date()} Writing node report for ${projectName.get()}")
outputFile.writeText(reportJson)
}
}

View File

@ -0,0 +1,6 @@
package dev.nx.gradle.data
import java.io.Serializable
data class Dependency(val source: String, val target: String, var sourceFile: String) :
Serializable

View File

@ -0,0 +1,10 @@
package dev.nx.gradle.data
import java.io.Serializable
import org.gradle.api.tasks.Input
data class ExternalDepData(
@Input val version: String?,
@Input val packageName: String,
@Input val hash: String?
) : Serializable

View File

@ -0,0 +1,11 @@
package dev.nx.gradle.data
import java.io.Serializable
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Nested
data class ExternalNode(
@Input var type: String?,
@Input val name: String,
@Nested var data: ExternalDepData
) : Serializable

View File

@ -0,0 +1,11 @@
package dev.nx.gradle.data
import java.io.Serializable
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Nested
data class GradleNodeReport(
@Nested val nodes: Map<String, ProjectNode>,
@Input val dependencies: Set<Dependency>,
@Nested val externalNodes: Map<String, ExternalNode>
) : Serializable

View File

@ -0,0 +1,17 @@
package dev.nx.gradle.data
import java.io.Serializable
typealias NxTarget = MutableMap<String, Any?>
typealias NxTargets = MutableMap<String, NxTarget>
typealias TargetGroup = MutableList<String>
typealias TargetGroups = MutableMap<String, TargetGroup>
data class GradleTargets(
val targets: NxTargets,
val targetGroups: TargetGroups,
var externalNodes: MutableMap<String, ExternalNode>
) : Serializable

View File

@ -0,0 +1,9 @@
package dev.nx.gradle.data
import java.io.Serializable
data class NodeMetadata(
val targetGroups: TargetGroups,
val technologies: List<String>,
val description: String?
) : Serializable

View File

@ -0,0 +1,10 @@
package dev.nx.gradle.data
import java.io.Serializable
import org.gradle.api.tasks.Input
data class ProjectNode(
@Input val targets: NxTargets,
@Input val metadata: NodeMetadata,
@Input val name: String
) : Serializable

View File

@ -0,0 +1,143 @@
package dev.nx.gradle.utils
import dev.nx.gradle.data.*
import java.io.File
import org.gradle.api.Task
import org.gradle.api.file.FileCollection
const val testCiTargetGroup = "verification"
/**
* Add atomized ci test targets Going to loop through each test files and create a target for each
* It is going to modify targets and targetGroups in place
*/
fun addTestCiTargets(
testFiles: FileCollection,
projectBuildPath: String,
testTask: Task,
targets: NxTargets,
targetGroups: TargetGroups,
projectRoot: String,
workspaceRoot: String,
ciTargetName: String
) {
ensureTargetGroupExists(targetGroups, testCiTargetGroup)
val gradlewCommand = getGradlewCommand()
val ciDependsOn = mutableListOf<Map<String, String>>()
val filteredTestFiles = testFiles.filter { isTestFile(it, workspaceRoot) }
filteredTestFiles.forEach { testFile ->
val className = getTestClassNameIfAnnotated(testFile) ?: return@forEach
val testCiTarget =
buildTestCiTarget(
gradlewCommand = gradlewCommand,
projectBuildPath = projectBuildPath,
testClassName = className,
testFile = testFile,
testTask = testTask,
projectRoot = projectRoot,
workspaceRoot = workspaceRoot)
val targetName = "$ciTargetName--$className"
targets[targetName] = testCiTarget
targetGroups[testCiTargetGroup]?.add(targetName)
ciDependsOn.add(mapOf("target" to targetName, "projects" to "self", "params" to "forward"))
}
testTask.logger.info("$testTask ci tasks: $ciDependsOn")
if (ciDependsOn.isNotEmpty()) {
ensureParentCiTarget(
targets = targets,
targetGroups = targetGroups,
ciTargetName = ciTargetName,
projectBuildPath = projectBuildPath,
dependsOn = ciDependsOn)
}
}
private fun getTestClassNameIfAnnotated(file: File): String? {
if (!file.exists()) return null
val content = file.readText()
if (!content.contains("@Test")) return null
val classRegex = Regex("""class\s+([A-Za-z_][A-Za-z0-9_]*)""")
val match = classRegex.find(content)
return match?.groupValues?.get(1)
}
fun ensureTargetGroupExists(targetGroups: TargetGroups, group: String) {
if (!targetGroups.containsKey(group)) {
targetGroups[group] = mutableListOf()
}
}
private fun isTestFile(file: File, workspaceRoot: String): Boolean {
val fileName = file.name.substringBefore(".")
val regex = "^(?!abstract).*?(Test)(s)?\\d*".toRegex(RegexOption.IGNORE_CASE)
return file.path.startsWith(workspaceRoot) && regex.matches(fileName)
}
private fun buildTestCiTarget(
gradlewCommand: String,
projectBuildPath: String,
testClassName: String,
testFile: File,
testTask: Task,
projectRoot: String,
workspaceRoot: String
): MutableMap<String, Any?> {
val target =
mutableMapOf<String, Any?>(
"command" to "$gradlewCommand ${projectBuildPath}:test --tests $testClassName",
"metadata" to
getMetadata("Runs Gradle test $testClassName in CI", projectBuildPath, "test"),
"cache" to true,
"inputs" to arrayOf(replaceRootInPath(testFile.path, projectRoot, workspaceRoot)))
getDependsOnForTask(testTask, null)
?.takeIf { it.isNotEmpty() }
?.let {
testTask.logger.info("$testTask: processed ${it.size} dependsOn")
target["dependsOn"] = it
}
getOutputsForTask(testTask, projectRoot, workspaceRoot)
?.takeIf { it.isNotEmpty() }
?.let {
testTask.logger.info("$testTask: processed ${it.size} outputs")
target["outputs"] = it
}
return target
}
private fun ensureParentCiTarget(
targets: NxTargets,
targetGroups: TargetGroups,
ciTargetName: String,
projectBuildPath: String,
dependsOn: List<Map<String, String>>
) {
val ciTarget =
targets.getOrPut(ciTargetName) {
mutableMapOf<String, Any?>().apply {
put("executor", "nx:noop")
put("metadata", getMetadata("Runs Gradle Tests in CI", projectBuildPath, "test", "test"))
put("dependsOn", mutableListOf<Map<String, String>>())
put("cache", true)
}
}
val dependsOnList = ciTarget.getOrPut("dependsOn") { mutableListOf<Any?>() } as MutableList<Any?>
dependsOnList.addAll(dependsOn)
if (targetGroups[testCiTargetGroup]?.contains(ciTargetName) != true) {
targetGroups[testCiTargetGroup]?.add(ciTargetName)
}
}

View File

@ -0,0 +1,159 @@
package dev.nx.gradle.utils
import dev.nx.gradle.data.*
import java.util.*
import org.gradle.api.Project
/** Loops through a project and populate dependencies and nodes for each target */
fun createNodeForProject(
project: Project,
targetNameOverrides: Map<String, String>,
workspaceRoot: String,
cwd: String
): GradleNodeReport {
val logger = project.logger
logger.info("${Date()} ${project.name} createNodeForProject: get nodes and dependencies")
// Initialize dependencies with an empty Set to prevent null issues
val dependencies: MutableSet<Dependency> =
try {
getDependenciesForProject(project)
} catch (e: Exception) {
logger.info(
"${Date()} ${project.name} createNodeForProject: get dependencies error: ${e.message}")
mutableSetOf()
}
logger.info("${Date()} ${project.name} createNodeForProject: got dependencies")
// Initialize nodes and externalNodes with empty maps to prevent null issues
var nodes: Map<String, ProjectNode>
var externalNodes: Map<String, ExternalNode>
try {
val gradleTargets: GradleTargets =
processTargetsForProject(project, dependencies, targetNameOverrides, workspaceRoot, cwd)
val projectRoot = project.projectDir.path
val projectNode =
ProjectNode(
targets = gradleTargets.targets,
metadata =
NodeMetadata(gradleTargets.targetGroups, listOf("gradle"), project.description),
name = project.name)
nodes = mapOf(projectRoot to projectNode)
externalNodes = gradleTargets.externalNodes
logger.info(
"${Date()} ${project.name} createNodeForProject: get nodes and external nodes for $projectRoot")
} catch (e: Exception) {
logger.info("${project.name}: get nodes error: ${e.message}")
nodes = emptyMap()
externalNodes = emptyMap()
}
return GradleNodeReport(nodes, dependencies, externalNodes)
}
/**
* Process targets for project
*
* @return targets and targetGroups
*/
fun processTargetsForProject(
project: Project,
dependencies: MutableSet<Dependency>,
targetNameOverrides: Map<String, String>,
workspaceRoot: String,
cwd: String
): GradleTargets {
val targets: NxTargets = mutableMapOf<String, MutableMap<String, Any?>>()
val targetGroups: TargetGroups = mutableMapOf<String, MutableList<String>>()
val externalNodes = mutableMapOf<String, ExternalNode>()
val projectRoot = project.projectDir.path
project.logger.info("Using workspace root $workspaceRoot")
var projectBuildPath: String =
project
.buildTreePath // get the build path of project e.g. :app, :utils:number-utils, :buildSrc
if (projectBuildPath.endsWith(":")) { // root project is ":", manually remove last :
projectBuildPath = projectBuildPath.dropLast(1)
}
val logger = project.logger
logger.info("${Date()} ${project}: process targets")
var gradleProject = project.buildTreePath
if (!gradleProject.endsWith(":")) {
gradleProject += ":"
}
project.tasks.forEach { task ->
try {
logger.info("${Date()} ${project.name}: Processing $task")
val taskName = targetNameOverrides.getOrDefault(task.name + "TargetName", task.name)
// add task to target groups
val group: String? = task.group
if (!group.isNullOrBlank()) {
if (targetGroups.contains(group)) {
targetGroups[group]?.add(task.name)
} else {
targetGroups[group] = mutableListOf(task.name)
}
}
val target =
processTask(
task,
projectBuildPath,
projectRoot,
workspaceRoot,
cwd,
externalNodes,
dependencies,
targetNameOverrides)
targets[taskName] = target
val ciTargetName = targetNameOverrides.getOrDefault("ciTargetName", null)
ciTargetName?.let {
if (task.name.startsWith("compileTest")) {
val testTask = project.getTasksByName("test", false)
if (testTask.isNotEmpty()) {
addTestCiTargets(
task.inputs.sourceFiles,
projectBuildPath,
testTask.first(),
targets,
targetGroups,
projectRoot,
workspaceRoot,
it)
}
}
// Add the `$ciTargetName-check` target when processing the "check" task
if (task.name == "check") {
val replacedDependencies =
(target["dependsOn"] as? List<*>)?.map { dep ->
if (dep.toString() == targetNameOverrides.getOrDefault("testTargetName", "test"))
ciTargetName
else dep.toString()
} ?: emptyList()
// Copy the original target and override "dependsOn"
val newTarget = target.toMutableMap()
newTarget["dependsOn"] = replacedDependencies
val ciCheckTargetName = "$ciTargetName-check"
targets[ciCheckTargetName] = newTarget
ensureTargetGroupExists(targetGroups, testCiTargetGroup)
targetGroups[testCiTargetGroup]?.add(ciCheckTargetName)
}
}
logger.info("${Date()} ${project.name}: Processed $task")
} catch (e: Exception) {
logger.info("${task}: process task error $e")
logger.debug("Stack trace:", e)
}
}
return GradleTargets(targets, targetGroups, externalNodes)
}

View File

@ -0,0 +1,348 @@
package dev.nx.gradle.utils
import dev.nx.gradle.data.*
import org.gradle.api.Named
import org.gradle.api.NamedDomainObjectProvider
import org.gradle.api.Task
import org.gradle.api.tasks.TaskProvider
/**
* Process a task and convert it into target Going to populate:
* - cache
* - inputs
* - outputs
* - command
* - metadata
* - options with cwd and args
*/
fun processTask(
task: Task,
projectBuildPath: String,
projectRoot: String,
workspaceRoot: String,
cwd: String,
externalNodes: MutableMap<String, ExternalNode>,
dependencies: MutableSet<Dependency>,
targetNameOverrides: Map<String, String>
): MutableMap<String, Any?> {
val logger = task.logger
logger.info("NxProjectReportTask: process $task for $projectRoot")
val target = mutableMapOf<String, Any?>()
target["cache"] = true // set cache to be always true
// process inputs
val inputs = getInputsForTask(task, projectRoot, workspaceRoot, externalNodes)
if (!inputs.isNullOrEmpty()) {
logger.info("${task}: processed ${inputs.size} inputs")
target["inputs"] = inputs
}
// process outputs
val outputs = getOutputsForTask(task, projectRoot, workspaceRoot)
if (!outputs.isNullOrEmpty()) {
logger.info("${task}: processed ${outputs.size} outputs")
target["outputs"] = outputs
}
// process dependsOn
val dependsOn = getDependsOnForTask(task, dependencies, targetNameOverrides)
if (!dependsOn.isNullOrEmpty()) {
logger.info("${task}: processed ${dependsOn.size} dependsOn")
target["dependsOn"] = dependsOn
}
val gradlewCommand = getGradlewCommand()
target["command"] = "$gradlewCommand ${projectBuildPath}:${task.name}"
val metadata = getMetadata(task.description ?: "Run ${task.name}", projectBuildPath, task.name)
target["metadata"] = metadata
target["options"] = mapOf("cwd" to cwd)
return target
}
fun getGradlewCommand(): String {
val gradlewCommand: String
val operatingSystem = System.getProperty("os.name").lowercase()
gradlewCommand =
if (operatingSystem.contains("win")) {
".\\gradlew.bat"
} else {
"./gradlew"
}
return gradlewCommand
}
/**
* Parse task and get inputs for this task
*
* @param task task to process
* @return a list of inputs including external dependencies, null if empty or an error occurred
*/
fun getInputsForTask(
task: Task,
projectRoot: String,
workspaceRoot: String,
externalNodes: MutableMap<String, ExternalNode>?
): MutableList<Any>? {
return try {
val mappedInputsIncludeExternal: MutableList<Any> = mutableListOf()
val inputs = task.inputs
val externalDependencies = mutableListOf<String>()
inputs.sourceFiles.forEach { file ->
val path: String = file.path
// replace the absolute path to contain {projectRoot} or {workspaceRoot}
val pathWithReplacedRoot = replaceRootInPath(path, projectRoot, workspaceRoot)
if (pathWithReplacedRoot != null) { // if the path is inside workspace
mappedInputsIncludeExternal.add((pathWithReplacedRoot))
}
// if the path is outside of workspace
if (pathWithReplacedRoot == null &&
externalNodes != null) { // add it to external dependencies
try {
val externalDep = getExternalDepFromInputFile(path, externalNodes, task.logger)
externalDep?.let { externalDependencies.add(it) }
} catch (e: Exception) {
task.logger.info("${task}: get external dependency error $e")
}
}
}
if (externalDependencies.isNotEmpty()) {
mappedInputsIncludeExternal.add(mutableMapOf("externalDependencies" to externalDependencies))
}
if (mappedInputsIncludeExternal.isNotEmpty()) {
return mappedInputsIncludeExternal
}
return null
} catch (e: Exception) {
// Log the error but don't fail the build
task.logger.info("Error getting outputs for ${task.path}: ${e.message}")
task.logger.debug("Stack trace:", e)
null
}
}
/**
* Get outputs for task
*
* @param task task to process
* @return list of outputs file, will not include if output file is outside workspace, null if empty
* or an error occurred
*/
fun getOutputsForTask(task: Task, projectRoot: String, workspaceRoot: String): List<String>? {
return try {
val outputs = task.outputs.files
if (!outputs.isEmpty) {
return outputs.mapNotNull { file ->
val path: String = file.path
replaceRootInPath(path, projectRoot, workspaceRoot)
}
}
null
} catch (e: Exception) {
// Log the error but don't fail the build
task.logger.info("Error getting outputs for ${task.path}: ${e.message}")
task.logger.debug("Stack trace:", e)
null
}
}
/**
* Get dependsOn for task, handling configuration timing safely. Rewrites dependency task names
* based on targetNameOverrides (e.g., test -> ci).
*
* @param task task to process
* @param dependencies optional set to collect inter-project Dependency objects
* @param targetNameOverrides optional map of overrides (e.g., test -> ci)
* @return list of dependsOn task names (possibly replaced), or null if none found or error occurred
*/
fun getDependsOnForTask(
task: Task,
dependencies: MutableSet<Dependency>?,
targetNameOverrides: Map<String, String> = emptyMap()
): List<String>? {
fun mapTasksToNames(tasks: Collection<Task>): List<String> {
return tasks.map { depTask ->
val depProject = depTask.project
val taskProject = task.project
if (task.name != "buildDependents" && depProject != taskProject && dependencies != null) {
dependencies.add(
Dependency(
taskProject.projectDir.path,
depProject.projectDir.path,
taskProject.buildFile.path))
}
// Check if this task name needs to be overridden
val taskName = targetNameOverrides.getOrDefault(depTask.name + "TargetName", depTask.name)
val overriddenTaskName =
if (depProject == taskProject) {
taskName
} else {
"${depProject.name}:${taskName}"
}
overriddenTaskName
}
}
return try {
val dependsOnEntries = task.dependsOn
// Prefer task.dependsOn
if (dependsOnEntries.isNotEmpty()) {
val resolvedTasks =
dependsOnEntries.flatMap { dep ->
when (dep) {
is Task -> listOf(dep)
is TaskProvider<*>,
is NamedDomainObjectProvider<*> -> {
val providerName = (dep as Named).name
val foundTask = task.project.tasks.findByName(providerName)
if (foundTask != null) {
listOf(foundTask)
} else {
task.logger.info(
"${dep::class.simpleName} '$providerName' did not resolve to a task in project ${task.project.name}")
emptyList()
}
}
is String -> {
val foundTask = task.project.tasks.findByPath(dep)
if (foundTask != null) {
listOf(foundTask)
} else {
task.logger.info(
"Task string '$dep' could not be resolved in project ${task.project.name}")
emptyList()
}
}
else -> {
task.logger.info(
"Unhandled dependency type ${dep::class.java} for task ${task.path}")
emptyList()
}
}
}
if (resolvedTasks.isNotEmpty()) {
return mapTasksToNames(resolvedTasks)
}
}
// Fallback: taskDependencies.getDependencies(task)
val fallbackDeps =
try {
task.taskDependencies.getDependencies(null)
} catch (e: Exception) {
task.logger.info("Error calling getDependencies for ${task.path}: ${e.message}")
task.logger.debug("Stack trace:", e)
emptySet<Task>()
}
if (fallbackDeps.isNotEmpty()) {
return mapTasksToNames(fallbackDeps)
}
null
} catch (e: Exception) {
task.logger.info("Unexpected error getting dependencies for ${task.path}: ${e.message}")
task.logger.debug("Stack trace:", e)
null
}
}
/**
* Get metadata for task
*
* @param description
*/
fun getMetadata(
description: String?,
projectBuildPath: String,
taskName: String,
nonAtomizedTarget: String? = null
): Map<String, Any?> {
val gradlewCommand = getGradlewCommand()
return mapOf(
"description" to description,
"technologies" to arrayOf("gradle"),
"help" to mapOf("command" to "$gradlewCommand help --task ${projectBuildPath}:${taskName}"),
"nonAtomizedTarget" to nonAtomizedTarget)
}
/**
* Converts a file path like:
* org.apache.commons/commons-lang3/3.13.0/b7263237aa89c1f99b327197c41d0669707a462e/commons-lang3-3.13.0.jar
*
* Into an external dependency with key: "gradle:commons-lang3-3.13.0" with value: { "type":
* "gradle", "name": "commons-lang3", "data": { "version": "3.13.0", "packageName":
* "org.apache.commons.commons-lang3", "hash": "b7263237aa89c1f99b327197c41d0669707a462e",} }
*
* @param inputFile Path to the dependency jar.
* @param externalNodes Map to populate with the resulting ExternalNode.
* @return The external dependency key (e.g., gradle:commons-lang3-3.13.0), or null if parsing
* fails.
*/
fun getExternalDepFromInputFile(
inputFile: String,
externalNodes: MutableMap<String, ExternalNode>,
logger: org.gradle.api.logging.Logger
): String? {
try {
val segments = inputFile.split("/")
// Expecting at least 5 segments to safely extract group, package, version, hash, filename
if (segments.size < 5) {
logger.warn("Invalid input path: '$inputFile'. Expected at least 5 segments.")
return null
}
val fileName = segments.last()
// Remove any file extension (after the last dot), if present
val nameKey = fileName.substringBeforeLast(".", fileName)
val hash = segments[segments.size - 2]
val version = segments[segments.size - 3]
val packageName = segments[segments.size - 4]
val packageGroup = segments[segments.size - 5]
val fullPackageName = "$packageGroup.$packageName"
val data = ExternalDepData(version, fullPackageName, hash)
val externalKey = "gradle:$nameKey"
val node = ExternalNode("gradle", externalKey, data)
externalNodes[externalKey] = node
return externalKey
} catch (e: Exception) {
logger.warn("Failed to parse inputFile '$inputFile': ${e.message}")
logger.debug("Stack trace:", e)
return null
}
}
/**
* Going to replace the projectRoot with {projectRoot} and workspaceRoot with {workspaceRoot}
*
* @return mapped path if inside workspace, null if outside workspace
*/
fun replaceRootInPath(p: String, projectRoot: String, workspaceRoot: String): String? {
var path = p
if (path.startsWith(projectRoot)) {
path = path.replace(projectRoot, "{projectRoot}")
return path
} else if (path.startsWith(workspaceRoot)) {
path = path.replace(workspaceRoot, "{workspaceRoot}")
return path
}
return null
}

View File

@ -0,0 +1,30 @@
package dev.nx.gradle.utils
import dev.nx.gradle.data.Dependency
import org.gradle.api.Project
private val dependencyCache = mutableMapOf<Project, Set<Dependency>>()
fun getDependenciesForProject(project: Project): MutableSet<Dependency> {
return dependencyCache
.getOrPut(project) { buildDependenciesForProject(project) }
.toMutableSet() // Return a new mutable copy to prevent modifying the cached set
}
private fun buildDependenciesForProject(project: Project): Set<Dependency> {
val dependencies = mutableSetOf<Dependency>()
// Include subprojects manually
project.subprojects.forEach { childProject ->
dependencies.add(
Dependency(project.projectDir.path, childProject.projectDir.path, project.buildFile.path))
}
// Include included builds manually
project.gradle.includedBuilds.forEach { includedBuild ->
dependencies.add(
Dependency(project.projectDir.path, includedBuild.projectDir.path, project.buildFile.path))
}
return dependencies
}

View File

@ -0,0 +1,81 @@
package dev.nx.gradle.utils
import dev.nx.gradle.data.*
import java.io.File
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.testfixtures.ProjectBuilder
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class AddTestCiTargetsTest {
private lateinit var project: Project
private lateinit var testTask: Task
private lateinit var workspaceRoot: File
private lateinit var projectRoot: File
@BeforeEach
fun setup() {
workspaceRoot = createTempDir("workspace")
projectRoot = File(workspaceRoot, "project-a").apply { mkdirs() }
project = ProjectBuilder.builder().withProjectDir(projectRoot).build()
testTask = project.task("test")
}
@Test
fun `should generate test CI targets and group correctly`() {
val testFile1 =
File(projectRoot, "src/test/kotlin/MyFirstTest.kt").apply {
parentFile.mkdirs()
writeText("@Test class MyFirstTest")
}
val testFile2 =
File(projectRoot, "src/test/kotlin/AnotherTest.kt").apply {
parentFile.mkdirs()
writeText("@Test class AnotherTest")
}
val testFiles = project.files(testFile1, testFile2)
val targets = mutableMapOf<String, MutableMap<String, Any?>>()
val targetGroups = mutableMapOf<String, MutableList<String>>()
val ciTargetName = "ci"
addTestCiTargets(
testFiles = testFiles,
projectBuildPath = ":project-a",
testTask = testTask,
targets = targets,
targetGroups = targetGroups,
projectRoot = projectRoot.absolutePath,
workspaceRoot = workspaceRoot.absolutePath,
ciTargetName = ciTargetName)
// Assert each test file created a CI target
assertTrue(targets.containsKey("ci--MyFirstTest"))
assertTrue(targets.containsKey("ci--AnotherTest"))
// Assert test group contains individual targets and parent ci task
val group = targetGroups[testCiTargetGroup]
assertTrue(group != null)
assertTrue(group!!.contains("ci--MyFirstTest"))
assertTrue(group.contains("ci--AnotherTest"))
assertTrue(group.contains("ci"))
// Assert parent CI task includes dependsOn
val parentCi = targets["ci"]
val dependsOn = parentCi?.get("dependsOn") as? List<*>
assertEquals(2, dependsOn!!.size)
val firstTarget = targets["ci--MyFirstTest"]!!
assertTrue(firstTarget["command"].toString().contains("--tests MyFirstTest"))
assertEquals(true, firstTarget["cache"])
assertTrue((firstTarget["inputs"] as Array<*>)[0].toString().contains("{projectRoot}"))
assertEquals("nx:noop", parentCi["executor"])
}
}

View File

@ -0,0 +1,54 @@
package dev.nx.gradle.utils
import dev.nx.gradle.data.*
import kotlin.test.*
import org.gradle.testfixtures.ProjectBuilder
class CreateNodeForProjectTest {
@Test
fun `should return GradleNodeReport with targets and metadata`() {
// Arrange
val workspaceRoot = createTempDir("workspace").absolutePath
val projectDir = createTempDir("project")
val project = ProjectBuilder.builder().withProjectDir(projectDir).build()
// Create a couple of dummy tasks
project.task("compileJava").apply {
group = "build"
description = "Compiles Java sources"
}
project.task("test").apply {
group = "verification"
description = "Runs the tests"
}
val targetNameOverrides = mapOf<String, String>()
// Act
val result =
createNodeForProject(
project = project,
targetNameOverrides = targetNameOverrides,
workspaceRoot = workspaceRoot,
cwd = "{projectRoot}")
// Assert
val projectRoot = project.projectDir.absolutePath
assertTrue(result.nodes.containsKey(projectRoot), "Expected node for project root")
val projectNode = result.nodes[projectRoot]
assertNotNull(projectNode, "ProjectNode should not be null")
// Check target metadata
assertEquals(project.name, projectNode.name)
assertNotNull(projectNode.targets["compileJava"], "Expected compileJava target")
assertNotNull(projectNode.targets["test"], "Expected test target")
assertEquals("build", projectNode.metadata.targetGroups.keys.firstOrNull())
// Dependencies and external nodes should default to empty
assertTrue(result.dependencies.isEmpty(), "Expected no dependencies")
assertTrue(result.externalNodes.isEmpty(), "Expected no external nodes")
}
}

View File

@ -0,0 +1,96 @@
package dev.nx.gradle.utils
import dev.nx.gradle.data.Dependency
import dev.nx.gradle.data.ExternalNode
import org.gradle.testfixtures.ProjectBuilder
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import org.mockito.kotlin.*
class ProcessTaskUtilsTest {
@Test
fun `test replaceRootInPath`() {
val path = "/home/user/workspace/project/src/main/java"
val projectRoot = "/home/user/workspace/project"
val workspaceRoot = "/home/user/workspace"
assertEquals("{projectRoot}/src/main/java", replaceRootInPath(path, projectRoot, workspaceRoot))
assertEquals(
"{workspaceRoot}/project/src/main/java",
replaceRootInPath(path, "/other/path", workspaceRoot))
assertNull(replaceRootInPath("/external/other", projectRoot, workspaceRoot))
}
@Test
fun `test getGradlewCommand`() {
val command = getGradlewCommand()
assertTrue(command.contains("gradlew"))
}
@Test
fun `test getMetadata`() {
val metadata = getMetadata("Compile Java", ":project", "compileJava")
assertEquals("Compile Java", metadata["description"])
assertEquals("gradle", (metadata["technologies"] as Array<*>)[0])
}
@Test
fun `test getExternalDepFromInputFile valid path`() {
val externalNodes = mutableMapOf<String, ExternalNode>()
val path = "org/apache/commons/commons-lang3/3.13.0/hash/commons-lang3-3.13.0.jar"
val key = getExternalDepFromInputFile(path, externalNodes, mock())
assertEquals("gradle:commons-lang3-3.13.0", key)
assertTrue(externalNodes.containsKey("gradle:commons-lang3-3.13.0"))
}
@Test
fun `test getExternalDepFromInputFile invalid path`() {
val externalNodes = mutableMapOf<String, ExternalNode>()
val key = getExternalDepFromInputFile("invalid/path.jar", externalNodes, mock())
assertNull(key)
assertTrue(externalNodes.isEmpty())
}
@Test
fun `test getDependsOnForTask with direct dependsOn`() {
val project = ProjectBuilder.builder().build()
val taskA = project.tasks.register("taskA").get()
val taskB = project.tasks.register("taskB").get()
taskA.dependsOn(taskB)
val dependencies = mutableSetOf<Dependency>()
val dependsOn = getDependsOnForTask(taskA, dependencies)
assertNotNull(dependsOn)
assertTrue(dependsOn!!.contains("taskB"))
}
@Test
fun `test processTask basic properties`() {
val project = ProjectBuilder.builder().build()
val task = project.tasks.register("compileJava").get()
task.group = "build"
task.description = "Compiles Java source files"
val result =
processTask(
task,
projectBuildPath = ":project",
projectRoot = project.projectDir.path,
workspaceRoot = project.rootDir.path,
cwd = ".",
externalNodes = mutableMapOf(),
dependencies = mutableSetOf(),
targetNameOverrides = emptyMap())
assertEquals(true, result["cache"])
assertTrue((result["command"] as String).contains("gradlew"))
assertNotNull(result["metadata"])
assertNotNull(result["options"])
}
}

View File

@ -13,7 +13,7 @@ jobs:
_JAVA_OPTIONS: '-Xmx3g' _JAVA_OPTIONS: '-Xmx3g'
GRADLE_OPTS: '-Dorg.gradle.daemon=false -Dorg.gradle.workers.max=2' GRADLE_OPTS: '-Dorg.gradle.daemon=false -Dorg.gradle.workers.max=2'
docker: docker:
- image: cimg/openjdk:17.0-node - image: cimg/openjdk:21.0-node
steps: steps:
- checkout - checkout
@ -66,15 +66,15 @@ jobs:
# Uncomment this line to enable task distribution # Uncomment this line to enable task distribution
# - run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-jvm" --stop-agents-after="build" # - run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-jvm" --stop-agents-after="build"
- name: Set up JDK 17 for x64 - name: Set up JDK 21 for x64
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
java-version: '17' java-version: '21'
distribution: 'temurin' distribution: 'temurin'
architecture: x64 architecture: x64
- name: Setup Gradle - name: Setup Gradle
uses: gradle/gradle-build-action@v2 uses: gradle/gradle-build-action@v4
- uses: nrwl/nx-set-shas@v4 - uses: nrwl/nx-set-shas@v4
@ -96,7 +96,7 @@ jobs:
_JAVA_OPTIONS: '-Xmx3g' _JAVA_OPTIONS: '-Xmx3g'
GRADLE_OPTS: '-Dorg.gradle.daemon=false -Dorg.gradle.workers.max=2' GRADLE_OPTS: '-Dorg.gradle.daemon=false -Dorg.gradle.workers.max=2'
docker: docker:
- image: cimg/openjdk:17.0-node - image: cimg/openjdk:21.0-node
steps: steps:
- checkout - checkout
@ -149,15 +149,15 @@ jobs:
# Connect your workspace by running "nx connect" and uncomment this line to enable task distribution # Connect your workspace by running "nx connect" and uncomment this line to enable task distribution
# - run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-jvm" --stop-agents-after="build" # - run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-jvm" --stop-agents-after="build"
- name: Set up JDK 17 for x64 - name: Set up JDK 21 for x64
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
java-version: '17' java-version: '21'
distribution: 'temurin' distribution: 'temurin'
architecture: x64 architecture: x64
- name: Setup Gradle - name: Setup Gradle
uses: gradle/gradle-build-action@v2 uses: gradle/gradle-build-action@v4
- uses: nrwl/nx-set-shas@v4 - uses: nrwl/nx-set-shas@v4

View File

@ -10,7 +10,7 @@ jobs:
_JAVA_OPTIONS: "-Xmx3g" _JAVA_OPTIONS: "-Xmx3g"
GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.workers.max=2" GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.workers.max=2"
docker: docker:
- image: cimg/openjdk:17.0-node - image: cimg/openjdk:21.0-node
steps: steps:
- checkout - checkout

View File

@ -25,15 +25,15 @@ jobs:
<% if (connectedToCloud) { %># Uncomment this line to enable task distribution<% } else { %># Connect your workspace by running "nx connect" and uncomment this line to enable task distribution<% } %> <% if (connectedToCloud) { %># Uncomment this line to enable task distribution<% } else { %># Connect your workspace by running "nx connect" and uncomment this line to enable task distribution<% } %>
# - run: <%= packageManagerPrefix %> nx-cloud start-ci-run --distribute-on="3 linux-medium-jvm" --stop-agents-after="build" # - run: <%= packageManagerPrefix %> nx-cloud start-ci-run --distribute-on="3 linux-medium-jvm" --stop-agents-after="build"
- name: Set up JDK 17 for x64 - name: Set up JDK 21 for x64
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
java-version: '17' java-version: '21'
distribution: 'temurin' distribution: 'temurin'
architecture: x64 architecture: x64
- name: Setup Gradle - name: Setup Gradle
uses: gradle/gradle-build-action@v2 uses: gradle/gradle-build-action@v4
- uses: nrwl/nx-set-shas@v4 - uses: nrwl/nx-set-shas@v4

View File

@ -24,7 +24,6 @@ describe('@nx/gradle:init', () => {
"options": { "options": {
"buildTargetName": "build", "buildTargetName": "build",
"classesTargetName": "classes", "classesTargetName": "classes",
"includeSubprojectsTasks": false,
"testTargetName": "test", "testTargetName": "test",
}, },
"plugin": "@nx/gradle", "plugin": "@nx/gradle",
@ -49,7 +48,6 @@ describe('@nx/gradle:init', () => {
"options": { "options": {
"buildTargetName": "build", "buildTargetName": "build",
"classesTargetName": "classes", "classesTargetName": "classes",
"includeSubprojectsTasks": false,
"testTargetName": "test", "testTargetName": "test",
}, },
"plugin": "@nx/gradle", "plugin": "@nx/gradle",

View File

@ -9,7 +9,11 @@ import {
Tree, Tree,
updateNxJson, updateNxJson,
} from '@nx/devkit'; } from '@nx/devkit';
import { nxVersion } from '../../utils/versions'; import {
gradleProjectGraphPluginName,
gradleProjectGraphVersion,
nxVersion,
} from '../../utils/versions';
import { InitGeneratorSchema } from './schema'; import { InitGeneratorSchema } from './schema';
import { hasGradlePlugin } from '../../utils/has-gradle-plugin'; import { hasGradlePlugin } from '../../utils/has-gradle-plugin';
import { dirname, join, basename } from 'path'; import { dirname, join, basename } from 'path';
@ -52,7 +56,6 @@ function addPlugin(tree: Tree) {
testTargetName: 'test', testTargetName: 'test',
classesTargetName: 'classes', classesTargetName: 'classes',
buildTargetName: 'build', buildTargetName: 'build',
includeSubprojectsTasks: false,
}, },
}); });
updateNxJson(tree, nxJson); updateNxJson(tree, nxJson);
@ -67,16 +70,18 @@ export async function addBuildGradleFileNextToSettingsGradle(tree: Tree) {
'**/settings.gradle?(.kts)', '**/settings.gradle?(.kts)',
]); ]);
settingsGradleFiles.forEach((settingsGradleFile) => { settingsGradleFiles.forEach((settingsGradleFile) => {
addProjectReportToBuildGradle(settingsGradleFile, tree); addNxProjectGraphPluginToBuildGradle(settingsGradleFile, tree);
}); });
} }
/** /**
* - creates a build.gradle file next to the settings.gradle file if it does not exist. * - creates a build.gradle file next to the settings.gradle file if it does not exist.
* - adds the project-report plugin to the build.gradle file if it does not exist. * - adds the NxProjectGraphPlugin plugin to the build.gradle file if it does not exist.
* - adds a task to generate project reports for all subprojects and included builds.
*/ */
function addProjectReportToBuildGradle(settingsGradleFile: string, tree: Tree) { function addNxProjectGraphPluginToBuildGradle(
settingsGradleFile: string,
tree: Tree
) {
const filename = basename(settingsGradleFile); const filename = basename(settingsGradleFile);
let gradleFilePath = 'build.gradle'; let gradleFilePath = 'build.gradle';
if (filename.endsWith('.kts')) { if (filename.endsWith('.kts')) {
@ -90,53 +95,49 @@ function addProjectReportToBuildGradle(settingsGradleFile: string, tree: Tree) {
buildGradleContent = tree.read(gradleFilePath).toString(); buildGradleContent = tree.read(gradleFilePath).toString();
} }
if (buildGradleContent.includes('allprojects')) { const nxProjectGraphReportPlugin = filename.endsWith('.kts')
if (!buildGradleContent.includes('"project-report"')) { ? `id("${gradleProjectGraphPluginName}") version("${gradleProjectGraphVersion}")`
logger.warn(`Please add the project-report plugin to your ${gradleFilePath}: : `id "${gradleProjectGraphPluginName}" version "${gradleProjectGraphVersion}"`;
allprojects { if (buildGradleContent.includes('plugins {')) {
apply { if (!buildGradleContent.includes(gradleProjectGraphPluginName)) {
plugin("project-report") buildGradleContent = buildGradleContent.replace(
} 'plugins {',
}`); `plugins {
${nxProjectGraphReportPlugin}`
);
} }
} else { } else {
buildGradleContent += `\n\rallprojects { buildGradleContent = `plugins {
${nxProjectGraphReportPlugin}
}\n\r${buildGradleContent}`;
}
const applyNxProjectGraphReportPlugin = `plugin("${gradleProjectGraphPluginName}")`;
if (buildGradleContent.includes('allprojects {')) {
if (
!buildGradleContent.includes(
`plugin("${gradleProjectGraphPluginName}")`
) &&
!buildGradleContent.includes(`plugin('${gradleProjectGraphPluginName}')`)
) {
logger.warn(
`Please add the ${gradleProjectGraphPluginName} plugin to your ${gradleFilePath}:
allprojects {
apply { apply {
plugin("project-report") ${applyNxProjectGraphReportPlugin}
} }
}`; }`
);
}
} else {
buildGradleContent = `${buildGradleContent}\n\rallprojects {
apply {
${applyNxProjectGraphReportPlugin}
}
}`;
} }
if (!buildGradleContent.includes(`tasks.register("projectReportAll")`)) { tree.write(gradleFilePath, buildGradleContent);
if (gradleFilePath.endsWith('.kts')) {
buildGradleContent += `\n\rtasks.register("projectReportAll") {
// All project reports of subprojects
allprojects.forEach {
dependsOn(it.tasks.get("projectReport"))
}
// All projectReportAll of included builds
gradle.includedBuilds.forEach {
dependsOn(it.task(":projectReportAll"))
}
}`;
} else {
buildGradleContent += `\n\rtasks.register("projectReportAll") {
// All project reports of subprojects
allprojects.forEach {
dependsOn(it.tasks.getAt("projectReport"))
}
// All projectReportAll of included builds
gradle.includedBuilds.forEach {
dependsOn(it.task(":projectReportAll"))
}
}`;
}
}
if (buildGradleContent) {
tree.write(gradleFilePath, buildGradleContent);
}
} }
export function updateNxJsonConfiguration(tree: Tree) { export function updateNxJsonConfiguration(tree: Tree) {

View File

@ -1,5 +1,5 @@
import { Tree } from '@nx/devkit'; import { globAsync, logger, Tree } from '@nx/devkit';
import { addBuildGradleFileNextToSettingsGradle } from '../../generators/init/init'; import { basename, dirname, join } from 'node:path';
/** /**
* This migration adds task `projectReportAll` to build.gradle files * This migration adds task `projectReportAll` to build.gradle files
@ -7,3 +7,83 @@ import { addBuildGradleFileNextToSettingsGradle } from '../../generators/init/in
export default async function update(tree: Tree) { export default async function update(tree: Tree) {
await addBuildGradleFileNextToSettingsGradle(tree); await addBuildGradleFileNextToSettingsGradle(tree);
} }
/**
* This function creates and populate build.gradle file next to the settings.gradle file.
*/
export async function addBuildGradleFileNextToSettingsGradle(tree: Tree) {
const settingsGradleFiles = await globAsync(tree, [
'**/settings.gradle?(.kts)',
]);
settingsGradleFiles.forEach((settingsGradleFile) => {
addProjectReportToBuildGradle(settingsGradleFile, tree);
});
}
/**
* - creates a build.gradle file next to the settings.gradle file if it does not exist.
* - adds the project-report plugin to the build.gradle file if it does not exist.
* - adds a task to generate project reports for all subprojects and included builds.
*/
function addProjectReportToBuildGradle(settingsGradleFile: string, tree: Tree) {
const filename = basename(settingsGradleFile);
let gradleFilePath = 'build.gradle';
if (filename.endsWith('.kts')) {
gradleFilePath = 'build.gradle.kts';
}
gradleFilePath = join(dirname(settingsGradleFile), gradleFilePath);
let buildGradleContent = '';
if (!tree.exists(gradleFilePath)) {
tree.write(gradleFilePath, buildGradleContent); // create a build.gradle file near settings.gradle file if it does not exist
} else {
buildGradleContent = tree.read(gradleFilePath).toString();
}
if (buildGradleContent.includes('allprojects')) {
if (!buildGradleContent.includes('"project-report"')) {
logger.warn(`Please add the project-report plugin to your ${gradleFilePath}:
allprojects {
apply {
plugin("project-report")
}
}`);
}
} else {
buildGradleContent += `\n\rallprojects {
apply {
plugin("project-report")
}
}`;
}
if (!buildGradleContent.includes(`tasks.register("projectReportAll")`)) {
if (gradleFilePath.endsWith('.kts')) {
buildGradleContent += `\n\rtasks.register("projectReportAll") {
// All project reports of subprojects
allprojects.forEach {
dependsOn(it.tasks.get("projectReport"))
}
// All projectReportAll of included builds
gradle.includedBuilds.forEach {
dependsOn(it.task(":projectReportAll"))
}
}`;
} else {
buildGradleContent += `\n\rtasks.register("projectReportAll") {
// All project reports of subprojects
allprojects.forEach {
dependsOn(it.tasks.getAt("projectReport"))
}
// All projectReportAll of included builds
gradle.includedBuilds.forEach {
dependsOn(it.task(":projectReportAll"))
}
}`;
}
}
if (buildGradleContent) {
tree.write(gradleFilePath, buildGradleContent);
}
}

View File

@ -1,11 +1,9 @@
#### Add includeSubprojectsTasks to build.gradle File #### Add includeSubprojectsTasks to @nx/gradle Plugin Options
Add includeSubprojectsTasks to build.gradle file Add includeSubprojectsTasks to @nx/gradle plugin options in nx.json file
#### Sample Code Changes #### Sample Code Changes
Update import paths for `withModuleFederation` and `withModuleFederationForSSR`.
{% tabs %} {% tabs %}
{% tab label="Before" %} {% tab label="Before" %}

View File

@ -1,6 +1,6 @@
import { Tree, readNxJson, updateNxJson } from '@nx/devkit'; import { Tree, readNxJson, updateNxJson } from '@nx/devkit';
import { hasGradlePlugin } from '../../utils/has-gradle-plugin'; import { hasGradlePlugin } from '../../utils/has-gradle-plugin';
import { GradlePluginOptions } from '../../plugin/nodes'; import { GradlePluginOptions } from '../../plugin-v1/nodes';
// This function add options includeSubprojectsTasks as true in nx.json for gradle plugin // This function add options includeSubprojectsTasks as true in nx.json for gradle plugin
export default function update(tree: Tree) { export default function update(tree: Tree) {

View File

@ -0,0 +1,26 @@
#### Change @nx/gradle plugin to @nx/gradle/plugin-v1
Change @nx/gradle plugin to version 1 in nx.json
#### Sample Code Changes
{% tabs %}
{% tab label="Before" %}
```json {% fileName="nx.json" %}
{
"plugins": ["@nx/gradle"]
}
```
{% /tab %}
{% tab label="After" %}
```json {% highlightLines=[5] fileName="nx.json" %}
{
"plugins": ["@nx/gradle/plugin-v1"]
}
```
{% /tab %}
{% /tabs %}

View File

@ -0,0 +1,57 @@
import { Tree, readNxJson } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import update from './change-plugin-to-v1';
describe('ChangePluginToV1', () => {
let tree: Tree;
beforeAll(() => {
tree = createTreeWithEmptyWorkspace();
});
it('should not add @nx/gradle plugin if it does not exist', async () => {
tree.write('nx.json', JSON.stringify({ namedInputs: {} }));
update(tree);
expect(readNxJson(tree)).toMatchInlineSnapshot(`
{
"namedInputs": {},
}
`);
});
it('should change @nx/gradle to @nx/gradle/plugin-v1 plugin', async () => {
tree.write('nx.json', JSON.stringify({ plugins: ['@nx/gradle'] }));
update(tree);
expect(readNxJson(tree)).toMatchInlineSnapshot(`
{
"plugins": [
"@nx/gradle/plugin-v1",
],
}
`);
});
it('should add change to @nx/gradle plugin with options', async () => {
tree.write(
'nx.json',
JSON.stringify({
plugins: [
{ plugin: '@nx/gradle', options: { testTargetName: 'test' } },
],
})
);
update(tree);
expect(readNxJson(tree)).toMatchInlineSnapshot(`
{
"plugins": [
{
"options": {
"testTargetName": "test",
},
"plugin": "@nx/gradle/plugin-v1",
},
],
}
`);
});
});

View File

@ -0,0 +1,25 @@
import { Tree, readNxJson, updateNxJson } from '@nx/devkit';
import { hasGradlePlugin } from '../../utils/has-gradle-plugin';
/* This function changes the plugin to v1
* Replace @nx/gradle with @nx/gradle/plugin-v1
*/
export default function update(tree: Tree) {
const nxJson = readNxJson(tree);
if (!nxJson) {
return;
}
if (!hasGradlePlugin(tree)) {
return;
}
let gradlePluginIndex = nxJson.plugins.findIndex((p) =>
typeof p === 'string' ? p === '@nx/gradle' : p.plugin === '@nx/gradle'
);
let gradlePlugin = nxJson.plugins[gradlePluginIndex];
if (typeof gradlePlugin === 'string') {
nxJson.plugins[gradlePluginIndex] = '@nx/gradle/plugin-v1';
} else {
gradlePlugin.plugin = '@nx/gradle/plugin-v1';
}
updateNxJson(tree, nxJson);
}

View File

@ -0,0 +1,96 @@
import {
CreateDependencies,
CreateDependenciesContext,
DependencyType,
FileMap,
RawProjectGraphDependency,
validateDependency,
} from '@nx/devkit';
import { basename, dirname } from 'node:path';
import { getCurrentGradleReport } from './utils/get-gradle-report';
import { GRADLE_BUILD_FILES } from '../utils/split-config-files';
export const createDependencies: CreateDependencies = async (
_,
context: CreateDependenciesContext
) => {
const gradleFiles: string[] = findGradleFiles(context.filesToProcess);
if (gradleFiles.length === 0) {
return [];
}
const gradleDependenciesStart = performance.mark('gradleDependencies:start');
const {
gradleFileToGradleProjectMap,
gradleProjectNameToProjectRootMap,
gradleProjectToDepsMap,
gradleProjectToChildProjects,
} = getCurrentGradleReport();
const dependencies: Set<RawProjectGraphDependency> = new Set();
for (const gradleFile of gradleFiles) {
const gradleProject = gradleFileToGradleProjectMap.get(gradleFile);
const projectName = Object.values(context.projects).find(
(project) => project.root === dirname(gradleFile)
)?.name;
const dependedProjects: Set<string> =
gradleProjectToDepsMap.get(gradleProject);
if (projectName && dependedProjects?.size) {
dependedProjects?.forEach((dependedProject) => {
const targetProjectRoot = gradleProjectNameToProjectRootMap.get(
dependedProject
) as string;
const targetProjectName = Object.values(context.projects).find(
(project) => project.root === targetProjectRoot
)?.name;
if (targetProjectName) {
const dependency: RawProjectGraphDependency = {
source: projectName as string,
target: targetProjectName as string,
type: DependencyType.static,
sourceFile: gradleFile,
};
validateDependency(dependency, context);
dependencies.add(dependency);
}
});
}
gradleProjectToChildProjects.get(gradleProject)?.forEach((childProject) => {
if (childProject) {
const dependency: RawProjectGraphDependency = {
source: projectName as string,
target: childProject,
type: DependencyType.static,
sourceFile: gradleFile,
};
validateDependency(dependency, context);
dependencies.add(dependency);
}
});
}
const gradleDependenciesEnd = performance.mark('gradleDependencies:end');
performance.measure(
'gradleDependencies',
gradleDependenciesStart.name,
gradleDependenciesEnd.name
);
return Array.from(dependencies);
};
function findGradleFiles(fileMap: FileMap): string[] {
const gradleFiles: string[] = [];
for (const [_, files] of Object.entries(fileMap.projectFileMap)) {
for (const file of files) {
if (GRADLE_BUILD_FILES.has(basename(file.file))) {
gradleFiles.push(file.file);
}
}
}
return gradleFiles;
}

View File

@ -0,0 +1,587 @@
import { CreateNodesContext } from '@nx/devkit';
import { TempFs } from 'nx/src/internal-testing-utils/temp-fs';
import { type GradleReport } from './utils/get-gradle-report';
let gradleReport: GradleReport;
jest.mock('./utils/get-gradle-report', () => {
return {
GRADLE_BUILD_FILES: new Set(['build.gradle', 'build.gradle.kts']),
populateGradleReport: jest.fn().mockImplementation(() => void 0),
getCurrentGradleReport: jest.fn().mockImplementation(() => gradleReport),
};
});
import { createNodesV2 } from './nodes';
describe('@nx/gradle/plugin-v1/nodes', () => {
let createNodesFunction = createNodesV2[1];
let context: CreateNodesContext;
let tempFs: TempFs;
let cwd: string;
beforeEach(async () => {
tempFs = new TempFs('test');
gradleReport = {
gradleFileToGradleProjectMap: new Map<string, string>([
['proj/build.gradle', 'proj'],
]),
gradleProjectToDepsMap: new Map<string, Set<string>>(),
gradleFileToOutputDirsMap: new Map<string, Map<string, string>>([
['proj/build.gradle', new Map([['build', 'build']])],
]),
gradleProjectToTasksMap: new Map<string, Set<string>>([
['proj', new Set(['test'])],
]),
gradleProjectToTasksTypeMap: new Map<string, Map<string, string>>([
[
'proj',
new Map([
['test', 'Verification'],
['build', 'Build'],
]),
],
]),
gradleProjectToProjectName: new Map<string, string>([['proj', 'proj']]),
gradleProjectNameToProjectRootMap: new Map<string, string>([
['proj', 'proj'],
]),
gradleProjectToChildProjects: new Map<string, string[]>(),
};
cwd = process.cwd();
process.chdir(tempFs.tempDir);
context = {
nxJsonConfiguration: {
namedInputs: {
default: ['{projectRoot}/**/*'],
production: ['!{projectRoot}/**/*.spec.ts'],
},
},
workspaceRoot: tempFs.tempDir,
configFiles: [],
};
await tempFs.createFiles({
'proj/build.gradle': ``,
gradlew: '',
});
});
afterEach(() => {
jest.resetModules();
process.chdir(cwd);
});
it('should create nodes based on gradle', async () => {
const results = await createNodesFunction(
['proj/build.gradle'],
{
buildTargetName: 'build',
},
context
);
expect(results).toMatchInlineSnapshot(`
[
[
"proj/build.gradle",
{
"projects": {
"proj": {
"metadata": {
"targetGroups": {
"Verification": [
"test",
],
},
"technologies": [
"gradle",
],
},
"name": "proj",
"projectType": "application",
"targets": {
"test": {
"cache": true,
"command": "./gradlew proj:test",
"dependsOn": [
"testClasses",
],
"inputs": [
"default",
"^production",
],
"metadata": {
"help": {
"command": "./gradlew help --task proj:test",
"example": {
"options": {
"args": [
"--rerun",
],
},
},
},
"technologies": [
"gradle",
],
},
"options": {
"cwd": ".",
},
},
},
},
},
},
],
]
`);
});
it('should create nodes include subprojects tasks', async () => {
const results = await createNodesFunction(
['proj/build.gradle'],
{
buildTargetName: 'build',
includeSubprojectsTasks: true,
},
context
);
expect(results).toMatchInlineSnapshot(`
[
[
"proj/build.gradle",
{
"projects": {
"proj": {
"metadata": {
"targetGroups": {
"Build": [
"build",
],
"Verification": [
"test",
],
},
"technologies": [
"gradle",
],
},
"name": "proj",
"projectType": "application",
"targets": {
"build": {
"cache": true,
"command": "./gradlew proj:build",
"dependsOn": [
"^build",
"classes",
"test",
],
"inputs": [
"production",
"^production",
],
"metadata": {
"help": {
"command": "./gradlew help --task proj:build",
"example": {
"options": {
"args": [
"--rerun",
],
},
},
},
"technologies": [
"gradle",
],
},
"options": {
"cwd": ".",
},
"outputs": [
"build",
],
},
"test": {
"cache": true,
"command": "./gradlew proj:test",
"dependsOn": [
"testClasses",
],
"inputs": [
"default",
"^production",
],
"metadata": {
"help": {
"command": "./gradlew help --task proj:test",
"example": {
"options": {
"args": [
"--rerun",
],
},
},
},
"technologies": [
"gradle",
],
},
"options": {
"cwd": ".",
},
},
},
},
},
},
],
]
`);
});
it('should create nodes based on gradle for nested project root', async () => {
gradleReport = {
gradleFileToGradleProjectMap: new Map<string, string>([
['nested/nested/proj/build.gradle', 'proj'],
]),
gradleProjectToDepsMap: new Map<string, Set<string>>(),
gradleFileToOutputDirsMap: new Map<string, Map<string, string>>([
['nested/nested/proj/build.gradle', new Map([['build', 'build']])],
]),
gradleProjectToTasksMap: new Map<string, Set<string>>([
['proj', new Set(['test'])],
]),
gradleProjectToTasksTypeMap: new Map<string, Map<string, string>>([
['proj', new Map([['test', 'Verification']])],
]),
gradleProjectToProjectName: new Map<string, string>([['proj', 'proj']]),
gradleProjectNameToProjectRootMap: new Map<string, string>([
['proj', 'proj'],
]),
gradleProjectToChildProjects: new Map<string, string[]>(),
};
await tempFs.createFiles({
'nested/nested/proj/build.gradle': ``,
});
const results = await createNodesFunction(
['nested/nested/proj/build.gradle'],
{
buildTargetName: 'build',
},
context
);
expect(results).toMatchInlineSnapshot(`
[
[
"nested/nested/proj/build.gradle",
{
"projects": {
"nested/nested/proj": {
"metadata": {
"targetGroups": {
"Verification": [
"test",
],
},
"technologies": [
"gradle",
],
},
"name": "proj",
"projectType": "application",
"targets": {
"test": {
"cache": true,
"command": "./gradlew proj:test",
"dependsOn": [
"testClasses",
],
"inputs": [
"default",
"^production",
],
"metadata": {
"help": {
"command": "./gradlew help --task proj:test",
"example": {
"options": {
"args": [
"--rerun",
],
},
},
},
"technologies": [
"gradle",
],
},
"options": {
"cwd": ".",
},
},
},
},
},
},
],
]
`);
});
describe('with atomized tests targets', () => {
beforeEach(async () => {
gradleReport = {
gradleFileToGradleProjectMap: new Map<string, string>([
['nested/nested/proj/build.gradle', 'proj'],
]),
gradleProjectToDepsMap: new Map<string, Set<string>>(),
gradleFileToOutputDirsMap: new Map<string, Map<string, string>>([
['nested/nested/proj/build.gradle', new Map([['build', 'build']])],
]),
gradleProjectToTasksMap: new Map<string, Set<string>>([
['proj', new Set(['test'])],
]),
gradleProjectToTasksTypeMap: new Map<string, Map<string, string>>([
['proj', new Map([['test', 'Test']])],
]),
gradleProjectToProjectName: new Map<string, string>([['proj', 'proj']]),
gradleProjectNameToProjectRootMap: new Map<string, string>([
['proj', 'proj'],
]),
gradleProjectToChildProjects: new Map<string, string[]>(),
};
await tempFs.createFiles({
'nested/nested/proj/build.gradle': ``,
});
await tempFs.createFiles({
'proj/src/test/java/test/rootTest.java': ``,
});
await tempFs.createFiles({
'nested/nested/proj/src/test/java/test/aTest.java': ``,
});
await tempFs.createFiles({
'nested/nested/proj/src/test/java/test/bTest.java': ``,
});
await tempFs.createFiles({
'nested/nested/proj/src/test/java/test/cTests.java': ``,
});
});
it('should create nodes with atomized tests targets based on gradle for nested project root', async () => {
const results = await createNodesFunction(
[
'nested/nested/proj/build.gradle',
'proj/src/test/java/test/rootTest.java',
'nested/nested/proj/src/test/java/test/aTest.java',
'nested/nested/proj/src/test/java/test/bTest.java',
'nested/nested/proj/src/test/java/test/cTests.java',
],
{
buildTargetName: 'build',
ciTargetName: 'test-ci',
},
context
);
expect(results).toMatchInlineSnapshot(`
[
[
"nested/nested/proj/build.gradle",
{
"projects": {
"nested/nested/proj": {
"metadata": {
"targetGroups": {
"Test": [
"test-ci--aTest",
"test-ci--bTest",
"test-ci--cTests",
"test-ci",
"test",
],
},
"technologies": [
"gradle",
],
},
"name": "proj",
"projectType": "application",
"targets": {
"test": {
"cache": false,
"command": "./gradlew proj:test",
"dependsOn": [
"testClasses",
],
"inputs": [
"default",
"^production",
],
"metadata": {
"help": {
"command": "./gradlew help --task proj:test",
"example": {
"options": {
"args": [
"--rerun",
],
},
},
},
"technologies": [
"gradle",
],
},
"options": {
"cwd": ".",
},
},
"test-ci": {
"cache": true,
"dependsOn": [
{
"params": "forward",
"projects": "self",
"target": "test-ci--aTest",
},
{
"params": "forward",
"projects": "self",
"target": "test-ci--bTest",
},
{
"params": "forward",
"projects": "self",
"target": "test-ci--cTests",
},
],
"executor": "nx:noop",
"inputs": [
"default",
"^production",
],
"metadata": {
"description": "Runs Gradle Tests in CI",
"help": {
"command": "./gradlew help --task proj:test",
"example": {
"options": {
"args": [
"--rerun",
],
},
},
},
"nonAtomizedTarget": "test",
"technologies": [
"gradle",
],
},
},
"test-ci--aTest": {
"cache": true,
"command": "./gradlew proj:test --tests aTest",
"dependsOn": [
"testClasses",
],
"inputs": [
"default",
"^production",
],
"metadata": {
"description": "Runs Gradle test nested/nested/proj/src/test/java/test/aTest.java in CI",
"help": {
"command": "./gradlew help --task proj:test",
"example": {
"options": {
"args": [
"--rerun",
],
},
},
},
"technologies": [
"gradle",
],
},
"options": {
"cwd": ".",
},
},
"test-ci--bTest": {
"cache": true,
"command": "./gradlew proj:test --tests bTest",
"dependsOn": [
"testClasses",
],
"inputs": [
"default",
"^production",
],
"metadata": {
"description": "Runs Gradle test nested/nested/proj/src/test/java/test/bTest.java in CI",
"help": {
"command": "./gradlew help --task proj:test",
"example": {
"options": {
"args": [
"--rerun",
],
},
},
},
"technologies": [
"gradle",
],
},
"options": {
"cwd": ".",
},
},
"test-ci--cTests": {
"cache": true,
"command": "./gradlew proj:test --tests cTests",
"dependsOn": [
"testClasses",
],
"inputs": [
"default",
"^production",
],
"metadata": {
"description": "Runs Gradle test nested/nested/proj/src/test/java/test/cTests.java in CI",
"help": {
"command": "./gradlew help --task proj:test",
"example": {
"options": {
"args": [
"--rerun",
],
},
},
},
"technologies": [
"gradle",
],
},
"options": {
"cwd": ".",
},
},
},
},
},
},
],
]
`);
});
});
});

View File

@ -0,0 +1,435 @@
import {
CreateNodes,
CreateNodesV2,
CreateNodesContext,
ProjectConfiguration,
TargetConfiguration,
createNodesFromFiles,
readJsonFile,
writeJsonFile,
CreateNodesFunction,
logger,
} from '@nx/devkit';
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
import { existsSync } from 'node:fs';
import { basename, dirname, join } from 'node:path';
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
import { findProjectForPath } from 'nx/src/devkit-internals';
import {
populateGradleReport,
getCurrentGradleReport,
GradleReport,
} from './utils/get-gradle-report';
import { hashObject } from 'nx/src/hasher/file-hasher';
import {
gradleConfigAndTestGlob,
gradleConfigGlob,
splitConfigFiles,
} from '../utils/split-config-files';
import { getGradleExecFile, findGradlewFile } from '../utils/exec-gradle';
const cacheableTaskType = new Set(['Build', 'Verification']);
const dependsOnMap = {
build: ['^build', 'classes', 'test'],
testClasses: ['classes'],
test: ['testClasses'],
classes: ['^classes'],
};
interface GradleTask {
type: string;
name: string;
}
export interface GradlePluginOptions {
includeSubprojectsTasks?: boolean; // default is false, show all gradle tasks in the project
ciTargetName?: string;
testTargetName?: string;
classesTargetName?: string;
buildTargetName?: string;
[taskTargetName: string]: string | undefined | boolean;
}
function normalizeOptions(options: GradlePluginOptions): GradlePluginOptions {
options ??= {};
options.testTargetName ??= 'test';
options.classesTargetName ??= 'classes';
options.buildTargetName ??= 'build';
return options;
}
type GradleTargets = Record<string, Partial<ProjectConfiguration>>;
function readTargetsCache(cachePath: string): GradleTargets {
return existsSync(cachePath) ? readJsonFile(cachePath) : {};
}
export function writeTargetsToCache(cachePath: string, results: GradleTargets) {
writeJsonFile(cachePath, results);
}
export const createNodesV2: CreateNodesV2<GradlePluginOptions> = [
gradleConfigAndTestGlob,
async (files, options, context) => {
const { buildFiles, projectRoots, gradlewFiles, testFiles } =
splitConfigFiles(files);
const optionsHash = hashObject(options);
const cachePath = join(
workspaceDataDirectory,
`gradle-${optionsHash}.hash`
);
const targetsCache = readTargetsCache(cachePath);
await populateGradleReport(
context.workspaceRoot,
gradlewFiles.map((f) => join(context.workspaceRoot, f))
);
const gradleReport = getCurrentGradleReport();
const gradleProjectRootToTestFilesMap = getGradleProjectRootToTestFilesMap(
testFiles,
projectRoots
);
try {
return createNodesFromFiles(
makeCreateNodesForGradleConfigFile(
gradleReport,
targetsCache,
gradleProjectRootToTestFilesMap
),
buildFiles,
options,
context
);
} finally {
writeTargetsToCache(cachePath, targetsCache);
}
},
];
export const makeCreateNodesForGradleConfigFile =
(
gradleReport: GradleReport,
targetsCache: GradleTargets = {},
gradleProjectRootToTestFilesMap: Record<string, string[]> = {}
): CreateNodesFunction =>
async (
gradleFilePath,
options: GradlePluginOptions | undefined,
context: CreateNodesContext
) => {
const projectRoot = dirname(gradleFilePath);
options = normalizeOptions(options);
const hash = await calculateHashForCreateNodes(
projectRoot,
options ?? {},
context
);
targetsCache[hash] ??= await createGradleProject(
gradleReport,
gradleFilePath,
options,
context,
gradleProjectRootToTestFilesMap[projectRoot]
);
const project = targetsCache[hash];
if (!project) {
return {};
}
return {
projects: {
[projectRoot]: project,
},
};
};
/**
@deprecated This is replaced with {@link createNodesV2}. Update your plugin to export its own `createNodesV2` function that wraps this one instead.
This function will change to the v2 function in Nx 20.
*/
export const createNodes: CreateNodes<GradlePluginOptions> = [
gradleConfigGlob,
async (buildFile, options, context) => {
logger.warn(
'`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.'
);
const { gradlewFiles } = splitConfigFiles(context.configFiles);
await populateGradleReport(context.workspaceRoot, gradlewFiles);
const gradleReport = getCurrentGradleReport();
const internalCreateNodes =
makeCreateNodesForGradleConfigFile(gradleReport);
return await internalCreateNodes(buildFile, options, context);
},
];
async function createGradleProject(
gradleReport: GradleReport,
gradleFilePath: string,
options: GradlePluginOptions | undefined,
context: CreateNodesContext,
testFiles = []
) {
try {
const {
gradleProjectToTasksTypeMap,
gradleProjectToTasksMap,
gradleFileToOutputDirsMap,
gradleFileToGradleProjectMap,
gradleProjectToProjectName,
} = gradleReport;
const gradleProject = gradleFileToGradleProjectMap.get(
gradleFilePath
) as string;
const projectName = gradleProjectToProjectName.get(gradleProject);
if (!projectName) {
return;
}
const tasksTypeMap: Map<string, string> = gradleProjectToTasksTypeMap.get(
gradleProject
) as Map<string, string>;
const tasksSet = gradleProjectToTasksMap.get(gradleProject) as Set<string>;
let tasks: GradleTask[] = [];
tasksSet.forEach((taskName) => {
tasks.push({
type: tasksTypeMap?.get(taskName) as string,
name: taskName,
});
});
if (options.includeSubprojectsTasks) {
tasksTypeMap.forEach((taskType, taskName) => {
if (!tasksSet.has(taskName)) {
tasks.push({
type: taskType,
name: taskName,
});
}
});
}
const outputDirs = gradleFileToOutputDirsMap.get(gradleFilePath) as Map<
string,
string
>;
const { targets, targetGroups } = await createGradleTargets(
tasks,
options,
context,
outputDirs,
gradleProject,
gradleFilePath,
testFiles
);
const project: Partial<ProjectConfiguration> = {
name: projectName,
projectType: 'application',
targets,
metadata: {
targetGroups,
technologies: ['gradle'],
},
};
return project;
} catch (e) {
console.error(e);
return undefined;
}
}
async function createGradleTargets(
tasks: GradleTask[],
options: GradlePluginOptions | undefined,
context: CreateNodesContext,
outputDirs: Map<string, string>,
gradleProject: string,
gradleBuildFilePath: string,
testFiles: string[] = []
): Promise<{
targetGroups: Record<string, string[]>;
targets: Record<string, TargetConfiguration>;
}> {
const inputsMap = createInputsMap(context);
const gradlewFileDirectory = dirname(
findGradlewFile(gradleBuildFilePath, context.workspaceRoot)
);
const targets: Record<string, TargetConfiguration> = {};
const targetGroups: Record<string, string[]> = {};
for (const task of tasks) {
const targetName = options?.[`${task.name}TargetName`] ?? task.name;
let outputs = [outputDirs.get(task.name)].filter(Boolean);
if (task.name === 'test') {
outputs = [
outputDirs.get('testReport'),
outputDirs.get('testResults'),
].filter(Boolean);
getTestCiTargets(
testFiles,
gradleProject,
targetName as string,
options.ciTargetName,
inputsMap['test'],
outputs,
task.type,
targets,
targetGroups,
gradlewFileDirectory
);
}
const taskCommandToRun = `${gradleProject ? gradleProject + ':' : ''}${
task.name
}`;
targets[targetName as string] = {
command: `${getGradleExecFile()} ${taskCommandToRun}`,
options: {
cwd: gradlewFileDirectory,
},
cache: cacheableTaskType.has(task.type),
inputs: inputsMap[task.name],
dependsOn: dependsOnMap[task.name],
metadata: {
technologies: ['gradle'],
help: {
command: `${getGradleExecFile()} help --task ${taskCommandToRun}`,
example: {
options: {
args: ['--rerun'],
},
},
},
},
...(outputs && outputs.length ? { outputs } : {}),
};
if (task.type) {
if (!targetGroups[task.type]) {
targetGroups[task.type] = [];
}
targetGroups[task.type].push(targetName as string);
}
}
return { targetGroups, targets };
}
function createInputsMap(
context: CreateNodesContext
): Record<string, TargetConfiguration['inputs']> {
const namedInputs = context.nxJsonConfiguration.namedInputs;
return {
build: namedInputs?.production
? ['production', '^production']
: ['default', '^default'],
test: ['default', namedInputs?.production ? '^production' : '^default'],
classes: namedInputs?.production
? ['production', '^production']
: ['default', '^default'],
};
}
function getTestCiTargets(
testFiles: string[],
gradleProject: string,
testTargetName: string,
ciTargetName: string,
inputs: TargetConfiguration['inputs'],
outputs: string[],
targetGroupName: string,
targets: Record<string, TargetConfiguration>,
targetGroups: Record<string, string[]>,
gradlewFileDirectory: string
): void {
if (!testFiles || testFiles.length === 0 || !ciTargetName) {
return;
}
const taskCommandToRun = `${gradleProject ? gradleProject + ':' : ''}test`;
if (!targetGroups[targetGroupName]) {
targetGroups[targetGroupName] = [];
}
const dependsOn: TargetConfiguration['dependsOn'] = [];
testFiles.forEach((testFile) => {
const testName = basename(testFile).split('.')[0];
const targetName = ciTargetName + '--' + testName;
targets[targetName] = {
command: `${getGradleExecFile()} ${taskCommandToRun} --tests ${testName}`,
options: {
cwd: gradlewFileDirectory,
},
cache: true,
inputs,
dependsOn: dependsOnMap['test'],
metadata: {
technologies: ['gradle'],
description: `Runs Gradle test ${testFile} in CI`,
help: {
command: `${getGradleExecFile()} help --task ${taskCommandToRun}`,
example: {
options: {
args: ['--rerun'],
},
},
},
},
...(outputs && outputs.length > 0 ? { outputs } : {}),
};
targetGroups[targetGroupName].push(targetName);
dependsOn.push({
target: targetName,
projects: 'self',
params: 'forward',
});
});
targets[ciTargetName] = {
executor: 'nx:noop',
cache: true,
inputs,
dependsOn: dependsOn,
...(outputs && outputs.length > 0 ? { outputs } : {}),
metadata: {
technologies: ['gradle'],
description: 'Runs Gradle Tests in CI',
nonAtomizedTarget: testTargetName,
help: {
command: `${getGradleExecFile()} help --task ${taskCommandToRun}`,
example: {
options: {
args: ['--rerun'],
},
},
},
},
};
targetGroups[targetGroupName].push(ciTargetName);
}
function getGradleProjectRootToTestFilesMap(
testFiles: string[],
projectRoots: string[]
): Record<string, string[]> | undefined {
if (testFiles.length === 0 || projectRoots.length === 0) {
return;
}
const roots = new Map(projectRoots.map((root) => [root, root]));
const testFilesToGradleProjectMap: Record<string, string[]> = {};
testFiles.forEach((testFile) => {
const projectRoot = findProjectForPath(testFile, roots);
if (projectRoot) {
if (!testFilesToGradleProjectMap[projectRoot]) {
testFilesToGradleProjectMap[projectRoot] = [];
}
testFilesToGradleProjectMap[projectRoot].push(testFile);
}
});
return testFilesToGradleProjectMap;
}

View File

@ -1,8 +1,5 @@
> Task :dependencyReport > Task :dependencyReport
See the report at: file:///tmp/build/reports/project/dependencies.txt See the report at: file://__dirname__/__mocks__/gradle-dependencies.txt
> Task :htmlDependencyReport
See the report at: file:///tmp/build/reports/project/dependencies/index.html
> Task :propertyReport > Task :propertyReport
See the report at: file:///tmp/build/reports/project/properties.txt See the report at: file:///tmp/build/reports/project/properties.txt
@ -11,10 +8,7 @@ See the report at: file:///tmp/build/reports/project/properties.txt
See the report at: file:///tmp/build/reports/project/tasks.txt See the report at: file:///tmp/build/reports/project/tasks.txt
> Task :app:dependencyReport > Task :app:dependencyReport
See the report at: file:///tmp/app/build/reports/project/dependencies.txt See the report at: file://__dirname__/__mocks__/gradle-dependencies.txt
> Task :app:htmlDependencyReport
See the report at: file:///tmp/app/build/reports/project/dependencies/index.html
> Task :app:propertyReport > Task :app:propertyReport
See the report at: file:///tmp/app/build/reports/project/properties.txt See the report at: file:///tmp/app/build/reports/project/properties.txt
@ -25,10 +19,7 @@ NAMED TASK1: This is executed during the configuration phase
See the report at: file:///tmp/app/build/reports/project/tasks.txt See the report at: file:///tmp/app/build/reports/project/tasks.txt
> Task :list:dependencyReport > Task :list:dependencyReport
See the report at: file:///tmp/list/build/reports/project/dependencies.txt See the report at: file://__dirname__/__mocks__/gradle-dependencies.txt
> Task :list:htmlDependencyReport
See the report at: file:///tmp/list/build/reports/project/dependencies/index.html
> Task :list:propertyReport > Task :list:propertyReport
See the report at: file:///tmp/list/build/reports/project/properties.txt See the report at: file:///tmp/list/build/reports/project/properties.txt
@ -37,10 +28,7 @@ See the report at: file:///tmp/list/build/reports/project/properties.txt
See the report at: file:///tmp/list/build/reports/project/tasks.txt See the report at: file:///tmp/list/build/reports/project/tasks.txt
> Task :utilities:dependencyReport > Task :utilities:dependencyReport
See the report at: file:///tmp/utilities/build/reports/project/dependencies.txt See the report at: file://__dirname__/__mocks__/gradle-dependencies.txt
> Task :utilities:htmlDependencyReport
See the report at: file:///tmp/utilities/build/reports/project/dependencies/index.html
> Task :utilities:propertyReport > Task :utilities:propertyReport
See the report at: file:///tmp/utilities/build/reports/project/properties.txt See the report at: file:///tmp/utilities/build/reports/project/properties.txt

View File

@ -1,5 +1,5 @@
> Task :dependencyReport > Task :dependencyReport
See the report at: file:///tmp/build/reports/project/dependencies.txt See the report at: file://__dirname__/__mocks__/gradle-dependencies.txt
> Task :htmlDependencyReport > Task :htmlDependencyReport
See the report at: file:///tmp/build/reports/project/dependencies/index.html See the report at: file:///tmp/build/reports/project/dependencies/index.html
@ -11,10 +11,7 @@ See the report at: file:///tmp/build/reports/project/properties.txt
See the report at: file:///tmp/build/reports/project/tasks.txt See the report at: file:///tmp/build/reports/project/tasks.txt
> Task :app:dependencyReport > Task :app:dependencyReport
See the report at: file:///tmp/app/build/reports/project/dependencies.txt See the report at: file://__dirname__/__mocks__/gradle-dependencies.txt
> Task :app:htmlDependencyReport
See the report at: file:///tmp/app/build/reports/project/dependencies/index.html
> Task :app:propertyReport > Task :app:propertyReport
See the report at: file:///tmp/app/build/reports/project/properties.txt See the report at: file:///tmp/app/build/reports/project/properties.txt
@ -23,7 +20,7 @@ See the report at: file:///tmp/app/build/reports/project/properties.txt
See the report at: file:///tmp/app/build/reports/project/tasks.txt See the report at: file:///tmp/app/build/reports/project/tasks.txt
> Task :list:dependencyReport > Task :list:dependencyReport
See the report at: file:///tmp/list/build/reports/project/dependencies.txt See the report at: file://__dirname__/__mocks__/gradle-dependencies.txt
> Task :list:htmlDependencyReport > Task :list:htmlDependencyReport
See the report at: file:///tmp/list/build/reports/project/dependencies/index.html See the report at: file:///tmp/list/build/reports/project/dependencies/index.html
@ -35,10 +32,7 @@ See the report at: file:///tmp/list/build/reports/project/properties.txt
See the report at: file:///tmp/list/build/reports/project/tasks.txt See the report at: file:///tmp/list/build/reports/project/tasks.txt
> Task :utilities:dependencyReport > Task :utilities:dependencyReport
See the report at: file:///tmp/utilities/build/reports/project/dependencies.txt See the report at: file://__dirname__/__mocks__/gradle-dependencies.txt
> Task :utilities:htmlDependencyReport
See the report at: file:///tmp/utilities/build/reports/project/dependencies/index.html
> Task :utilities:propertyReport > Task :utilities:propertyReport
See the report at: file:///tmp/utilities/build/reports/project/properties.txt See the report at: file:///tmp/utilities/build/reports/project/properties.txt

View File

@ -1,31 +1,103 @@
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { fileSync } from 'tmp';
import { join } from 'path'; import { join } from 'path';
import { import {
processGradleDependencies, processGradleDependencies,
processProjectReports, processProjectReports,
writeGradleReportToCache,
} from './get-gradle-report'; } from './get-gradle-report';
describe('processProjectReports', () => { describe('processProjectReports', () => {
const tmpFile = fileSync();
it('should process project reports', () => { it('should process project reports', () => {
const projectReportLines = readFileSync( const projectReportLines = readFileSync(
join(__dirname, '__mocks__/gradle-project-report.txt'), join(__dirname, '__mocks__/gradle-project-report.txt'),
'utf-8' 'utf-8'
).split('\n'); )
.replaceAll('__dirname__', __dirname)
.split('\n');
const report = processProjectReports(projectReportLines); const report = processProjectReports(projectReportLines);
expect( expect(
Object.keys(Object.fromEntries(report.gradleProjectToTasksTypeMap)) Object.keys(Object.fromEntries(report.gradleProjectToTasksTypeMap))
).toEqual(['', ':app', ':list', ':utilities']); ).toEqual(['', ':app', ':list', ':utilities']);
writeGradleReportToCache(tmpFile.name, report);
expect(readFileSync(tmpFile.name).toString()).toMatchInlineSnapshot(`
"{
"gradleFileToGradleProjectMap": {},
"gradleProjectToDepsMap": {
"": [
":utilities"
],
":app": [
":utilities"
],
":list": [
":utilities"
],
":utilities": [
":utilities"
]
},
"gradleFileToOutputDirsMap": {},
"gradleProjectToTasksTypeMap": {
"": {},
":app": {},
":list": {},
":utilities": {}
},
"gradleProjectToTasksMap": {},
"gradleProjectToProjectName": {},
"gradleProjectNameToProjectRootMap": {},
"gradleProjectToChildProjects": {}
}"
`);
}); });
it('should process project reports with println', () => { it('should process project reports with println', () => {
const projectReportLines = readFileSync( const projectReportLines = readFileSync(
join(__dirname, '__mocks__/gradle-project-report-println.txt'), join(__dirname, '__mocks__/gradle-project-report-println.txt'),
'utf-8' 'utf-8'
).split('\n'); )
.replaceAll('__dirname__', __dirname)
.split('\n');
const report = processProjectReports(projectReportLines); const report = processProjectReports(projectReportLines);
expect( expect(
Object.keys(Object.fromEntries(report.gradleProjectToTasksTypeMap)) Object.keys(Object.fromEntries(report.gradleProjectToTasksTypeMap))
).toEqual(['', ':app', ':list', ':utilities']); ).toEqual(['', ':app', ':list', ':utilities']);
writeGradleReportToCache(tmpFile.name, report);
expect(readFileSync(tmpFile.name).toString()).toMatchInlineSnapshot(`
"{
"gradleFileToGradleProjectMap": {},
"gradleProjectToDepsMap": {
"": [
":utilities"
],
":app": [
":utilities"
],
":list": [
":utilities"
],
":utilities": [
":utilities"
]
},
"gradleFileToOutputDirsMap": {},
"gradleProjectToTasksTypeMap": {
"": {},
":app": {},
":list": {},
":utilities": {}
},
"gradleProjectToTasksMap": {},
"gradleProjectToProjectName": {},
"gradleProjectNameToProjectRootMap": {},
"gradleProjectToChildProjects": {}
}"
`);
}); });
it('should process properties report with child projects', () => { it('should process properties report with child projects', () => {

View File

@ -11,18 +11,15 @@ import {
import { hashWithWorkspaceContext } from 'nx/src/utils/workspace-context'; import { hashWithWorkspaceContext } from 'nx/src/utils/workspace-context';
import { dirname } from 'path'; import { dirname } from 'path';
import { gradleConfigAndTestGlob } from './split-config-files'; import { gradleConfigAndTestGlob } from '../../utils/split-config-files';
import { import { getProjectReportLines } from './get-project-report-lines';
getProjectReportLines,
fileSeparator,
newLineSeparator,
} from './get-project-report-lines';
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory'; import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
import { fileSeparator, newLineSeparator } from '../../utils/exec-gradle';
export interface GradleReport { export interface GradleReport {
gradleFileToGradleProjectMap: Map<string, string>; gradleFileToGradleProjectMap: Map<string, string>;
buildFileToDepsMap: Map<string, Set<string>>;
gradleFileToOutputDirsMap: Map<string, Map<string, string>>; gradleFileToOutputDirsMap: Map<string, Map<string, string>>;
gradleProjectToDepsMap: Map<string, Set<string>>;
gradleProjectToTasksTypeMap: Map<string, Map<string, string>>; gradleProjectToTasksTypeMap: Map<string, Map<string, string>>;
gradleProjectToTasksMap: Map<string, Set<string>>; gradleProjectToTasksMap: Map<string, Set<string>>;
gradleProjectToProjectName: Map<string, string>; gradleProjectToProjectName: Map<string, string>;
@ -33,7 +30,7 @@ export interface GradleReport {
export interface GradleReportJSON { export interface GradleReportJSON {
hash: string; hash: string;
gradleFileToGradleProjectMap: Record<string, string>; gradleFileToGradleProjectMap: Record<string, string>;
buildFileToDepsMap: Record<string, Set<string>>; gradleProjectToDepsMap: Record<string, Array<string>>;
gradleFileToOutputDirsMap: Record<string, Record<string, string>>; gradleFileToOutputDirsMap: Record<string, Record<string, string>>;
gradleProjectToTasksTypeMap: Record<string, Record<string, string>>; gradleProjectToTasksTypeMap: Record<string, Record<string, string>>;
gradleProjectToTasksMap: Record<string, Array<string>>; gradleProjectToTasksMap: Record<string, Array<string>>;
@ -56,8 +53,10 @@ function readGradleReportCache(
gradleFileToGradleProjectMap: new Map( gradleFileToGradleProjectMap: new Map(
Object.entries(gradleReportJson['gradleFileToGradleProjectMap']) Object.entries(gradleReportJson['gradleFileToGradleProjectMap'])
), ),
buildFileToDepsMap: new Map( gradleProjectToDepsMap: new Map(
Object.entries(gradleReportJson['buildFileToDepsMap']) Object.entries(gradleReportJson['gradleProjectToDepsMap']).map(
([key, value]) => [key, new Set(value)]
)
), ),
gradleFileToOutputDirsMap: new Map( gradleFileToOutputDirsMap: new Map(
Object.entries(gradleReportJson['gradleFileToOutputDirsMap']).map( Object.entries(gradleReportJson['gradleFileToOutputDirsMap']).map(
@ -96,7 +95,12 @@ export function writeGradleReportToCache(
gradleFileToGradleProjectMap: Object.fromEntries( gradleFileToGradleProjectMap: Object.fromEntries(
results.gradleFileToGradleProjectMap results.gradleFileToGradleProjectMap
), ),
buildFileToDepsMap: Object.fromEntries(results.buildFileToDepsMap), gradleProjectToDepsMap: Object.fromEntries(
Array.from(results.gradleProjectToDepsMap).map(([key, value]) => [
key,
Array.from(value),
])
),
gradleFileToOutputDirsMap: Object.fromEntries( gradleFileToOutputDirsMap: Object.fromEntries(
Array.from(results.gradleFileToOutputDirsMap).map(([key, value]) => [ Array.from(results.gradleFileToOutputDirsMap).map(([key, value]) => [
key, key,
@ -215,7 +219,7 @@ export function processProjectReports(
* Map of Gradle File path to Gradle Project Name * Map of Gradle File path to Gradle Project Name
*/ */
const gradleFileToGradleProjectMap = new Map<string, string>(); const gradleFileToGradleProjectMap = new Map<string, string>();
const dependenciesMap = new Map<string, string>(); const gradleProjectToDepsMap = new Map<string, Set<string>>();
/** /**
* Map of Gradle Build File to tasks type map * Map of Gradle Build File to tasks type map
*/ */
@ -223,10 +227,6 @@ export function processProjectReports(
const gradleProjectToTasksMap = new Map<string, Set<string>>(); const gradleProjectToTasksMap = new Map<string, Set<string>>();
const gradleProjectToProjectName = new Map<string, string>(); const gradleProjectToProjectName = new Map<string, string>();
const gradleProjectNameToProjectRootMap = new Map<string, string>(); const gradleProjectNameToProjectRootMap = new Map<string, string>();
/**
* Map of buildFile to dependencies report path
*/
const buildFileToDepsMap = new Map<string, Set<string>>();
/** /**
* Map fo possible output files of each gradle file * Map fo possible output files of each gradle file
* e.g. {build.gradle.kts: { projectReportDir: '' testReportDir: '' }} * e.g. {build.gradle.kts: { projectReportDir: '' testReportDir: '' }}
@ -253,7 +253,10 @@ export function processProjectReports(
index++; index++;
} }
const [_, file] = projectReportLines[index].split(fileSeparator); const [_, file] = projectReportLines[index].split(fileSeparator);
dependenciesMap.set(gradleProject, file); gradleProjectToDepsMap.set(
gradleProject,
processGradleDependencies(file)
);
} }
if (line.endsWith('propertyReport')) { if (line.endsWith('propertyReport')) {
const gradleProject = line.substring( const gradleProject = line.substring(
@ -320,13 +323,6 @@ export function processProjectReports(
relative(workspaceRoot, absBuildFilePath) relative(workspaceRoot, absBuildFilePath)
); );
const buildDir = relative(workspaceRoot, absBuildDirPath); const buildDir = relative(workspaceRoot, absBuildDirPath);
const depsFile = dependenciesMap.get(gradleProject);
if (depsFile) {
buildFileToDepsMap.set(
buildFile,
processGradleDependencies(depsFile)
);
}
outputDirMap.set('build', `{workspaceRoot}/${buildDir}`); outputDirMap.set('build', `{workspaceRoot}/${buildDir}`);
outputDirMap.set( outputDirMap.set(
@ -389,9 +385,9 @@ export function processProjectReports(
return { return {
gradleFileToGradleProjectMap, gradleFileToGradleProjectMap,
buildFileToDepsMap,
gradleFileToOutputDirsMap, gradleFileToOutputDirsMap,
gradleProjectToTasksTypeMap, gradleProjectToTasksTypeMap,
gradleProjectToDepsMap,
gradleProjectToTasksMap, gradleProjectToTasksMap,
gradleProjectToProjectName, gradleProjectToProjectName,
gradleProjectNameToProjectRootMap, gradleProjectNameToProjectRootMap,

View File

@ -1,16 +1,7 @@
import { AggregateCreateNodesError, logger, output } from '@nx/devkit'; import { AggregateCreateNodesError, logger, output } from '@nx/devkit';
import { execGradleAsync } from './exec-gradle'; import { execGradleAsync, newLineSeparator } from '../../utils/exec-gradle';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import { dirname, join } from 'path'; import { dirname, join } from 'path';
import { execSync } from 'child_process';
export const fileSeparator = process.platform.startsWith('win')
? 'file:///'
: 'file://';
export const newLineSeparator = process.platform.startsWith('win')
? '\r\n'
: '\n';
/** /**
* This function executes the gradle projectReportAll task and returns the output as an array of lines. * This function executes the gradle projectReportAll task and returns the output as an array of lines.
@ -21,8 +12,6 @@ export async function getProjectReportLines(
gradlewFile: string gradlewFile: string
): Promise<string[]> { ): Promise<string[]> {
let projectReportBuffer: Buffer; let projectReportBuffer: Buffer;
// Attempt to run projectReport or projectReportAll task, regardless of build.gradle or build.gradle.kts location
try { try {
projectReportBuffer = await execGradleAsync(gradlewFile, [ projectReportBuffer = await execGradleAsync(gradlewFile, [
'projectReportAll', 'projectReportAll',

View File

@ -0,0 +1,849 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`@nx/gradle/plugin/nodes should create nodes based on gradle 1`] = `
[
[
"proj/build.gradle",
{
"externalNodes": {},
"projects": {
"proj": {
"metadata": {
"targetGroups": {
"help": [
"buildEnvironment",
],
},
"technologies": [
"gradle",
],
},
"name": "gradle-tutorial",
"root": "proj",
"targets": {
"buildEnvironment": {
"cache": true,
"command": "./gradlew :buildEnvironment",
"metadata": {
"description": "Displays all buildscript dependencies declared in root project 'gradle-tutorial'.",
"technologies": [
"gradle",
],
},
"options": {
"cwd": "proj",
},
},
},
},
},
},
],
]
`;
exports[`@nx/gradle/plugin/nodes should create nodes based on gradle for nested project root 1`] = `
[
[
"nested/nested/proj/build.gradle",
{
"externalNodes": {},
"projects": {
"nested/nested/proj": {
"metadata": {
"targetGroups": {
"help": [
"buildEnvironment",
],
},
"technologies": [
"gradle",
],
},
"name": "my-composite",
"root": "nested/nested/proj",
"targets": {
"buildEnvironment": {
"cache": true,
"command": "./gradlew :buildEnvironment",
"metadata": {
"description": "Displays all buildscript dependencies declared in root project 'my-composite'.",
"technologies": [
"gradle",
],
},
"options": {
"cwd": "nested/nested/proj",
},
},
},
},
},
},
],
]
`;
exports[`@nx/gradle/plugin/nodes should create nodes with atomized tests targets based on gradle if ciTargetName is specified 1`] = `
[
[
"proj/application/build.gradle",
{
"externalNodes": {},
"projects": {
"proj/application": {
"metadata": {
"targetGroups": {
"verification": [
"ci",
],
},
"technologies": [
"gradle",
],
},
"name": "application",
"root": "proj/application",
"targets": {
"ci": {
"cache": true,
"dependsOn": [
{
"params": "forward",
"projects": "self",
"target": "ci--DemoApplicationTest10",
},
{
"params": "forward",
"projects": "self",
"target": "ci--DemoApplicationTest7",
},
{
"params": "forward",
"projects": "self",
"target": "ci--DemoApplicationTest6",
},
{
"params": "forward",
"projects": "self",
"target": "ci--DemoApplicationTest3",
},
{
"params": "forward",
"projects": "self",
"target": "ci--DemoApplicationTest2",
},
{
"params": "forward",
"projects": "self",
"target": "ci--DemoApplicationTest9",
},
{
"params": "forward",
"projects": "self",
"target": "ci--DemoApplicationTest",
},
{
"params": "forward",
"projects": "self",
"target": "ci--DemoApplicationTest5",
},
{
"params": "forward",
"projects": "self",
"target": "ci--DemoApplicationTest4",
},
{
"params": "forward",
"projects": "self",
"target": "ci--DemoApplicationTest8",
},
],
"executor": "nx:noop",
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest10.java",
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest7.java",
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest6.java",
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest3.java",
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest2.java",
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest9.java",
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest.java",
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest5.java",
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest4.java",
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest8.java",
],
"metadata": {
"description": "Runs Gradle Tests in CI",
"technologies": [
"gradle",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin",
],
},
"ci--DemoApplicationTest": {
"cache": true,
"command": "./gradlew :application:test --tests DemoApplicationTest",
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar",
],
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest.java",
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest.java in CI",
"technologies": [
"gradle",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin",
],
},
"ci--DemoApplicationTest10": {
"cache": true,
"command": "./gradlew :application:test --tests DemoApplicationTest10",
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar",
],
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest10.java",
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest10.java in CI",
"technologies": [
"gradle",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin",
],
},
"ci--DemoApplicationTest2": {
"cache": true,
"command": "./gradlew :application:test --tests DemoApplicationTest2",
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar",
],
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest2.java",
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest2.java in CI",
"technologies": [
"gradle",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin",
],
},
"ci--DemoApplicationTest3": {
"cache": true,
"command": "./gradlew :application:test --tests DemoApplicationTest3",
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar",
],
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest3.java",
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest3.java in CI",
"technologies": [
"gradle",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin",
],
},
"ci--DemoApplicationTest4": {
"cache": true,
"command": "./gradlew :application:test --tests DemoApplicationTest4",
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar",
],
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest4.java",
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest4.java in CI",
"technologies": [
"gradle",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin",
],
},
"ci--DemoApplicationTest5": {
"cache": true,
"command": "./gradlew :application:test --tests DemoApplicationTest5",
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar",
],
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest5.java",
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest5.java in CI",
"technologies": [
"gradle",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin",
],
},
"ci--DemoApplicationTest6": {
"cache": true,
"command": "./gradlew :application:test --tests DemoApplicationTest6",
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar",
],
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest6.java",
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest6.java in CI",
"technologies": [
"gradle",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin",
],
},
"ci--DemoApplicationTest7": {
"cache": true,
"command": "./gradlew :application:test --tests DemoApplicationTest7",
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar",
],
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest7.java",
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest7.java in CI",
"technologies": [
"gradle",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin",
],
},
"ci--DemoApplicationTest8": {
"cache": true,
"command": "./gradlew :application:test --tests DemoApplicationTest8",
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar",
],
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest8.java",
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest8.java in CI",
"technologies": [
"gradle",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin",
],
},
"ci--DemoApplicationTest9": {
"cache": true,
"command": "./gradlew :application:test --tests DemoApplicationTest9",
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar",
],
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest9.java",
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest9.java in CI",
"technologies": [
"gradle",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin",
],
},
},
},
},
},
],
]
`;
exports[`@nx/gradle/plugin/nodes should not create nodes with atomized tests targets based on gradle if ciTargetName is not specified 1`] = `
[
[
"proj/application/build.gradle",
{
"externalNodes": {},
"projects": {
"proj/application": {
"metadata": {
"targetGroups": {
"verification": [
"ci",
],
},
"technologies": [
"gradle",
],
},
"name": "application",
"root": "proj/application",
"targets": {
"ci": {
"cache": true,
"dependsOn": [
{
"params": "forward",
"projects": "self",
"target": "ci--DemoApplicationTest10",
},
{
"params": "forward",
"projects": "self",
"target": "ci--DemoApplicationTest7",
},
{
"params": "forward",
"projects": "self",
"target": "ci--DemoApplicationTest6",
},
{
"params": "forward",
"projects": "self",
"target": "ci--DemoApplicationTest3",
},
{
"params": "forward",
"projects": "self",
"target": "ci--DemoApplicationTest2",
},
{
"params": "forward",
"projects": "self",
"target": "ci--DemoApplicationTest9",
},
{
"params": "forward",
"projects": "self",
"target": "ci--DemoApplicationTest",
},
{
"params": "forward",
"projects": "self",
"target": "ci--DemoApplicationTest5",
},
{
"params": "forward",
"projects": "self",
"target": "ci--DemoApplicationTest4",
},
{
"params": "forward",
"projects": "self",
"target": "ci--DemoApplicationTest8",
},
],
"executor": "nx:noop",
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest10.java",
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest7.java",
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest6.java",
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest3.java",
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest2.java",
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest9.java",
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest.java",
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest5.java",
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest4.java",
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest8.java",
],
"metadata": {
"description": "Runs Gradle Tests in CI",
"technologies": [
"gradle",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin",
],
},
"ci--DemoApplicationTest": {
"cache": true,
"command": "./gradlew :application:test --tests DemoApplicationTest",
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar",
],
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest.java",
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest.java in CI",
"technologies": [
"gradle",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin",
],
},
"ci--DemoApplicationTest10": {
"cache": true,
"command": "./gradlew :application:test --tests DemoApplicationTest10",
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar",
],
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest10.java",
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest10.java in CI",
"technologies": [
"gradle",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin",
],
},
"ci--DemoApplicationTest2": {
"cache": true,
"command": "./gradlew :application:test --tests DemoApplicationTest2",
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar",
],
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest2.java",
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest2.java in CI",
"technologies": [
"gradle",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin",
],
},
"ci--DemoApplicationTest3": {
"cache": true,
"command": "./gradlew :application:test --tests DemoApplicationTest3",
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar",
],
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest3.java",
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest3.java in CI",
"technologies": [
"gradle",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin",
],
},
"ci--DemoApplicationTest4": {
"cache": true,
"command": "./gradlew :application:test --tests DemoApplicationTest4",
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar",
],
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest4.java",
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest4.java in CI",
"technologies": [
"gradle",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin",
],
},
"ci--DemoApplicationTest5": {
"cache": true,
"command": "./gradlew :application:test --tests DemoApplicationTest5",
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar",
],
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest5.java",
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest5.java in CI",
"technologies": [
"gradle",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin",
],
},
"ci--DemoApplicationTest6": {
"cache": true,
"command": "./gradlew :application:test --tests DemoApplicationTest6",
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar",
],
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest6.java",
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest6.java in CI",
"technologies": [
"gradle",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin",
],
},
"ci--DemoApplicationTest7": {
"cache": true,
"command": "./gradlew :application:test --tests DemoApplicationTest7",
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar",
],
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest7.java",
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest7.java in CI",
"technologies": [
"gradle",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin",
],
},
"ci--DemoApplicationTest8": {
"cache": true,
"command": "./gradlew :application:test --tests DemoApplicationTest8",
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar",
],
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest8.java",
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest8.java in CI",
"technologies": [
"gradle",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin",
],
},
"ci--DemoApplicationTest9": {
"cache": true,
"command": "./gradlew :application:test --tests DemoApplicationTest9",
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar",
],
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest9.java",
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest9.java in CI",
"technologies": [
"gradle",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin",
],
},
},
},
},
},
],
]
`;

View File

@ -2,94 +2,71 @@ import {
CreateDependencies, CreateDependencies,
CreateDependenciesContext, CreateDependenciesContext,
DependencyType, DependencyType,
FileMap, logger,
RawProjectGraphDependency, normalizePath,
StaticDependency,
validateDependency, validateDependency,
workspaceRoot,
} from '@nx/devkit'; } from '@nx/devkit';
import { basename, dirname } from 'node:path'; import { relative } from 'node:path';
import { getCurrentGradleReport } from '../utils/get-gradle-report'; import {
import { GRADLE_BUILD_FILES } from '../utils/split-config-files'; getCurrentProjectGraphReport,
populateProjectGraph,
} from './utils/get-project-graph-from-gradle-plugin';
import { GradlePluginOptions } from './utils/gradle-plugin-options';
import { GRALDEW_FILES, splitConfigFiles } from '../utils/split-config-files';
import { globWithWorkspaceContext } from 'nx/src/utils/workspace-context';
export const createDependencies: CreateDependencies = async ( export const createDependencies: CreateDependencies<
_, GradlePluginOptions
> = async (
options: GradlePluginOptions,
context: CreateDependenciesContext context: CreateDependenciesContext
) => { ) => {
const gradleFiles: string[] = findGradleFiles(context.filesToProcess); const files = await globWithWorkspaceContext(
if (gradleFiles.length === 0) { workspaceRoot,
return []; Array.from(GRALDEW_FILES)
}
const gradleDependenciesStart = performance.mark('gradleDependencies:start');
const {
gradleFileToGradleProjectMap,
gradleProjectNameToProjectRootMap,
buildFileToDepsMap,
gradleProjectToChildProjects,
} = getCurrentGradleReport();
const dependencies: Set<RawProjectGraphDependency> = new Set();
for (const gradleFile of gradleFiles) {
const gradleProject = gradleFileToGradleProjectMap.get(gradleFile);
const projectName = Object.values(context.projects).find(
(project) => project.root === dirname(gradleFile)
)?.name;
const dependedProjects: Set<string> = buildFileToDepsMap.get(gradleFile);
if (projectName && dependedProjects?.size) {
dependedProjects?.forEach((dependedProject) => {
const targetProjectRoot = gradleProjectNameToProjectRootMap.get(
dependedProject
) as string;
const targetProjectName = Object.values(context.projects).find(
(project) => project.root === targetProjectRoot
)?.name;
if (targetProjectName) {
const dependency: RawProjectGraphDependency = {
source: projectName as string,
target: targetProjectName as string,
type: DependencyType.static,
sourceFile: gradleFile,
};
validateDependency(dependency, context);
dependencies.add(dependency);
}
});
}
gradleProjectToChildProjects.get(gradleProject)?.forEach((childProject) => {
if (childProject) {
const dependency: RawProjectGraphDependency = {
source: projectName as string,
target: childProject,
type: DependencyType.static,
sourceFile: gradleFile,
};
validateDependency(dependency, context);
dependencies.add(dependency);
}
});
}
const gradleDependenciesEnd = performance.mark('gradleDependencies:end');
performance.measure(
'gradleDependencies',
gradleDependenciesStart.name,
gradleDependenciesEnd.name
); );
const { gradlewFiles } = splitConfigFiles(files);
await populateProjectGraph(context.workspaceRoot, gradlewFiles, options);
const { dependencies: dependenciesFromReport } =
getCurrentProjectGraphReport();
return Array.from(dependencies); const dependencies: Array<StaticDependency> = [];
}; dependenciesFromReport.forEach((dependencyFromPlugin: StaticDependency) => {
try {
function findGradleFiles(fileMap: FileMap): string[] { const source =
const gradleFiles: string[] = []; relative(workspaceRoot, dependencyFromPlugin.source) || '.';
const sourceProjectName =
for (const [_, files] of Object.entries(fileMap.projectFileMap)) { Object.values(context.projects).find(
for (const file of files) { (project) => source === project.root
if (GRADLE_BUILD_FILES.has(basename(file.file))) { )?.name ?? dependencyFromPlugin.source;
gradleFiles.push(file.file); const target =
relative(workspaceRoot, dependencyFromPlugin.target) || '.';
const targetProjectName =
Object.values(context.projects).find(
(project) => target === project.root
)?.name ?? dependencyFromPlugin.target;
if (!sourceProjectName || !targetProjectName) {
return;
} }
const dependency: StaticDependency = {
source: sourceProjectName,
target: targetProjectName,
type: DependencyType.static,
sourceFile: normalizePath(
relative(workspaceRoot, dependencyFromPlugin.sourceFile)
),
};
validateDependency(dependency, context);
dependencies.push(dependency);
} catch {
logger.warn(
`Unable to parse dependency from gradle plugin: ${dependencyFromPlugin.source} -> ${dependencyFromPlugin.target}`
);
} }
} });
return gradleFiles; return dependencies;
} };

View File

@ -1,20 +1,22 @@
import { CreateNodesContext } from '@nx/devkit'; import { CreateNodesContext, readJsonFile } from '@nx/devkit';
import { join } from 'path';
import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; import { TempFs } from 'nx/src/internal-testing-utils/temp-fs';
import { type GradleReport } from '../utils/get-gradle-report'; import { type ProjectGraphReport } from './utils/get-project-graph-from-gradle-plugin';
let gradleReport: GradleReport; let gradleReport: ProjectGraphReport;
jest.mock('../utils/get-gradle-report', () => { jest.mock('./utils/get-project-graph-from-gradle-plugin', () => {
return { return {
GRADLE_BUILD_FILES: new Set(['build.gradle', 'build.gradle.kts']), GRADLE_BUILD_FILES: new Set(['build.gradle', 'build.gradle.kts']),
populateGradleReport: jest.fn().mockImplementation(() => void 0), populateProjectGraph: jest.fn().mockImplementation(() => void 0),
getCurrentGradleReport: jest.fn().mockImplementation(() => gradleReport), getCurrentProjectGraphReport: jest
.fn()
.mockImplementation(() => gradleReport),
}; };
}); });
import { createNodesV2 } from './nodes'; import { createNodesV2 } from './nodes';
describe('@nx/gradle/plugin', () => { describe('@nx/gradle/plugin/nodes', () => {
let createNodesFunction = createNodesV2[1]; let createNodesFunction = createNodesV2[1];
let context: CreateNodesContext; let context: CreateNodesContext;
let tempFs: TempFs; let tempFs: TempFs;
@ -22,32 +24,9 @@ describe('@nx/gradle/plugin', () => {
beforeEach(async () => { beforeEach(async () => {
tempFs = new TempFs('test'); tempFs = new TempFs('test');
gradleReport = { gradleReport = readJsonFile(
gradleFileToGradleProjectMap: new Map<string, string>([ join(__dirname, 'utils/__mocks__/gradle_tutorial.json')
['proj/build.gradle', 'proj'], );
]),
buildFileToDepsMap: new Map<string, Set<string>>(),
gradleFileToOutputDirsMap: new Map<string, Map<string, string>>([
['proj/build.gradle', new Map([['build', 'build']])],
]),
gradleProjectToTasksMap: new Map<string, Set<string>>([
['proj', new Set(['test'])],
]),
gradleProjectToTasksTypeMap: new Map<string, Map<string, string>>([
[
'proj',
new Map([
['test', 'Verification'],
['build', 'Build'],
]),
],
]),
gradleProjectToProjectName: new Map<string, string>([['proj', 'proj']]),
gradleProjectNameToProjectRootMap: new Map<string, string>([
['proj', 'proj'],
]),
gradleProjectToChildProjects: new Map<string, string[]>(),
};
cwd = process.cwd(); cwd = process.cwd();
process.chdir(tempFs.tempDir); process.chdir(tempFs.tempDir);
context = { context = {
@ -81,190 +60,13 @@ describe('@nx/gradle/plugin', () => {
context context
); );
expect(results).toMatchInlineSnapshot(` expect(results).toMatchSnapshot();
[
[
"proj/build.gradle",
{
"projects": {
"proj": {
"metadata": {
"targetGroups": {
"Verification": [
"test",
],
},
"technologies": [
"gradle",
],
},
"name": "proj",
"projectType": "application",
"targets": {
"test": {
"cache": true,
"command": "./gradlew proj:test",
"dependsOn": [
"testClasses",
],
"inputs": [
"default",
"^production",
],
"metadata": {
"help": {
"command": "./gradlew help --task proj:test",
"example": {
"options": {
"args": [
"--rerun",
],
},
},
},
"technologies": [
"gradle",
],
},
"options": {
"cwd": ".",
},
},
},
},
},
},
],
]
`);
});
it('should create nodes include subprojects tasks', async () => {
const results = await createNodesFunction(
['proj/build.gradle'],
{
buildTargetName: 'build',
includeSubprojectsTasks: true,
},
context
);
expect(results).toMatchInlineSnapshot(`
[
[
"proj/build.gradle",
{
"projects": {
"proj": {
"metadata": {
"targetGroups": {
"Build": [
"build",
],
"Verification": [
"test",
],
},
"technologies": [
"gradle",
],
},
"name": "proj",
"projectType": "application",
"targets": {
"build": {
"cache": true,
"command": "./gradlew proj:build",
"dependsOn": [
"^build",
"classes",
"test",
],
"inputs": [
"production",
"^production",
],
"metadata": {
"help": {
"command": "./gradlew help --task proj:build",
"example": {
"options": {
"args": [
"--rerun",
],
},
},
},
"technologies": [
"gradle",
],
},
"options": {
"cwd": ".",
},
"outputs": [
"build",
],
},
"test": {
"cache": true,
"command": "./gradlew proj:test",
"dependsOn": [
"testClasses",
],
"inputs": [
"default",
"^production",
],
"metadata": {
"help": {
"command": "./gradlew help --task proj:test",
"example": {
"options": {
"args": [
"--rerun",
],
},
},
},
"technologies": [
"gradle",
],
},
"options": {
"cwd": ".",
},
},
},
},
},
},
],
]
`);
}); });
it('should create nodes based on gradle for nested project root', async () => { it('should create nodes based on gradle for nested project root', async () => {
gradleReport = { gradleReport = readJsonFile(
gradleFileToGradleProjectMap: new Map<string, string>([ join(__dirname, '/utils/__mocks__/gradle_composite.json')
['nested/nested/proj/build.gradle', 'proj'], );
]),
buildFileToDepsMap: new Map<string, Set<string>>(),
gradleFileToOutputDirsMap: new Map<string, Map<string, string>>([
['nested/nested/proj/build.gradle', new Map([['build', 'build']])],
]),
gradleProjectToTasksMap: new Map<string, Set<string>>([
['proj', new Set(['test'])],
]),
gradleProjectToTasksTypeMap: new Map<string, Map<string, string>>([
['proj', new Map([['test', 'Verification']])],
]),
gradleProjectToProjectName: new Map<string, string>([['proj', 'proj']]),
gradleProjectNameToProjectRootMap: new Map<string, string>([
['proj', 'proj'],
]),
gradleProjectToChildProjects: new Map<string, string[]>(),
};
await tempFs.createFiles({ await tempFs.createFiles({
'nested/nested/proj/build.gradle': ``, 'nested/nested/proj/build.gradle': ``,
}); });
@ -277,311 +79,30 @@ describe('@nx/gradle/plugin', () => {
context context
); );
expect(results).toMatchInlineSnapshot(` expect(results).toMatchSnapshot();
[
[
"nested/nested/proj/build.gradle",
{
"projects": {
"nested/nested/proj": {
"metadata": {
"targetGroups": {
"Verification": [
"test",
],
},
"technologies": [
"gradle",
],
},
"name": "proj",
"projectType": "application",
"targets": {
"test": {
"cache": true,
"command": "./gradlew proj:test",
"dependsOn": [
"testClasses",
],
"inputs": [
"default",
"^production",
],
"metadata": {
"help": {
"command": "./gradlew help --task proj:test",
"example": {
"options": {
"args": [
"--rerun",
],
},
},
},
"technologies": [
"gradle",
],
},
"options": {
"cwd": ".",
},
},
},
},
},
},
],
]
`);
}); });
describe('with atomized tests targets', () => { it('should create nodes with atomized tests targets based on gradle if ciTargetName is specified', async () => {
beforeEach(async () => { const results = await createNodesFunction(
gradleReport = { ['proj/application/build.gradle'],
gradleFileToGradleProjectMap: new Map<string, string>([ {
['nested/nested/proj/build.gradle', 'proj'], buildTargetName: 'build',
]), ciTargetName: 'test-ci',
buildFileToDepsMap: new Map<string, Set<string>>(), },
gradleFileToOutputDirsMap: new Map<string, Map<string, string>>([ context
['nested/nested/proj/build.gradle', new Map([['build', 'build']])], );
]),
gradleProjectToTasksMap: new Map<string, Set<string>>([
['proj', new Set(['test'])],
]),
gradleProjectToTasksTypeMap: new Map<string, Map<string, string>>([
['proj', new Map([['test', 'Test']])],
]),
gradleProjectToProjectName: new Map<string, string>([['proj', 'proj']]),
gradleProjectNameToProjectRootMap: new Map<string, string>([
['proj', 'proj'],
]),
gradleProjectToChildProjects: new Map<string, string[]>(),
};
await tempFs.createFiles({
'nested/nested/proj/build.gradle': ``,
});
await tempFs.createFiles({
'proj/src/test/java/test/rootTest.java': ``,
});
await tempFs.createFiles({
'nested/nested/proj/src/test/java/test/aTest.java': ``,
});
await tempFs.createFiles({
'nested/nested/proj/src/test/java/test/bTest.java': ``,
});
await tempFs.createFiles({
'nested/nested/proj/src/test/java/test/cTests.java': ``,
});
});
it('should create nodes with atomized tests targets based on gradle for nested project root', async () => { expect(results).toMatchSnapshot();
const results = await createNodesFunction( });
[
'nested/nested/proj/build.gradle',
'proj/src/test/java/test/rootTest.java',
'nested/nested/proj/src/test/java/test/aTest.java',
'nested/nested/proj/src/test/java/test/bTest.java',
'nested/nested/proj/src/test/java/test/cTests.java',
],
{
buildTargetName: 'build',
ciTargetName: 'test-ci',
},
context
);
expect(results).toMatchInlineSnapshot(` it('should not create nodes with atomized tests targets based on gradle if ciTargetName is not specified', async () => {
[ const results = await createNodesFunction(
[ ['proj/application/build.gradle'],
"nested/nested/proj/build.gradle", {
{ buildTargetName: 'build',
"projects": { },
"nested/nested/proj": { context
"metadata": { );
"targetGroups": { expect(results).toMatchSnapshot();
"Test": [
"test-ci--aTest",
"test-ci--bTest",
"test-ci--cTests",
"test-ci",
"test",
],
},
"technologies": [
"gradle",
],
},
"name": "proj",
"projectType": "application",
"targets": {
"test": {
"cache": false,
"command": "./gradlew proj:test",
"dependsOn": [
"testClasses",
],
"inputs": [
"default",
"^production",
],
"metadata": {
"help": {
"command": "./gradlew help --task proj:test",
"example": {
"options": {
"args": [
"--rerun",
],
},
},
},
"technologies": [
"gradle",
],
},
"options": {
"cwd": ".",
},
},
"test-ci": {
"cache": true,
"dependsOn": [
{
"params": "forward",
"projects": "self",
"target": "test-ci--aTest",
},
{
"params": "forward",
"projects": "self",
"target": "test-ci--bTest",
},
{
"params": "forward",
"projects": "self",
"target": "test-ci--cTests",
},
],
"executor": "nx:noop",
"inputs": [
"default",
"^production",
],
"metadata": {
"description": "Runs Gradle Tests in CI",
"help": {
"command": "./gradlew help --task proj:test",
"example": {
"options": {
"args": [
"--rerun",
],
},
},
},
"nonAtomizedTarget": "test",
"technologies": [
"gradle",
],
},
},
"test-ci--aTest": {
"cache": true,
"command": "./gradlew proj:test --tests aTest",
"dependsOn": [
"testClasses",
],
"inputs": [
"default",
"^production",
],
"metadata": {
"description": "Runs Gradle test nested/nested/proj/src/test/java/test/aTest.java in CI",
"help": {
"command": "./gradlew help --task proj:test",
"example": {
"options": {
"args": [
"--rerun",
],
},
},
},
"technologies": [
"gradle",
],
},
"options": {
"cwd": ".",
},
},
"test-ci--bTest": {
"cache": true,
"command": "./gradlew proj:test --tests bTest",
"dependsOn": [
"testClasses",
],
"inputs": [
"default",
"^production",
],
"metadata": {
"description": "Runs Gradle test nested/nested/proj/src/test/java/test/bTest.java in CI",
"help": {
"command": "./gradlew help --task proj:test",
"example": {
"options": {
"args": [
"--rerun",
],
},
},
},
"technologies": [
"gradle",
],
},
"options": {
"cwd": ".",
},
},
"test-ci--cTests": {
"cache": true,
"command": "./gradlew proj:test --tests cTests",
"dependsOn": [
"testClasses",
],
"inputs": [
"default",
"^production",
],
"metadata": {
"description": "Runs Gradle test nested/nested/proj/src/test/java/test/cTests.java in CI",
"help": {
"command": "./gradlew help --task proj:test",
"example": {
"options": {
"args": [
"--rerun",
],
},
},
},
"technologies": [
"gradle",
],
},
"options": {
"cwd": ".",
},
},
},
},
},
},
],
]
`);
});
}); });
}); });

View File

@ -1,67 +1,36 @@
import { import {
CreateNodes,
CreateNodesV2, CreateNodesV2,
CreateNodesContext, CreateNodesContext,
ProjectConfiguration, ProjectConfiguration,
TargetConfiguration,
createNodesFromFiles, createNodesFromFiles,
readJsonFile, readJsonFile,
writeJsonFile, writeJsonFile,
CreateNodesFunction, CreateNodesFunction,
logger, workspaceRoot,
ProjectGraphExternalNode,
} from '@nx/devkit'; } from '@nx/devkit';
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes'; import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
import { existsSync } from 'node:fs'; import { existsSync } from 'node:fs';
import { basename, dirname, join } from 'node:path'; import { dirname, join } from 'node:path';
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory'; import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
import { findProjectForPath } from 'nx/src/devkit-internals';
import {
populateGradleReport,
getCurrentGradleReport,
GradleReport,
} from '../utils/get-gradle-report';
import { hashObject } from 'nx/src/hasher/file-hasher'; import { hashObject } from 'nx/src/hasher/file-hasher';
import { import {
gradleConfigAndTestGlob, gradleConfigAndTestGlob,
gradleConfigGlob,
splitConfigFiles, splitConfigFiles,
} from '../utils/split-config-files'; } from '../utils/split-config-files';
import { getGradleExecFile, findGraldewFile } from '../utils/exec-gradle'; import {
getCurrentProjectGraphReport,
const cacheableTaskType = new Set(['Build', 'Verification']); populateProjectGraph,
const dependsOnMap = { } from './utils/get-project-graph-from-gradle-plugin';
build: ['^build', 'classes', 'test'], import {
testClasses: ['classes'], GradlePluginOptions,
test: ['testClasses'], normalizeOptions,
classes: ['^classes'], } from './utils/gradle-plugin-options';
};
interface GradleTask {
type: string;
name: string;
}
export interface GradlePluginOptions {
includeSubprojectsTasks?: boolean; // default is false, show all gradle tasks in the project
ciTargetName?: string;
testTargetName?: string;
classesTargetName?: string;
buildTargetName?: string;
[taskTargetName: string]: string | undefined | boolean;
}
function normalizeOptions(options: GradlePluginOptions): GradlePluginOptions {
options ??= {};
options.testTargetName ??= 'test';
options.classesTargetName ??= 'classes';
options.buildTargetName ??= 'build';
return options;
}
type GradleTargets = Record<string, Partial<ProjectConfiguration>>; type GradleTargets = Record<string, Partial<ProjectConfiguration>>;
function readTargetsCache(cachePath: string): GradleTargets { function readProjectsCache(cachePath: string): GradleTargets {
return existsSync(cachePath) ? readJsonFile(cachePath) : {}; return existsSync(cachePath) ? readJsonFile(cachePath) : {};
} }
@ -72,47 +41,39 @@ export function writeTargetsToCache(cachePath: string, results: GradleTargets) {
export const createNodesV2: CreateNodesV2<GradlePluginOptions> = [ export const createNodesV2: CreateNodesV2<GradlePluginOptions> = [
gradleConfigAndTestGlob, gradleConfigAndTestGlob,
async (files, options, context) => { async (files, options, context) => {
const { buildFiles, projectRoots, gradlewFiles, testFiles } = const { buildFiles, gradlewFiles } = splitConfigFiles(files);
splitConfigFiles(files);
const optionsHash = hashObject(options); const optionsHash = hashObject(options);
const cachePath = join( const cachePath = join(
workspaceDataDirectory, workspaceDataDirectory,
`gradle-${optionsHash}.hash` `gradle-${optionsHash}.hash`
); );
const targetsCache = readTargetsCache(cachePath); const projectsCache = readProjectsCache(cachePath);
await populateGradleReport( await populateProjectGraph(
context.workspaceRoot, context.workspaceRoot,
gradlewFiles.map((f) => join(context.workspaceRoot, f)) gradlewFiles.map((f) => join(context.workspaceRoot, f)),
); options
const gradleReport = getCurrentGradleReport();
const gradleProjectRootToTestFilesMap = getGradleProjectRootToTestFilesMap(
testFiles,
projectRoots
); );
const { nodes, externalNodes } = getCurrentProjectGraphReport();
try { try {
return createNodesFromFiles( return createNodesFromFiles(
makeCreateNodesForGradleConfigFile( makeCreateNodesForGradleConfigFile(nodes, projectsCache, externalNodes),
gradleReport,
targetsCache,
gradleProjectRootToTestFilesMap
),
buildFiles, buildFiles,
options, options,
context context
); );
} finally { } finally {
writeTargetsToCache(cachePath, targetsCache); writeTargetsToCache(cachePath, projectsCache);
} }
}, },
]; ];
export const makeCreateNodesForGradleConfigFile = export const makeCreateNodesForGradleConfigFile =
( (
gradleReport: GradleReport, projects: Record<string, Partial<ProjectConfiguration>>,
targetsCache: GradleTargets = {}, projectsCache: GradleTargets = {},
gradleProjectRootToTestFilesMap: Record<string, string[]> = {} externalNodes: Record<string, ProjectGraphExternalNode> = {}
): CreateNodesFunction => ): CreateNodesFunction =>
async ( async (
gradleFilePath, gradleFilePath,
@ -127,309 +88,18 @@ export const makeCreateNodesForGradleConfigFile =
options ?? {}, options ?? {},
context context
); );
targetsCache[hash] ??= await createGradleProject( projectsCache[hash] ??=
gradleReport, projects[projectRoot] ?? projects[join(workspaceRoot, projectRoot)];
gradleFilePath, const project = projectsCache[hash];
options,
context,
gradleProjectRootToTestFilesMap[projectRoot]
);
const project = targetsCache[hash];
if (!project) { if (!project) {
return {}; return {};
} }
project.root = projectRoot;
return { return {
projects: { projects: {
[projectRoot]: project, [projectRoot]: project,
}, },
externalNodes: externalNodes,
}; };
}; };
/**
@deprecated This is replaced with {@link createNodesV2}. Update your plugin to export its own `createNodesV2` function that wraps this one instead.
This function will change to the v2 function in Nx 20.
*/
export const createNodes: CreateNodes<GradlePluginOptions> = [
gradleConfigGlob,
async (buildFile, options, context) => {
logger.warn(
'`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.'
);
const { gradlewFiles } = splitConfigFiles(context.configFiles);
await populateGradleReport(context.workspaceRoot, gradlewFiles);
const gradleReport = getCurrentGradleReport();
const internalCreateNodes =
makeCreateNodesForGradleConfigFile(gradleReport);
return await internalCreateNodes(buildFile, options, context);
},
];
async function createGradleProject(
gradleReport: GradleReport,
gradleFilePath: string,
options: GradlePluginOptions | undefined,
context: CreateNodesContext,
testFiles = []
) {
try {
const {
gradleProjectToTasksTypeMap,
gradleProjectToTasksMap,
gradleFileToOutputDirsMap,
gradleFileToGradleProjectMap,
gradleProjectToProjectName,
} = gradleReport;
const gradleProject = gradleFileToGradleProjectMap.get(
gradleFilePath
) as string;
const projectName = gradleProjectToProjectName.get(gradleProject);
if (!projectName) {
return;
}
const tasksTypeMap: Map<string, string> = gradleProjectToTasksTypeMap.get(
gradleProject
) as Map<string, string>;
const tasksSet = gradleProjectToTasksMap.get(gradleProject) as Set<string>;
let tasks: GradleTask[] = [];
tasksSet.forEach((taskName) => {
tasks.push({
type: tasksTypeMap?.get(taskName) as string,
name: taskName,
});
});
if (options.includeSubprojectsTasks) {
tasksTypeMap.forEach((taskType, taskName) => {
if (!tasksSet.has(taskName)) {
tasks.push({
type: taskType,
name: taskName,
});
}
});
}
const outputDirs = gradleFileToOutputDirsMap.get(gradleFilePath) as Map<
string,
string
>;
const { targets, targetGroups } = await createGradleTargets(
tasks,
options,
context,
outputDirs,
gradleProject,
gradleFilePath,
testFiles
);
const project: Partial<ProjectConfiguration> = {
name: projectName,
projectType: 'application',
targets,
metadata: {
targetGroups,
technologies: ['gradle'],
},
};
return project;
} catch (e) {
console.error(e);
return undefined;
}
}
async function createGradleTargets(
tasks: GradleTask[],
options: GradlePluginOptions | undefined,
context: CreateNodesContext,
outputDirs: Map<string, string>,
gradleProject: string,
gradleBuildFilePath: string,
testFiles: string[] = []
): Promise<{
targetGroups: Record<string, string[]>;
targets: Record<string, TargetConfiguration>;
}> {
const inputsMap = createInputsMap(context);
const gradlewFileDirectory = dirname(
findGraldewFile(gradleBuildFilePath, context.workspaceRoot)
);
const targets: Record<string, TargetConfiguration> = {};
const targetGroups: Record<string, string[]> = {};
for (const task of tasks) {
const targetName = options?.[`${task.name}TargetName`] ?? task.name;
let outputs = [outputDirs.get(task.name)].filter(Boolean);
if (task.name === 'test') {
outputs = [
outputDirs.get('testReport'),
outputDirs.get('testResults'),
].filter(Boolean);
getTestCiTargets(
testFiles,
gradleProject,
targetName as string,
options.ciTargetName,
inputsMap['test'],
outputs,
task.type,
targets,
targetGroups,
gradlewFileDirectory
);
}
const taskCommandToRun = `${gradleProject ? gradleProject + ':' : ''}${
task.name
}`;
targets[targetName as string] = {
command: `${getGradleExecFile()} ${taskCommandToRun}`,
options: {
cwd: gradlewFileDirectory,
},
cache: cacheableTaskType.has(task.type),
inputs: inputsMap[task.name],
dependsOn: dependsOnMap[task.name],
metadata: {
technologies: ['gradle'],
help: {
command: `${getGradleExecFile()} help --task ${taskCommandToRun}`,
example: {
options: {
args: ['--rerun'],
},
},
},
},
...(outputs && outputs.length ? { outputs } : {}),
};
if (task.type) {
if (!targetGroups[task.type]) {
targetGroups[task.type] = [];
}
targetGroups[task.type].push(targetName as string);
}
}
return { targetGroups, targets };
}
function createInputsMap(
context: CreateNodesContext
): Record<string, TargetConfiguration['inputs']> {
const namedInputs = context.nxJsonConfiguration.namedInputs;
return {
build: namedInputs?.production
? ['production', '^production']
: ['default', '^default'],
test: ['default', namedInputs?.production ? '^production' : '^default'],
classes: namedInputs?.production
? ['production', '^production']
: ['default', '^default'],
};
}
function getTestCiTargets(
testFiles: string[],
gradleProject: string,
testTargetName: string,
ciTargetName: string,
inputs: TargetConfiguration['inputs'],
outputs: string[],
targetGroupName: string,
targets: Record<string, TargetConfiguration>,
targetGroups: Record<string, string[]>,
gradlewFileDirectory: string
): void {
if (!testFiles || testFiles.length === 0 || !ciTargetName) {
return;
}
const taskCommandToRun = `${gradleProject ? gradleProject + ':' : ''}test`;
if (!targetGroups[targetGroupName]) {
targetGroups[targetGroupName] = [];
}
const dependsOn: TargetConfiguration['dependsOn'] = [];
testFiles.forEach((testFile) => {
const testName = basename(testFile).split('.')[0];
const targetName = ciTargetName + '--' + testName;
targets[targetName] = {
command: `${getGradleExecFile()} ${taskCommandToRun} --tests ${testName}`,
options: {
cwd: gradlewFileDirectory,
},
cache: true,
inputs,
dependsOn: dependsOnMap['test'],
metadata: {
technologies: ['gradle'],
description: `Runs Gradle test ${testFile} in CI`,
help: {
command: `${getGradleExecFile()} help --task ${taskCommandToRun}`,
example: {
options: {
args: ['--rerun'],
},
},
},
},
...(outputs && outputs.length > 0 ? { outputs } : {}),
};
targetGroups[targetGroupName].push(targetName);
dependsOn.push({
target: targetName,
projects: 'self',
params: 'forward',
});
});
targets[ciTargetName] = {
executor: 'nx:noop',
cache: true,
inputs,
dependsOn: dependsOn,
...(outputs && outputs.length > 0 ? { outputs } : {}),
metadata: {
technologies: ['gradle'],
description: 'Runs Gradle Tests in CI',
nonAtomizedTarget: testTargetName,
help: {
command: `${getGradleExecFile()} help --task ${taskCommandToRun}`,
example: {
options: {
args: ['--rerun'],
},
},
},
},
};
targetGroups[targetGroupName].push(ciTargetName);
}
function getGradleProjectRootToTestFilesMap(
testFiles: string[],
projectRoots: string[]
): Record<string, string[]> | undefined {
if (testFiles.length === 0 || projectRoots.length === 0) {
return;
}
const roots = new Map(projectRoots.map((root) => [root, root]));
const testFilesToGradleProjectMap: Record<string, string[]> = {};
testFiles.forEach((testFile) => {
const projectRoot = findProjectForPath(testFile, roots);
if (projectRoot) {
if (!testFilesToGradleProjectMap[projectRoot]) {
testFilesToGradleProjectMap[projectRoot] = [];
}
testFilesToGradleProjectMap[projectRoot].push(testFile);
}
});
return testFilesToGradleProjectMap;
}

View File

@ -0,0 +1,38 @@
{
"nodes": {
"nested/nested/proj": {
"targets": {
"buildEnvironment": {
"cache": true,
"metadata": {
"description": "Displays all buildscript dependencies declared in root project \u0027my-composite\u0027.",
"technologies": ["gradle"]
},
"command": "./gradlew :buildEnvironment",
"options": {
"cwd": "nested/nested/proj"
}
}
},
"metadata": {
"targetGroups": {
"help": ["buildEnvironment"]
},
"technologies": ["gradle"]
},
"name": "my-composite"
}
},
"dependencies": [
{
"source": "nested/nested/proj",
"target": "projectRoot/my-app",
"sourceFile": "projectRoot/build.gradle.kts"
},
{
"source": "nested/nested/proj",
"target": "projectRoot/my-utils",
"sourceFile": "projectRoot/build.gradle.kts"
}
]
}

View File

@ -0,0 +1,590 @@
{
"targets": {
"assemble": {
"cache": true,
"parallelism": false,
"dependsOn": ["list:jar"],
"command": "./gradlew :list:assemble",
"metadata": {
"description": "Assembles the outputs of this project.",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:assemble" }
},
"options": { "cwd": "." }
},
"build": {
"cache": true,
"parallelism": false,
"dependsOn": ["list:check", "list:assemble"],
"command": "./gradlew :list:build",
"metadata": {
"description": "Assembles and tests this project.",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:build" }
},
"options": { "cwd": "." }
},
"buildDependents": {
"cache": true,
"parallelism": false,
"command": "./gradlew :list:buildDependents",
"metadata": {
"description": "Assembles and tests this project and all projects that depend on it.",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:buildDependents" }
},
"options": { "cwd": "." }
},
"buildEnvironment": {
"cache": true,
"parallelism": false,
"command": "./gradlew :list:buildEnvironment",
"metadata": {
"description": "Displays all buildscript dependencies declared in project \u0027:list\u0027.",
"technologies": ["gradle"],
"help": {
"command": "./gradlew help --task :list:buildEnvironment"
}
},
"options": { "cwd": "." }
},
"buildNeeded": {
"cache": true,
"parallelism": false,
"dependsOn": ["list:build"],
"command": "./gradlew :list:buildNeeded",
"metadata": {
"description": "Assembles and tests this project and all projects it depends on.",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:buildNeeded" }
},
"options": { "cwd": "." }
},
"check": {
"cache": true,
"parallelism": false,
"dependsOn": ["list:test"],
"command": "./gradlew :list:check",
"metadata": {
"description": "Runs all checks.",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:check" }
},
"options": { "cwd": "." }
},
"classes": {
"cache": true,
"parallelism": false,
"dependsOn": ["list:compileJava", "list:processResources"],
"command": "./gradlew :list:classes",
"metadata": {
"description": "Assembles main classes.",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:classes" }
},
"options": { "cwd": "." }
},
"clean": {
"cache": true,
"parallelism": false,
"command": "./gradlew :list:clean",
"metadata": {
"description": "Deletes the build directory.",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:clean" }
},
"options": { "cwd": "." }
},
"compileJava": {
"cache": true,
"parallelism": false,
"inputs": [
"{projectRoot}/src/main/java/org/example/list/LinkedList.java"
],
"outputs": [
"{projectRoot}/build/classes/java/main",
"{projectRoot}/build/generated/sources/annotationProcessor/java/main",
"{projectRoot}/build/generated/sources/headers/java/main",
"{projectRoot}/build/tmp/compileJava/previous-compilation-data.bin"
],
"command": "./gradlew :list:compileJava",
"metadata": {
"description": "Compiles main Java source.",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:compileJava" }
},
"options": { "cwd": "." }
},
"compileTestJava": {
"cache": true,
"parallelism": false,
"inputs": [
"{projectRoot}/src/test/java/org/example/list/LinkedListTest.java",
"{projectRoot}/src/test/java/org/example/list/LinkedList2Test.java"
],
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin"
],
"dependsOn": ["list:classes", "list:compileJava"],
"command": "./gradlew :list:compileTestJava",
"metadata": {
"description": "Compiles test Java source.",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:compileTestJava" }
},
"options": { "cwd": "." }
},
"ci--LinkedListTest": {
"command": "./gradlew :list:test --tests LinkedListTest",
"metadata": {
"description": "Runs Gradle test LinkedListTest in CI",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:test" }
},
"cache": true,
"parallelism": false,
"inputs": [
"{projectRoot}/src/test/java/org/example/list/LinkedListTest.java"
],
"dependsOn": [
"list:compileTestJava",
"list:testClasses",
"list:classes",
"list:compileJava"
],
"outputs": [
"{projectRoot}/build/test-results/test/binary",
"{projectRoot}/build/reports/tests/test",
"{projectRoot}/build/test-results/test"
]
},
"ci--LinkedList2Test": {
"command": "./gradlew :list:test --tests LinkedList2Test",
"metadata": {
"description": "Runs Gradle test LinkedList2Test in CI",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:test" }
},
"cache": true,
"parallelism": false,
"inputs": [
"{projectRoot}/src/test/java/org/example/list/LinkedList2Test.java"
],
"dependsOn": [
"list:compileTestJava",
"list:testClasses",
"list:classes",
"list:compileJava"
],
"outputs": [
"{projectRoot}/build/test-results/test/binary",
"{projectRoot}/build/reports/tests/test",
"{projectRoot}/build/test-results/test"
]
},
"ci": {
"executor": "nx:noop",
"metadata": {
"description": "Runs Gradle Tests in CI",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:test" }
},
"dependsOn": [
{
"target": "ci--LinkedListTest",
"projects": "self",
"params": "forward"
},
{
"target": "ci--LinkedList2Test",
"projects": "self",
"params": "forward"
}
],
"cache": true,
"parallelism": false
},
"components": {
"cache": true,
"parallelism": false,
"command": "./gradlew :list:components",
"metadata": {
"description": "Displays the components produced by project \u0027:list\u0027. [deprecated]",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:components" }
},
"options": { "cwd": "." }
},
"nxProjectGraph": {
"cache": true,
"parallelism": false,
"dependsOn": ["list:nxProjectGraphLocal"],
"command": "./gradlew :list:nxProjectGraph",
"metadata": {
"description": "Print nodes report for Nx",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:nxProjectGraph" }
},
"options": { "cwd": "." }
},
"createNodesLocal": {
"cache": true,
"parallelism": false,
"outputs": ["{projectRoot}/build/nx/list.json"],
"command": "./gradlew :list:nxProjectGraphLocal",
"metadata": {
"description": "Create nodes and dependencies for Nx",
"technologies": ["gradle"],
"help": {
"command": "./gradlew help --task :list:nxProjectGraphLocal"
}
},
"options": { "cwd": "." }
},
"dependencies": {
"cache": true,
"parallelism": false,
"command": "./gradlew :list:dependencies",
"metadata": {
"description": "Displays all dependencies declared in project \u0027:list\u0027.",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:dependencies" }
},
"options": { "cwd": "." }
},
"dependencyInsight": {
"cache": true,
"parallelism": false,
"command": "./gradlew :list:dependencyInsight",
"metadata": {
"description": "Displays the insight into a specific dependency in project \u0027:list\u0027.",
"technologies": ["gradle"],
"help": {
"command": "./gradlew help --task :list:dependencyInsight"
}
},
"options": { "cwd": "." }
},
"dependencyReport": {
"cache": true,
"parallelism": false,
"outputs": ["{projectRoot}/build/reports/project/dependencies.txt"],
"command": "./gradlew :list:dependencyReport",
"metadata": {
"description": "Generates a report about your library dependencies.",
"technologies": ["gradle"],
"help": {
"command": "./gradlew help --task :list:dependencyReport"
}
},
"options": { "cwd": "." }
},
"dependentComponents": {
"cache": true,
"parallelism": false,
"command": "./gradlew :list:dependentComponents",
"metadata": {
"description": "Displays the dependent components of components in project \u0027:list\u0027. [deprecated]",
"technologies": ["gradle"],
"help": {
"command": "./gradlew help --task :list:dependentComponents"
}
},
"options": { "cwd": "." }
},
"help": {
"cache": true,
"parallelism": false,
"command": "./gradlew :list:help",
"metadata": {
"description": "Displays a help message.",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:help" }
},
"options": { "cwd": "." }
},
"htmlDependencyReport": {
"cache": true,
"parallelism": false,
"outputs": ["{projectRoot}/build/reports/project/dependencies"],
"command": "./gradlew :list:htmlDependencyReport",
"metadata": {
"description": "Generates an HTML report about your library dependencies.",
"technologies": ["gradle"],
"help": {
"command": "./gradlew help --task :list:htmlDependencyReport"
}
},
"options": { "cwd": "." }
},
"jar": {
"cache": true,
"parallelism": false,
"inputs": ["{projectRoot}/build/tmp/jar/MANIFEST.MF"],
"outputs": ["{projectRoot}/build/libs/list.jar"],
"dependsOn": ["list:classes", "list:compileJava"],
"command": "./gradlew :list:jar",
"metadata": {
"description": "Assembles a jar archive containing the classes of the \u0027main\u0027 feature.",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:jar" }
},
"options": { "cwd": "." }
},
"javaToolchains": {
"cache": true,
"parallelism": false,
"command": "./gradlew :list:javaToolchains",
"metadata": {
"description": "Displays the detected java toolchains.",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:javaToolchains" }
},
"options": { "cwd": "." }
},
"javadoc": {
"cache": true,
"parallelism": false,
"inputs": [
"{projectRoot}/src/main/java/org/example/list/LinkedList.java"
],
"outputs": ["{projectRoot}/build/docs/javadoc"],
"dependsOn": ["list:classes", "list:compileJava"],
"command": "./gradlew :list:javadoc",
"metadata": {
"description": "Generates Javadoc API documentation for the \u0027main\u0027 feature.",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:javadoc" }
},
"options": { "cwd": "." }
},
"kotlinDslAccessorsReport": {
"cache": true,
"parallelism": false,
"command": "./gradlew :list:kotlinDslAccessorsReport",
"metadata": {
"description": "Prints the Kotlin code for accessing the currently available project extensions and conventions.",
"technologies": ["gradle"],
"help": {
"command": "./gradlew help --task :list:kotlinDslAccessorsReport"
}
},
"options": { "cwd": "." }
},
"model": {
"cache": true,
"parallelism": false,
"command": "./gradlew :list:model",
"metadata": {
"description": "Displays the configuration model of project \u0027:list\u0027. [deprecated]",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:model" }
},
"options": { "cwd": "." }
},
"outgoingVariants": {
"cache": true,
"parallelism": false,
"command": "./gradlew :list:outgoingVariants",
"metadata": {
"description": "Displays the outgoing variants of project \u0027:list\u0027.",
"technologies": ["gradle"],
"help": {
"command": "./gradlew help --task :list:outgoingVariants"
}
},
"options": { "cwd": "." }
},
"processResources": {
"cache": true,
"parallelism": false,
"outputs": ["{projectRoot}/build/resources/main"],
"command": "./gradlew :list:processResources",
"metadata": {
"description": "Processes main resources.",
"technologies": ["gradle"],
"help": {
"command": "./gradlew help --task :list:processResources"
}
},
"options": { "cwd": "." }
},
"processTestResources": {
"cache": true,
"parallelism": false,
"outputs": ["{projectRoot}/build/resources/test"],
"command": "./gradlew :list:processTestResources",
"metadata": {
"description": "Processes test resources.",
"technologies": ["gradle"],
"help": {
"command": "./gradlew help --task :list:processTestResources"
}
},
"options": { "cwd": "." }
},
"projectReport": {
"cache": true,
"parallelism": false,
"dependsOn": [
"list:taskReport",
"list:dependencyReport",
"list:propertyReport",
"list:htmlDependencyReport"
],
"command": "./gradlew :list:projectReport",
"metadata": {
"description": "Generates a report about your project.",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:projectReport" }
},
"options": { "cwd": "." }
},
"projects": {
"cache": true,
"parallelism": false,
"command": "./gradlew :list:projects",
"metadata": {
"description": "Displays the sub-projects of project \u0027:list\u0027.",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:projects" }
},
"options": { "cwd": "." }
},
"properties": {
"cache": true,
"parallelism": false,
"command": "./gradlew :list:properties",
"metadata": {
"description": "Displays the properties of project \u0027:list\u0027.",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:properties" }
},
"options": { "cwd": "." }
},
"propertyReport": {
"cache": true,
"parallelism": false,
"outputs": ["{projectRoot}/build/reports/project/properties.txt"],
"command": "./gradlew :list:propertyReport",
"metadata": {
"description": "Generates a report about your properties.",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:propertyReport" }
},
"options": { "cwd": "." }
},
"resolvableConfigurations": {
"cache": true,
"parallelism": false,
"command": "./gradlew :list:resolvableConfigurations",
"metadata": {
"description": "Displays the configurations that can be resolved in project \u0027:list\u0027.",
"technologies": ["gradle"],
"help": {
"command": "./gradlew help --task :list:resolvableConfigurations"
}
},
"options": { "cwd": "." }
},
"taskReport": {
"cache": true,
"parallelism": false,
"outputs": ["{projectRoot}/build/reports/project/tasks.txt"],
"command": "./gradlew :list:taskReport",
"metadata": {
"description": "Generates a report about your tasks.",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:taskReport" }
},
"options": { "cwd": "." }
},
"tasks": {
"cache": true,
"parallelism": false,
"command": "./gradlew :list:tasks",
"metadata": {
"description": "Displays the tasks runnable from project \u0027:list\u0027.",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:tasks" }
},
"options": { "cwd": "." }
},
"test": {
"cache": true,
"parallelism": false,
"outputs": [
"{projectRoot}/build/test-results/test/binary",
"{projectRoot}/build/reports/tests/test",
"{projectRoot}/build/test-results/test"
],
"dependsOn": [
"list:compileTestJava",
"list:testClasses",
"list:classes",
"list:compileJava"
],
"command": "./gradlew :list:test",
"metadata": {
"description": "Runs the test suite.",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:test" }
},
"options": { "cwd": "." }
},
"testClasses": {
"cache": true,
"parallelism": false,
"dependsOn": ["list:processTestResources", "list:compileTestJava"],
"command": "./gradlew :list:testClasses",
"metadata": {
"description": "Assembles test classes.",
"technologies": ["gradle"],
"help": { "command": "./gradlew help --task :list:testClasses" }
},
"options": { "cwd": "." }
}
},
"metadata": {
"targetGroups": {
"build": [
"assemble",
"build",
"buildDependents",
"buildNeeded",
"classes",
"clean",
"jar",
"testClasses"
],
"help": [
"buildEnvironment",
"dependencies",
"dependencyInsight",
"help",
"javaToolchains",
"kotlinDslAccessorsReport",
"outgoingVariants",
"projects",
"properties",
"resolvableConfigurations",
"tasks"
],
"verification": [
"check",
"ci--LinkedListTest",
"ci--LinkedList2Test",
"ci",
"test"
],
"Nx Custom": ["createNodes", "createNodesLocal"],
"documentation": ["javadoc"],
"reporting": ["projectReport"]
},
"technologies": ["gradle"]
},
"name": "list"
}

View File

@ -0,0 +1,344 @@
{
"nodes": {
"proj": {
"targets": {
"buildEnvironment": {
"cache": true,
"metadata": {
"description": "Displays all buildscript dependencies declared in root project \u0027gradle-tutorial\u0027.",
"technologies": ["gradle"]
},
"command": "./gradlew :buildEnvironment",
"options": { "cwd": "proj" }
}
},
"metadata": {
"targetGroups": {
"help": ["buildEnvironment"]
},
"technologies": ["gradle"]
},
"name": "gradle-tutorial"
},
"proj/application": {
"targets": {
"ci--DemoApplicationTest10": {
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest10.java"
],
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin"
],
"cache": true,
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar"
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest10.java in CI",
"technologies": ["gradle"]
},
"command": "./gradlew :application:test --tests DemoApplicationTest10",
"options": { "cwd": "proj" }
},
"ci--DemoApplicationTest7": {
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest7.java"
],
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin"
],
"cache": true,
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar"
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest7.java in CI",
"technologies": ["gradle"]
},
"command": "./gradlew :application:test --tests DemoApplicationTest7",
"options": { "cwd": "proj" }
},
"ci--DemoApplicationTest6": {
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest6.java"
],
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin"
],
"cache": true,
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar"
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest6.java in CI",
"technologies": ["gradle"]
},
"command": "./gradlew :application:test --tests DemoApplicationTest6",
"options": { "cwd": "proj" }
},
"ci--DemoApplicationTest3": {
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest3.java"
],
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin"
],
"cache": true,
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar"
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest3.java in CI",
"technologies": ["gradle"]
},
"command": "./gradlew :application:test --tests DemoApplicationTest3",
"options": { "cwd": "proj" }
},
"ci--DemoApplicationTest2": {
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest2.java"
],
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin"
],
"cache": true,
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar"
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest2.java in CI",
"technologies": ["gradle"]
},
"command": "./gradlew :application:test --tests DemoApplicationTest2",
"options": { "cwd": "proj" }
},
"ci--DemoApplicationTest9": {
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest9.java"
],
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin"
],
"cache": true,
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar"
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest9.java in CI",
"technologies": ["gradle"]
},
"command": "./gradlew :application:test --tests DemoApplicationTest9",
"options": { "cwd": "proj" }
},
"ci--DemoApplicationTest": {
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest.java"
],
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin"
],
"cache": true,
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar"
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest.java in CI",
"technologies": ["gradle"]
},
"command": "./gradlew :application:test --tests DemoApplicationTest",
"options": { "cwd": "proj" }
},
"ci--DemoApplicationTest5": {
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest5.java"
],
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin"
],
"cache": true,
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar"
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest5.java in CI",
"technologies": ["gradle"]
},
"command": "./gradlew :application:test --tests DemoApplicationTest5",
"options": { "cwd": "proj" }
},
"ci--DemoApplicationTest4": {
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest4.java"
],
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin"
],
"cache": true,
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar"
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest4.java in CI",
"technologies": ["gradle"]
},
"command": "./gradlew :application:test --tests DemoApplicationTest4",
"options": { "cwd": "proj" }
},
"ci--DemoApplicationTest8": {
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest8.java"
],
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin"
],
"cache": true,
"dependsOn": [
"application:classes",
"application:compileJava",
"library:jar"
],
"metadata": {
"description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest8.java in CI",
"technologies": ["gradle"]
},
"command": "./gradlew :application:test --tests DemoApplicationTest8",
"options": { "cwd": "proj" }
},
"ci": {
"inputs": [
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest10.java",
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest7.java",
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest6.java",
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest3.java",
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest2.java",
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest9.java",
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest.java",
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest5.java",
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest4.java",
"{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest8.java"
],
"outputs": [
"{projectRoot}/build/classes/java/test",
"{projectRoot}/build/generated/sources/annotationProcessor/java/test",
"{projectRoot}/build/generated/sources/headers/java/test",
"{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin"
],
"cache": true,
"dependsOn": [
{
"target": "ci--DemoApplicationTest10",
"projects": "self",
"params": "forward"
},
{
"target": "ci--DemoApplicationTest7",
"projects": "self",
"params": "forward"
},
{
"target": "ci--DemoApplicationTest6",
"projects": "self",
"params": "forward"
},
{
"target": "ci--DemoApplicationTest3",
"projects": "self",
"params": "forward"
},
{
"target": "ci--DemoApplicationTest2",
"projects": "self",
"params": "forward"
},
{
"target": "ci--DemoApplicationTest9",
"projects": "self",
"params": "forward"
},
{
"target": "ci--DemoApplicationTest",
"projects": "self",
"params": "forward"
},
{
"target": "ci--DemoApplicationTest5",
"projects": "self",
"params": "forward"
},
{
"target": "ci--DemoApplicationTest4",
"projects": "self",
"params": "forward"
},
{
"target": "ci--DemoApplicationTest8",
"projects": "self",
"params": "forward"
}
],
"metadata": {
"description": "Runs Gradle Tests in CI",
"technologies": ["gradle"]
},
"options": { "cwd": "proj" },
"executor": "nx:noop"
}
},
"metadata": {
"targetGroups": {
"verification": ["ci"]
},
"technologies": ["gradle"]
},
"name": "application"
}
}
}

View File

@ -0,0 +1,203 @@
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import {
AggregateCreateNodesError,
hashArray,
ProjectConfiguration,
ProjectGraphExternalNode,
readJsonFile,
StaticDependency,
writeJsonFile,
} from '@nx/devkit';
import { hashWithWorkspaceContext } from 'nx/src/utils/workspace-context';
import { gradleConfigAndTestGlob } from '../../utils/split-config-files';
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
import { getNxProjectGraphLines } from './get-project-graph-lines';
import { GradlePluginOptions } from './gradle-plugin-options';
import { hashObject } from 'nx/src/devkit-internals';
// the output json file from the gradle plugin
export interface ProjectGraphReport {
nodes: {
[appRoot: string]: Partial<ProjectConfiguration>;
};
dependencies: Array<StaticDependency>;
externalNodes?: Record<string, ProjectGraphExternalNode>;
}
export interface ProjectGraphReportCache extends ProjectGraphReport {
hash: string;
}
function readProjectGraphReportCache(
cachePath: string,
hash: string
): ProjectGraphReport | undefined {
const projectGraphReportCache: Partial<ProjectGraphReportCache> = existsSync(
cachePath
)
? readJsonFile(cachePath)
: undefined;
if (!projectGraphReportCache || projectGraphReportCache.hash !== hash) {
return;
}
return projectGraphReportCache as ProjectGraphReport;
}
export function writeProjectGraphReportToCache(
cachePath: string,
results: ProjectGraphReport
) {
let projectGraphReportJson: ProjectGraphReportCache = {
hash: gradleCurrentConfigHash,
...results,
};
writeJsonFile(cachePath, projectGraphReportJson);
}
let projectGraphReportCache: ProjectGraphReport;
let gradleCurrentConfigHash: string;
let projectGraphReportCachePath: string = join(
workspaceDataDirectory,
'gradle-nodes.hash'
);
export function getCurrentProjectGraphReport(): ProjectGraphReport {
if (!projectGraphReportCache) {
throw new AggregateCreateNodesError(
[
[
null,
new Error(
`Expected cached gradle report. Please open an issue at https://github.com/nrwl/nx/issues/new/choose`
),
],
],
[]
);
}
return projectGraphReportCache;
}
/**
* This function populates the gradle report cache.
* For each gradlew file, it runs the `nxProjectGraph` task and processes the output.
* It will throw an error if both tasks fail.
* It will accumulate the output of all gradlew files.
* @param workspaceRoot
* @param gradlewFiles absolute paths to all gradlew files in the workspace
* @returns Promise<void>
*/
export async function populateProjectGraph(
workspaceRoot: string,
gradlewFiles: string[],
options: GradlePluginOptions
): Promise<void> {
const gradleConfigHash = hashArray([
await hashWithWorkspaceContext(workspaceRoot, [gradleConfigAndTestGlob]),
hashObject(options),
process.env.CI,
]);
projectGraphReportCache ??= readProjectGraphReportCache(
projectGraphReportCachePath,
gradleConfigHash
);
if (
projectGraphReportCache &&
(!gradleCurrentConfigHash || gradleConfigHash === gradleCurrentConfigHash)
) {
return;
}
const gradleProjectGraphReportStart = performance.mark(
'gradleProjectGraphReport:start'
);
const projectGraphLines = await gradlewFiles.reduce(
async (
projectGraphLines: Promise<string[]>,
gradlewFile: string
): Promise<string[]> => {
const getNxProjectGraphLinesStart = performance.mark(
`${gradlewFile}GetNxProjectGraphLines:start`
);
const allLines = await projectGraphLines;
const currentLines = await getNxProjectGraphLines(
gradlewFile,
gradleConfigHash,
options
);
const getNxProjectGraphLinesEnd = performance.mark(
`${gradlewFile}GetNxProjectGraphLines:end`
);
performance.measure(
`${gradlewFile}GetNxProjectGraphLines`,
getNxProjectGraphLinesStart.name,
getNxProjectGraphLinesEnd.name
);
return [...allLines, ...currentLines];
},
Promise.resolve([])
);
const gradleProjectGraphReportEnd = performance.mark(
'gradleProjectGraphReport:end'
);
performance.measure(
'gradleProjectGraphReport',
gradleProjectGraphReportStart.name,
gradleProjectGraphReportEnd.name
);
gradleCurrentConfigHash = gradleConfigHash;
projectGraphReportCache = processNxProjectGraph(projectGraphLines);
writeProjectGraphReportToCache(
projectGraphReportCachePath,
projectGraphReportCache
);
}
export function processNxProjectGraph(
projectGraphLines: string[]
): ProjectGraphReport {
let index = 0;
let projectGraphReportForAllProjects: ProjectGraphReport = {
nodes: {},
dependencies: [],
externalNodes: {},
};
while (index < projectGraphLines.length) {
const line = projectGraphLines[index].trim();
if (line.startsWith('> Task ') && line.endsWith(':nxProjectGraph')) {
while (
index < projectGraphLines.length &&
!projectGraphLines[index].includes('.json')
) {
index++;
}
const file = projectGraphLines[index];
const projectGraphReportJson: ProjectGraphReport =
readJsonFile<ProjectGraphReport>(file);
projectGraphReportForAllProjects.nodes = {
...projectGraphReportForAllProjects.nodes,
...projectGraphReportJson.nodes,
};
if (projectGraphReportJson.dependencies) {
projectGraphReportForAllProjects.dependencies.push(
...projectGraphReportJson.dependencies
);
}
if (Object.keys(projectGraphReportJson.externalNodes ?? {}).length > 0) {
projectGraphReportForAllProjects.externalNodes = {
...projectGraphReportForAllProjects.externalNodes,
...projectGraphReportJson.externalNodes,
};
}
}
index++;
}
return projectGraphReportForAllProjects;
}

View File

@ -0,0 +1,90 @@
import { AggregateCreateNodesError, output, workspaceRoot } from '@nx/devkit';
import { execGradleAsync, newLineSeparator } from '../../utils/exec-gradle';
import { GradlePluginOptions } from './gradle-plugin-options';
import { dirname } from 'node:path';
export async function getNxProjectGraphLines(
gradlewFile: string,
gradleConfigHash: string,
gradlePluginOptions: GradlePluginOptions
): Promise<string[]> {
if (process.env.VERCEL) {
// skip on Vercel
return [];
}
let nxProjectGraphBuffer: Buffer;
const gradlePluginOptionsArgs =
Object.entries(gradlePluginOptions ?? {})?.map(
([key, value]) => `-P${key}=${value}`
) ?? [];
try {
nxProjectGraphBuffer = await execGradleAsync(gradlewFile, [
'nxProjectGraph',
`-Phash=${gradleConfigHash}`,
'--no-configuration-cache', // disable configuration cache
'--parallel', // add parallel to improve performance
'--build-cache', // enable build cache
'--warning-mode',
'none',
...gradlePluginOptionsArgs,
`-Pcwd=${dirname(gradlewFile)}`,
`-PworkspaceRoot=${workspaceRoot}`,
process.env.NX_VERBOSE_LOGGING ? '--info' : '',
]);
} catch (e: Buffer | Error | any) {
if (e.toString()?.includes('ERROR: JAVA_HOME')) {
throw new AggregateCreateNodesError(
[
[
gradlewFile,
new Error(
`Could not find Java. Please install Java and try again: https://www.java.com/en/download/help/index_installing.html.\n\r${e.toString()}`
),
],
],
[]
);
} else if (e.toString()?.includes(`Task 'nxProjectGraph' not found`)) {
throw new AggregateCreateNodesError(
[
[
gradlewFile,
new Error(
`Could not run 'nxProjectGraph' task. Please run 'nx generate @nx/gradle:init' to generate the necessary tasks.\n\r${e.toString()}`
),
],
],
[]
);
} else {
throw new AggregateCreateNodesError(
[
[
gradlewFile,
new Error(
`Could not run 'nxProjectGraph' Gradle task. Please install Gradle and try again: https://gradle.org/install/.\r\n${e.toString()}`
),
],
],
[]
);
}
}
const projectGraphLines = nxProjectGraphBuffer
.toString()
.split(newLineSeparator)
.filter((line) => line.trim() !== '');
if (process.env.NX_VERBOSE_LOGGING === 'true') {
output.log({
title: `Successfully ran 'nxProjectGraph' task using ${gradlewFile} with hash ${gradleConfigHash}`,
bodyLines: projectGraphLines,
});
}
return projectGraphLines;
}

View File

@ -0,0 +1,13 @@
export interface GradlePluginOptions {
testTargetName?: string;
ciTargetName?: string;
[taskTargetName: string]: string | undefined | boolean;
}
export function normalizeOptions(
options: GradlePluginOptions
): GradlePluginOptions {
options ??= {};
options.testTargetName ??= 'test';
return options;
}

View File

@ -1,8 +1,8 @@
import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; import { TempFs } from 'nx/src/internal-testing-utils/temp-fs';
import { findGraldewFile } from './exec-gradle'; import { findGradlewFile } from './exec-gradle';
describe('exec gradle', () => { describe('exec gradle', () => {
describe('findGraldewFile', () => { describe('findGradlewFile', () => {
let tempFs: TempFs; let tempFs: TempFs;
let cwd: string; let cwd: string;
@ -27,14 +27,14 @@ describe('exec gradle', () => {
'nested/nested/proj/src/test/java/test/aTest.java': ``, 'nested/nested/proj/src/test/java/test/aTest.java': ``,
'nested/nested/proj/src/test/java/test/bTest.java': ``, 'nested/nested/proj/src/test/java/test/bTest.java': ``,
}); });
let gradlewFile = findGraldewFile('proj/build.gradle', tempFs.tempDir); let gradlewFile = findGradlewFile('proj/build.gradle', tempFs.tempDir);
expect(gradlewFile).toEqual('gradlew'); expect(gradlewFile).toEqual('gradlew');
gradlewFile = findGraldewFile( gradlewFile = findGradlewFile(
'nested/nested/proj/build.gradle', 'nested/nested/proj/build.gradle',
tempFs.tempDir tempFs.tempDir
); );
expect(gradlewFile).toEqual('gradlew'); expect(gradlewFile).toEqual('gradlew');
gradlewFile = findGraldewFile( gradlewFile = findGradlewFile(
'nested/nested/proj/settings.gradle', 'nested/nested/proj/settings.gradle',
tempFs.tempDir tempFs.tempDir
); );
@ -54,16 +54,16 @@ describe('exec gradle', () => {
'nested/nested/proj/src/test/java/test/bTest.java': ``, 'nested/nested/proj/src/test/java/test/bTest.java': ``,
}); });
let gradlewFile = findGraldewFile('proj/build.gradle', tempFs.tempDir); let gradlewFile = findGradlewFile('proj/build.gradle', tempFs.tempDir);
expect(gradlewFile).toEqual('proj/gradlew'); expect(gradlewFile).toEqual('proj/gradlew');
gradlewFile = findGraldewFile('proj/settings.gradle', tempFs.tempDir); gradlewFile = findGradlewFile('proj/settings.gradle', tempFs.tempDir);
expect(gradlewFile).toEqual('proj/gradlew'); expect(gradlewFile).toEqual('proj/gradlew');
gradlewFile = findGraldewFile( gradlewFile = findGradlewFile(
'nested/nested/proj/build.gradle', 'nested/nested/proj/build.gradle',
tempFs.tempDir tempFs.tempDir
); );
expect(gradlewFile).toEqual('nested/nested/proj/gradlew'); expect(gradlewFile).toEqual('nested/nested/proj/gradlew');
gradlewFile = findGraldewFile( gradlewFile = findGradlewFile(
'nested/nested/proj/settings.gradle', 'nested/nested/proj/settings.gradle',
tempFs.tempDir tempFs.tempDir
); );
@ -80,7 +80,7 @@ describe('exec gradle', () => {
'nested/nested/proj/src/test/java/test/bTest.java': ``, 'nested/nested/proj/src/test/java/test/bTest.java': ``,
}); });
expect(() => expect(() =>
findGraldewFile('proj/build.gradle', tempFs.tempDir) findGradlewFile('proj/build.gradle', tempFs.tempDir)
).toThrow(); ).toThrow();
}); });
}); });

View File

@ -4,6 +4,14 @@ import { existsSync } from 'node:fs';
import { dirname, join } from 'node:path'; import { dirname, join } from 'node:path';
import { LARGE_BUFFER } from 'nx/src/executors/run-commands/run-commands.impl'; import { LARGE_BUFFER } from 'nx/src/executors/run-commands/run-commands.impl';
export const fileSeparator = process.platform.startsWith('win')
? 'file:///'
: 'file://';
export const newLineSeparator = process.platform.startsWith('win')
? '\r\n'
: '\n';
/** /**
* For gradle command, it needs to be run from the directory of the gradle binary * For gradle command, it needs to be run from the directory of the gradle binary
* @returns gradle binary file name * @returns gradle binary file name
@ -54,13 +62,13 @@ export function execGradleAsync(
/** /**
* This function recursively finds the nearest gradlew file in the workspace * This function recursively finds the nearest gradlew file in the workspace
* @param originalFileToSearch the original file to search for * @param originalFileToSearch the original file to search for, relative to workspace root, file path not directory path
* @param wr workspace root * @param wr workspace root
* @param currentSearchPath the path to start searching for gradlew file * @param currentSearchPath the path to start searching for gradlew file
* @returns the relative path of the gradlew file to workspace root, throws an error if gradlew file is not found * @returns the relative path of the gradlew file to workspace root, throws an error if gradlew file is not found
* It will return gradlew.bat file on windows and gradlew file on other platforms * It will return relative path to workspace root of gradlew.bat file on windows and gradlew file on other platforms
*/ */
export function findGraldewFile( export function findGradlewFile(
originalFileToSearch: string, originalFileToSearch: string,
wr: string = workspaceRoot, wr: string = workspaceRoot,
currentSearchPath?: string currentSearchPath?: string
@ -92,5 +100,5 @@ export function findGraldewFile(
} }
} }
return findGraldewFile(originalFileToSearch, wr, parent); return findGradlewFile(originalFileToSearch, wr, parent);
} }

View File

@ -8,6 +8,8 @@ export const GRADLE_TEST_FILES = [
'**/src/test/kotlin/**/*Test.kt', '**/src/test/kotlin/**/*Test.kt',
'**/src/test/java/**/*Tests.java', '**/src/test/java/**/*Tests.java',
'**/src/test/kotlin/**/*Tests.kt', '**/src/test/kotlin/**/*Tests.kt',
'**/src/test/groovy/**/*Test.groovy',
'**/src/test/groovy/**/*Tests.groovy',
]; ];
export const gradleConfigGlob = combineGlobPatterns( export const gradleConfigGlob = combineGlobPatterns(

View File

@ -1 +1,4 @@
export const nxVersion = require('../../package.json').version; export const nxVersion = require('../../package.json').version;
export const gradleProjectGraphPluginName = 'dev.nx.gradle.project-graph';
export const gradleProjectGraphVersion = '0.0.2';

876
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

18
settings.gradle.kts Normal file
View File

@ -0,0 +1,18 @@
/*
* This file was generated by the Gradle 'init' task.
*
* The settings file is used to specify which projects to include in your build.
* For more detailed information on multi-project builds, please refer to https://docs.gradle.org/8.5/userguide/building_swift_projects.html in the Gradle documentation.
*/
pluginManagement {
repositories {
mavenLocal()
mavenCentral()
gradlePluginPortal()
}
}
rootProject.name = "nx"
includeBuild("./packages/gradle/project-graph")