feat(gradle): gradle atomizer (#26663)

<!-- 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 -->

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

## 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 2024-07-05 12:22:37 -07:00 committed by GitHub
parent e15479b691
commit 62baf4f307
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 411 additions and 71 deletions

View File

@ -0,0 +1,10 @@
/* eslint-disable */
export default {
displayName: 'graph-ui-project-details',
preset: '../../jest.preset.js',
transform: {
'^.+\\.[tj]sx?$': 'babel-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/graph/graph-ui-project-details',
};

View File

@ -13,6 +13,9 @@
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
},
{
"path": "./tsconfig.storybook.json"
}

View File

@ -0,0 +1,20 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
]
}

View File

@ -6,5 +6,5 @@ export default {
'^.+\\.[tj]sx?$': 'babel-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/graph/ui-graph',
coverageDirectory: '../../coverage/graph/graph-ui-tooltips',
};

View File

@ -10,6 +10,7 @@ import { readFileSync } from 'node:fs';
import { basename } from 'node:path';
import {
GRADLE_BUILD_FILES,
getCurrentGradleReport,
newLineSeparator,
} from '../utils/get-gradle-report';
@ -58,14 +59,12 @@ export const createDependencies: CreateDependencies = async (
return Array.from(dependencies);
};
const gradleConfigFileNames = new Set(['build.gradle', 'build.gradle.kts']);
function findGradleFiles(fileMap: FileMap): string[] {
const gradleFiles: string[] = [];
for (const [_, files] of Object.entries(fileMap.projectFileMap)) {
for (const file of files) {
if (gradleConfigFileNames.has(basename(file.file))) {
if (GRADLE_BUILD_FILES.has(basename(file.file))) {
gradleFiles.push(file.file);
}
}

View File

@ -4,8 +4,9 @@ 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.ts', () => {
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),
};
@ -23,14 +24,14 @@ describe('@nx/gradle/plugin', () => {
tempFs = new TempFs('test');
gradleReport = {
gradleFileToGradleProjectMap: new Map<string, string>([
['proj/gradle.build', 'proj'],
['proj/build.gradle', 'proj'],
]),
buildFileToDepsMap: new Map<string, string>(),
gradleFileToOutputDirsMap: new Map<string, Map<string, string>>([
['proj/gradle.build', new Map([['build', 'build']])],
['proj/build.gradle', new Map([['build', 'build']])],
]),
gradleProjectToTasksTypeMap: new Map<string, Map<string, string>>([
['proj', new Map([['test', 'Test']])],
['proj', new Map([['test', 'Verification']])],
]),
gradleProjectToProjectName: new Map<string, string>([['proj', 'proj']]),
};
@ -48,7 +49,7 @@ describe('@nx/gradle/plugin', () => {
};
await tempFs.createFiles({
'proj/gradle.build': ``,
'proj/build.gradle': ``,
gradlew: '',
});
});
@ -60,7 +61,7 @@ describe('@nx/gradle/plugin', () => {
it('should create nodes based on gradle', async () => {
const results = await createNodesFunction(
['proj/gradle.build'],
['proj/build.gradle'],
{
buildTargetName: 'build',
},
@ -70,13 +71,13 @@ describe('@nx/gradle/plugin', () => {
expect(results).toMatchInlineSnapshot(`
[
[
"proj/gradle.build",
"proj/build.gradle",
{
"projects": {
"proj": {
"metadata": {
"targetGroups": {
"Test": [
"Verification": [
"test",
],
},
@ -87,7 +88,7 @@ describe('@nx/gradle/plugin', () => {
"name": "proj",
"targets": {
"test": {
"cache": false,
"cache": true,
"command": "./gradlew proj:test",
"dependsOn": [
"classes",
@ -114,23 +115,23 @@ describe('@nx/gradle/plugin', () => {
it('should create nodes based on gradle for nested project root', async () => {
gradleReport = {
gradleFileToGradleProjectMap: new Map<string, string>([
['nested/nested/proj/gradle.build', 'proj'],
['nested/nested/proj/build.gradle', 'proj'],
]),
buildFileToDepsMap: new Map<string, string>(),
gradleFileToOutputDirsMap: new Map<string, Map<string, string>>([
['nested/nested/proj/gradle.build', new Map([['build', 'build']])],
['nested/nested/proj/build.gradle', new Map([['build', 'build']])],
]),
gradleProjectToTasksTypeMap: new Map<string, Map<string, string>>([
['proj', new Map([['test', 'Test']])],
['proj', new Map([['test', 'Verification']])],
]),
gradleProjectToProjectName: new Map<string, string>([['proj', 'proj']]),
};
await tempFs.createFiles({
'nested/nested/proj/gradle.build': ``,
'nested/nested/proj/build.gradle': ``,
});
const results = await createNodesFunction(
['nested/nested/proj/gradle.build'],
['nested/nested/proj/build.gradle'],
{
buildTargetName: 'build',
},
@ -140,13 +141,101 @@ describe('@nx/gradle/plugin', () => {
expect(results).toMatchInlineSnapshot(`
[
[
"nested/nested/proj/gradle.build",
"nested/nested/proj/build.gradle",
{
"projects": {
"nested/nested/proj": {
"metadata": {
"targetGroups": {
"Verification": [
"test",
],
},
"technologies": [
"gradle",
],
},
"name": "proj",
"targets": {
"test": {
"cache": true,
"command": "./gradlew proj:test",
"dependsOn": [
"classes",
],
"inputs": [
"default",
"^production",
],
"metadata": {
"technologies": [
"gradle",
],
},
},
},
},
},
},
],
]
`);
});
it('should create nodes with atomized tests targets based on gradle for nested project root', async () => {
gradleReport = {
gradleFileToGradleProjectMap: new Map<string, string>([
['nested/nested/proj/build.gradle', 'proj'],
]),
buildFileToDepsMap: new Map<string, string>(),
gradleFileToOutputDirsMap: new Map<string, Map<string, string>>([
['nested/nested/proj/build.gradle', new Map([['build', 'build']])],
]),
gradleProjectToTasksTypeMap: new Map<string, Map<string, string>>([
['proj', new Map([['test', 'Test']])],
]),
gradleProjectToProjectName: new Map<string, string>([['proj', 'proj']]),
};
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/test.java': ``,
});
await tempFs.createFiles({
'nested/nested/proj/src/test/java/test/test1.java': ``,
});
const results = await createNodesFunction(
[
'nested/nested/proj/build.gradle',
'proj/src/test/java/test/rootTest.java',
'nested/nested/proj/src/test/java/test/test.java',
'nested/nested/proj/src/test/java/test/test1.java',
],
{
buildTargetName: 'build',
ciTargetName: 'test-ci',
},
context
);
expect(results).toMatchInlineSnapshot(`
[
[
"nested/nested/proj/build.gradle",
{
"projects": {
"nested/nested/proj": {
"metadata": {
"targetGroups": {
"Test": [
"test-ci--test",
"test-ci--test1",
"test-ci",
"test",
],
},
@ -172,6 +261,67 @@ describe('@nx/gradle/plugin', () => {
],
},
},
"test-ci": {
"cache": true,
"dependsOn": [
{
"params": "forward",
"projects": "self",
"target": "test-ci--test",
},
{
"params": "forward",
"projects": "self",
"target": "test-ci--test1",
},
],
"executor": "nx:noop",
"inputs": [
"default",
"^production",
],
"metadata": {
"description": "Runs Gradle Tests in CI",
"nonAtomizedTarget": "test",
"technologies": [
"gradle",
],
},
},
"test-ci--test": {
"cache": true,
"command": "./gradlew proj:test --tests test",
"dependsOn": [
"classes",
],
"inputs": [
"default",
"^production",
],
"metadata": {
"description": "Runs Gradle test nested/nested/proj/src/test/java/test/test.java in CI",
"technologies": [
"gradle",
],
},
},
"test-ci--test1": {
"cache": true,
"command": "./gradlew proj:test --tests test1",
"dependsOn": [
"classes",
],
"inputs": [
"default",
"^production",
],
"metadata": {
"description": "Runs Gradle test nested/nested/proj/src/test/java/test/test1.java in CI",
"technologies": [
"gradle",
],
},
},
},
},
},

View File

@ -2,7 +2,6 @@ import {
CreateNodes,
CreateNodesV2,
CreateNodesContext,
CreateNodesContextV2,
ProjectConfiguration,
TargetConfiguration,
createNodesFromFiles,
@ -13,8 +12,9 @@ import {
} from '@nx/devkit';
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
import { existsSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { basename, dirname, join } from 'node:path';
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
import { findProjectForPath } from 'nx/src/devkit-internals';
import { getGradleExecFile } from '../utils/exec-gradle';
import {
@ -22,6 +22,8 @@ import {
getCurrentGradleReport,
GradleReport,
gradleConfigGlob,
GRADLE_BUILD_FILES,
gradleConfigAndTestGlob,
} from '../utils/get-gradle-report';
import { hashObject } from 'nx/src/hasher/file-hasher';
@ -38,12 +40,21 @@ interface GradleTask {
}
export interface GradlePluginOptions {
ciTargetName?: string;
testTargetName?: string;
classesTargetName?: string;
buildTargetName?: string;
[taskTargetName: string]: string | undefined;
}
function normalizeOptions(options: GradlePluginOptions): GradlePluginOptions {
options ??= {};
options.testTargetName ??= 'test';
options.classesTargetName ??= 'classes';
options.buildTargetName ??= 'build';
return options;
}
type GradleTargets = Record<
string,
{
@ -62,8 +73,9 @@ export function writeTargetsToCache(cachePath: string, results: GradleTargets) {
}
export const createNodesV2: CreateNodesV2<GradlePluginOptions> = [
gradleConfigGlob,
async (configFiles, options, context) => {
gradleConfigAndTestGlob,
async (files, options, context) => {
const { configFiles, projectRoots, testFiles } = splitConfigFiles(files);
const optionsHash = hashObject(options);
const cachePath = join(
workspaceDataDirectory,
@ -73,10 +85,18 @@ export const createNodesV2: CreateNodesV2<GradlePluginOptions> = [
await populateGradleReport(context.workspaceRoot);
const gradleReport = getCurrentGradleReport();
const gradleProjectRootToTestFilesMap = getGradleProjectRootToTestFilesMap(
testFiles,
projectRoots
);
try {
return await createNodesFromFiles(
makeCreateNodes(gradleReport, targetsCache),
return createNodesFromFiles(
makeCreateNodesForGradleConfigFile(
gradleReport,
targetsCache,
gradleProjectRootToTestFilesMap
),
configFiles,
options,
context
@ -87,10 +107,11 @@ export const createNodesV2: CreateNodesV2<GradlePluginOptions> = [
},
];
export const makeCreateNodes =
export const makeCreateNodesForGradleConfigFile =
(
gradleReport: GradleReport,
targetsCache: GradleTargets
targetsCache: GradleTargets = {},
gradleProjectRootToTestFilesMap: Record<string, string[]> = {}
): CreateNodesFunction =>
async (
gradleFilePath,
@ -98,17 +119,19 @@ export const makeCreateNodes =
context: CreateNodesContext
) => {
const projectRoot = dirname(gradleFilePath);
options = normalizeOptions(options);
const hash = await calculateHashForCreateNodes(
projectRoot,
options ?? {},
context
);
targetsCache[hash] ??= createGradleProject(
targetsCache[hash] ??= await createGradleProject(
gradleReport,
gradleFilePath,
options,
context
context,
gradleProjectRootToTestFilesMap[projectRoot]
);
const project = targetsCache[hash];
if (!project) {
@ -133,16 +156,18 @@ export const createNodes: CreateNodes<GradlePluginOptions> = [
);
await populateGradleReport(context.workspaceRoot);
const gradleReport = getCurrentGradleReport();
const internalCreateNodes = makeCreateNodes(gradleReport, {});
const internalCreateNodes =
makeCreateNodesForGradleConfigFile(gradleReport);
return await internalCreateNodes(configFile, options, context);
},
];
function createGradleProject(
async function createGradleProject(
gradleReport: GradleReport,
gradleFilePath: string,
options: GradlePluginOptions | undefined,
context: CreateNodesContext
context: CreateNodesContext,
testFiles = []
) {
try {
const {
@ -177,12 +202,14 @@ function createGradleProject(
string
>;
const { targets, targetGroups } = createGradleTargets(
const { targets, targetGroups } = await createGradleTargets(
tasks,
options,
context,
outputDirs,
gradleProject
gradleProject,
gradleFilePath,
testFiles
);
const project = {
name: projectName,
@ -200,16 +227,18 @@ function createGradleProject(
}
}
function createGradleTargets(
async function createGradleTargets(
tasks: GradleTask[],
options: GradlePluginOptions | undefined,
context: CreateNodesContext,
outputDirs: Map<string, string>,
gradleProject: string
): {
gradleProject: string,
gradleFilePath: string,
testFiles: string[] = []
): Promise<{
targetGroups: Record<string, string[]>;
targets: Record<string, TargetConfiguration>;
} {
}> {
const inputsMap = createInputsMap(context);
const targets: Record<string, TargetConfiguration> = {};
@ -217,28 +246,43 @@ function createGradleTargets(
for (const task of tasks) {
const targetName = options?.[`${task.name}TargetName`] ?? task.name;
const outputs = outputDirs.get(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,
options.ciTargetName,
inputsMap['test'],
outputs,
task.type,
targets,
targetGroups
);
}
const taskCommandToRun = `${gradleProject ? gradleProject + ':' : ''}${
task.name
}`;
targets[targetName] = {
command: `${getGradleExecFile()} ${
gradleProject ? gradleProject + ':' : ''
}${task.name}`,
command: `${getGradleExecFile()} ${taskCommandToRun}`,
cache: cacheableTaskType.has(task.type),
inputs: inputsMap[task.name],
dependsOn: dependsOnMap[task.name],
metadata: {
technologies: ['gradle'],
},
...(outputs && outputs.length ? { outputs } : {}),
};
if (outputs) {
targets[targetName].outputs = [outputs];
}
if (!targetGroups[task.type]) {
targetGroups[task.type] = [];
}
targetGroups[task.type].push(task.name);
targetGroups[task.type].push(targetName);
}
return { targetGroups, targets };
}
@ -257,3 +301,103 @@ function createInputsMap(
: ['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[]>
): 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}`,
cache: true,
inputs,
dependsOn: dependsOnMap['test'],
metadata: {
technologies: ['gradle'],
description: `Runs Gradle test ${testFile} in CI`,
},
...(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,
},
};
targetGroups[targetGroupName].push(ciTargetName);
}
function splitConfigFiles(files: readonly string[]): {
configFiles: string[];
testFiles: string[];
projectRoots: string[];
} {
const configFiles = [];
const testFiles = [];
const projectRoots = new Set<string>();
files.forEach((file) => {
if (GRADLE_BUILD_FILES.has(basename(file))) {
configFiles.push(file);
projectRoots.add(dirname(file));
} else {
testFiles.push(file);
}
});
return { configFiles, testFiles, projectRoots: Array.from(projectRoots) };
}
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

@ -7,6 +7,7 @@ import {
normalizePath,
workspaceRoot,
} from '@nx/devkit';
import { combineGlobPatterns } from 'nx/src/utils/globs';
import { execGradleAsync } from './exec-gradle';
import { hashWithWorkspaceContext } from 'nx/src/utils/workspace-context';
@ -30,7 +31,20 @@ export interface GradleReport {
let gradleReportCache: GradleReport;
let gradleCurrentConfigHash: string;
export const gradleConfigGlob = '**/build.{gradle.kts,gradle}';
export const GRADLE_BUILD_FILES = new Set(['build.gradle', 'build.gradle.kts']);
export const GRADLE_TEST_FILES = [
'**/src/test/java/**/*.java',
'**/src/test/kotlin/**/*.kt',
];
export const gradleConfigGlob = combineGlobPatterns(
...Array.from(GRADLE_BUILD_FILES).map((file) => `**/${file}`)
);
export const gradleConfigAndTestGlob = combineGlobPatterns(
...Array.from(GRADLE_BUILD_FILES).map((file) => `**/${file}`),
...GRADLE_TEST_FILES
);
export function getCurrentGradleReport() {
if (!gradleReportCache) {