From 6d83dd7ff068caecb8bc1dc630ae5f2578b683c8 Mon Sep 17 00:00:00 2001 From: Emily Xiong Date: Tue, 19 Mar 2024 14:38:15 -0400 Subject: [PATCH] feat(gradle): add gradle init generator (#22245) --- e2e/gradle/src/gradle.test.ts | 123 ++++++++++-------- packages/gradle/generators.json | 11 ++ packages/gradle/index.ts | 1 + packages/gradle/package.json | 8 ++ .../gradle/src/generators/init/init.spec.ts | 73 +++++++++++ packages/gradle/src/generators/init/init.ts | 102 +++++++++++++++ .../gradle/src/generators/init/schema.d.ts | 6 + .../gradle/src/generators/init/schema.json | 34 +++++ packages/gradle/src/plugin/dependencies.ts | 25 ++-- .../gradle/src/utils/has-gradle-plugin.ts | 10 ++ packages/gradle/src/utils/versions.ts | 1 + packages/nx/src/command-line/add/add.ts | 2 +- packages/nx/src/command-line/init/init-v2.ts | 51 +++++--- scripts/commitizen.js | 1 + 14 files changed, 359 insertions(+), 89 deletions(-) create mode 100644 packages/gradle/generators.json create mode 100644 packages/gradle/src/generators/init/init.spec.ts create mode 100644 packages/gradle/src/generators/init/init.ts create mode 100644 packages/gradle/src/generators/init/schema.d.ts create mode 100644 packages/gradle/src/generators/init/schema.json create mode 100644 packages/gradle/src/utils/has-gradle-plugin.ts create mode 100644 packages/gradle/src/utils/versions.ts diff --git a/e2e/gradle/src/gradle.test.ts b/e2e/gradle/src/gradle.test.ts index 8611a082028a4..b128010e9410a 100644 --- a/e2e/gradle/src/gradle.test.ts +++ b/e2e/gradle/src/gradle.test.ts @@ -8,80 +8,89 @@ import { runCommand, uniq, updateFile, - updateJson, } from '@nx/e2e/utils'; import { execSync } from 'child_process'; describe('Gradle', () => { - let gradleProjectName = uniq('my-gradle-project'); + describe.each([{ type: 'kotlin' }, { type: 'groovy' }])( + '$type', + ({ type }: { type: 'kotlin' | 'groovy' }) => { + let gradleProjectName = uniq('my-gradle-project'); + beforeAll(() => { + newProject(); + createGradleProject(gradleProjectName, type); + }); + afterAll(() => cleanupProject()); - beforeAll(() => { - newProject(); - createGradleProject(gradleProjectName); - }); - 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); - 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 }); + // app depends on list and utilities + expect(buildOutput).toContain('nx run list:build'); + expect(buildOutput).toContain('nx run utilities:build'); - const buildOutput = runCLI('build app', { verbose: true }); - // app depends on list and utilities - expect(buildOutput).toContain('nx run list:build'); - expect(buildOutput).toContain('nx run utilities:build'); + checkFilesExist( + `app/build/libs/app.jar`, + `list/build/libs/list.jar`, + `utilities/build/libs/utilities.jar` + ); + }); - 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 'gradleProject.groovy-application-conventions' +} - it('should track dependencies for new app', () => { - createFile( - 'app2/build.gradle.kts', - ` - plugins { - id("gradleProject.kotlin-application-conventions") - } - - dependencies { - implementation(project(":app")) +dependencies { + implementation project(':app') +}` + ); + } else { + createFile( + `app2/build.gradle.kts`, + `plugins { + id("gradleProject.kotlin-library-conventions") +} + +dependencies { + implementation(project(":app")) +}` + ); + } + updateFile( + `settings.gradle${type === 'kotlin' ? '.kts' : ''}`, + (content) => { + content += `\r\ninclude("app2")`; + return content; + } + ); + const buildOutput = runCLI('build app2', { verbose: true }); + // app2 depends on app + expect(buildOutput).toContain('nx run app:build'); + }); } - ` - ); - updateFile(`settings.gradle.kts`, (content) => { - content += `\r\ninclude("app2")`; - return content; - }); - const buildOutput = runCLI('build app2', { verbose: true }); - // app2 depends on app - expect(buildOutput).toContain('nx run app:build'); - }); + ); }); -function createGradleProject(projectName: string) { +function createGradleProject( + projectName: string, + type: 'kotlin' | 'groovy' = 'kotlin' +) { e2eConsoleLogger(`Using java version: ${execSync('java --version')}`); e2eConsoleLogger(`Using gradle version: ${execSync('gradle --version')}`); e2eConsoleLogger(execSync(`gradle help --task :init`).toString()); e2eConsoleLogger( runCommand( - `gradle init --type kotlin-application --dsl kotlin --project-name ${projectName} --package gradleProject --no-incubating --split-project` + `gradle init --type ${type}-application --dsl ${type} --project-name ${projectName} --package gradleProject --no-incubating --split-project` ) ); - updateJson('nx.json', (nxJson) => { - nxJson.plugins = ['@nx/gradle']; - return nxJson; - }); - createFile( - 'build.gradle.kts', - `allprojects { - apply { - plugin("project-report") - } - }` - ); + runCLI(`add @nx/gradle`); } diff --git a/packages/gradle/generators.json b/packages/gradle/generators.json new file mode 100644 index 0000000000000..fd37905102647 --- /dev/null +++ b/packages/gradle/generators.json @@ -0,0 +1,11 @@ +{ + "name": "Nx Gradle", + "version": "0.1", + "generators": { + "init": { + "factory": "./src/generators/init/init#initGenerator", + "schema": "./src/generators/init/schema.json", + "description": "Initializes a Gradle project in the current workspace" + } + } +} diff --git a/packages/gradle/index.ts b/packages/gradle/index.ts index 1110b6451f6a1..3d6aa60a5de9f 100644 --- a/packages/gradle/index.ts +++ b/packages/gradle/index.ts @@ -1 +1,2 @@ export * from './plugin'; +export { initGenerator } from './src/generators/init/init'; diff --git a/packages/gradle/package.json b/packages/gradle/package.json index 666aba21829cf..dddc6bad359df 100644 --- a/packages/gradle/package.json +++ b/packages/gradle/package.json @@ -22,6 +22,14 @@ "url": "https://github.com/nrwl/nx/issues" }, "homepage": "https://nx.dev", + "generators": "./generators.json", + "exports": { + ".": "./index.js", + "./package.json": "./package.json", + "./migrations.json": "./migrations.json", + "./generators.json": "./generators.json", + "./plugin": "./plugin.js" + }, "nx-migrate": { "migrations": "./migrations.json" }, diff --git a/packages/gradle/src/generators/init/init.spec.ts b/packages/gradle/src/generators/init/init.spec.ts new file mode 100644 index 0000000000000..cfcc61fc862cf --- /dev/null +++ b/packages/gradle/src/generators/init/init.spec.ts @@ -0,0 +1,73 @@ +import { readNxJson, Tree, updateNxJson } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; + +import { initGenerator } from './init'; + +describe('@nx/gradle:init', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + tree.write('settings.gradle', ''); + }); + + it('should add the plugin', async () => { + await initGenerator(tree, { + skipFormat: true, + skipPackageJson: false, + }); + const nxJson = readNxJson(tree); + expect(nxJson.plugins).toMatchInlineSnapshot(` + [ + { + "options": { + "buildTargetName": "build", + "classesTargetName": "classes", + "testTargetName": "test", + }, + "plugin": "@nx/gradle/plugin", + }, + ] + `); + }); + + it('should not overwrite existing plugins', async () => { + updateNxJson(tree, { + plugins: ['foo'], + }); + await initGenerator(tree, { + skipFormat: true, + skipPackageJson: false, + }); + const nxJson = readNxJson(tree); + expect(nxJson.plugins).toMatchInlineSnapshot(` + [ + "foo", + { + "options": { + "buildTargetName": "build", + "classesTargetName": "classes", + "testTargetName": "test", + }, + "plugin": "@nx/gradle/plugin", + }, + ] + `); + }); + + it('should not add plugin if already in array', async () => { + updateNxJson(tree, { + plugins: ['@nx/gradle/plugin'], + }); + await initGenerator(tree, { + skipFormat: true, + skipPackageJson: false, + }); + const nxJson = readNxJson(tree); + expect(nxJson.plugins).toMatchInlineSnapshot(` + [ + "@nx/gradle/plugin", + ] + `); + }); +}); diff --git a/packages/gradle/src/generators/init/init.ts b/packages/gradle/src/generators/init/init.ts new file mode 100644 index 0000000000000..380c9020aac01 --- /dev/null +++ b/packages/gradle/src/generators/init/init.ts @@ -0,0 +1,102 @@ +import { + addDependenciesToPackageJson, + formatFiles, + GeneratorCallback, + logger, + readNxJson, + runTasksInSerial, + Tree, + updateNxJson, +} from '@nx/devkit'; +import { updatePackageScripts } from '@nx/devkit/src/utils/update-package-scripts'; +import { createNodes } from '../../plugin/nodes'; +import { nxVersion } from '../../utils/versions'; +import { InitGeneratorSchema } from './schema'; +import { hasGradlePlugin } from '../../utils/has-gradle-plugin'; + +export async function initGenerator(tree: Tree, options: InitGeneratorSchema) { + const tasks: GeneratorCallback[] = []; + + if (!options.skipPackageJson && tree.exists('package.json')) { + tasks.push( + addDependenciesToPackageJson( + tree, + {}, + { + '@nx/gradle': nxVersion, + }, + undefined, + options.keepExistingVersions + ) + ); + } + + addPlugin(tree); + addProjectReportToBuildGradle(tree); + + if (options.updatePackageScripts && tree.exists('package.json')) { + await updatePackageScripts(tree, createNodes); + } + + if (!options.skipFormat) { + await formatFiles(tree); + } + + return runTasksInSerial(...tasks); +} + +function addPlugin(tree: Tree) { + const nxJson = readNxJson(tree); + + if (!hasGradlePlugin(tree)) { + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/gradle/plugin', + options: { + testTargetName: 'test', + classesTargetName: 'classes', + buildTargetName: 'build', + }, + }); + updateNxJson(tree, nxJson); + } +} + +/** + * This function adds the project-report plugin to the build.gradle or build.gradle.kts file + */ +function addProjectReportToBuildGradle(tree: Tree) { + let buildGradleFile: string; + if (tree.exists('settings.gradle.kts')) { + buildGradleFile = 'build.gradle.kts'; + } else if (tree.exists('settings.gradle')) { + buildGradleFile = 'build.gradle'; + } else { + throw new Error( + 'Could not find settings.gradle or settings.gradle.kts file in your gradle workspace.' + ); + } + let buildGradleContent = ''; + if (tree.exists(buildGradleFile)) { + buildGradleContent = tree.read(buildGradleFile).toString(); + } + if (buildGradleContent.includes('allprojects')) { + if (!buildGradleContent.includes('"project-report')) { + logger.warn(`Please add the project-report plugin to your ${buildGradleFile}: +allprojects { + apply { + plugin("project-report") + } +}`); + } + } else { + buildGradleContent += `\n\rallprojects { + apply { + plugin("project-report") + } +}`; + tree.write(buildGradleFile, buildGradleContent); + } +} + +export default initGenerator; diff --git a/packages/gradle/src/generators/init/schema.d.ts b/packages/gradle/src/generators/init/schema.d.ts new file mode 100644 index 0000000000000..94a79a37d8010 --- /dev/null +++ b/packages/gradle/src/generators/init/schema.d.ts @@ -0,0 +1,6 @@ +export interface InitGeneratorSchema { + skipFormat?: boolean; + skipPackageJson?: boolean; + keepExistingVersions?: boolean; + updatePackageScripts?: boolean; +} diff --git a/packages/gradle/src/generators/init/schema.json b/packages/gradle/src/generators/init/schema.json new file mode 100644 index 0000000000000..ce459209c190e --- /dev/null +++ b/packages/gradle/src/generators/init/schema.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json-schema.org/schema", + "$id": "NxGradleInitSchema", + "title": "Gradle Init Generator", + "description": "Initializes a Gradle project in the current workspace.", + "type": "object", + "properties": { + "skipFormat": { + "description": "Skip formatting files.", + "type": "boolean", + "default": false, + "x-priority": "internal" + }, + "skipPackageJson": { + "type": "boolean", + "default": false, + "description": "Do not add dependencies to `package.json`.", + "x-priority": "internal" + }, + "keepExistingVersions": { + "type": "boolean", + "x-priority": "internal", + "description": "Keep existing dependencies versions", + "default": false + }, + "updatePackageScripts": { + "type": "boolean", + "x-priority": "internal", + "description": "Update `package.json` scripts with inferred targets", + "default": false + } + }, + "required": [] +} diff --git a/packages/gradle/src/plugin/dependencies.ts b/packages/gradle/src/plugin/dependencies.ts index 2edad1e123e3f..764bfc61dc270 100644 --- a/packages/gradle/src/plugin/dependencies.ts +++ b/packages/gradle/src/plugin/dependencies.ts @@ -39,12 +39,14 @@ export const createDependencies: CreateDependencies = async ( if (projectName && depsFile) { dependencies = dependencies.concat( - processGradleDependencies( - depsFile, - gradleProjectToProjectName, - projectName, - gradleFile, - context + Array.from( + processGradleDependencies( + depsFile, + gradleProjectToProjectName, + projectName, + gradleFile, + context + ) ) ); } @@ -85,12 +87,15 @@ function processGradleDependencies( sourceProjectName: string, gradleFile: string, context: CreateDependenciesContext -) { - const dependencies: RawProjectGraphDependency[] = []; +): Set { + const dependencies: Set = new Set(); const lines = readFileSync(depsFile).toString().split('\n'); let inDeps = false; for (const line of lines) { - if (line.startsWith('implementationDependenciesMetadata')) { + if ( + line.startsWith('implementationDependenciesMetadata') || + line.startsWith('compileClasspath') + ) { inDeps = true; continue; } @@ -116,7 +121,7 @@ function processGradleDependencies( sourceFile: gradleFile, }; validateDependency(dependency, context); - dependencies.push(dependency); + dependencies.add(dependency); } } } diff --git a/packages/gradle/src/utils/has-gradle-plugin.ts b/packages/gradle/src/utils/has-gradle-plugin.ts new file mode 100644 index 0000000000000..f1277d429ab0f --- /dev/null +++ b/packages/gradle/src/utils/has-gradle-plugin.ts @@ -0,0 +1,10 @@ +import { readNxJson, Tree } from '@nx/devkit'; + +export function hasGradlePlugin(tree: Tree): boolean { + const nxJson = readNxJson(tree); + return !!nxJson.plugins?.some((p) => + typeof p === 'string' + ? p === '@nx/gradle/plugin' + : p.plugin === '@nx/gradle/plugin' + ); +} diff --git a/packages/gradle/src/utils/versions.ts b/packages/gradle/src/utils/versions.ts new file mode 100644 index 0000000000000..e268dc8f82dd3 --- /dev/null +++ b/packages/gradle/src/utils/versions.ts @@ -0,0 +1 @@ +export const nxVersion = require('../../package.json').version; diff --git a/packages/nx/src/command-line/add/add.ts b/packages/nx/src/command-line/add/add.ts index 1d2fbb0a4e276..ebd810b52a23b 100644 --- a/packages/nx/src/command-line/add/add.ts +++ b/packages/nx/src/command-line/add/add.ts @@ -63,7 +63,7 @@ async function installPackage(pkgName: string, version: string): Promise { writeJsonFile('nx.json', nxJson); try { - await runNxAsync(''); + await runNxAsync('--help'); } catch (e) { // revert adding the plugin to nx.json nxJson.installation.plugins[pkgName] = undefined; diff --git a/packages/nx/src/command-line/init/init-v2.ts b/packages/nx/src/command-line/init/init-v2.ts index cc859a3864b50..01fca0532c046 100644 --- a/packages/nx/src/command-line/init/init-v2.ts +++ b/packages/nx/src/command-line/init/init-v2.ts @@ -49,6 +49,13 @@ export async function initHandler(options: InitArgs): Promise { ); } generateDotNxSetup(version); + const { plugins } = await detectPlugins(); + plugins.forEach((plugin) => { + execSync(`./nx add ${plugin}`, { + stdio: 'inherit', + }); + }); + // invokes the wrapper, thus invoking the initial installation process runNxSync('--version', { stdio: 'ignore' }); return; @@ -190,6 +197,9 @@ async function detectPlugins(): Promise< } } } + if (existsSync('gradlew') || existsSync('gradlew.bat')) { + detectedPlugins.add('@nx/gradle'); + } const plugins = Array.from(detectedPlugins); @@ -214,25 +224,24 @@ async function detectPlugins(): Promise< if (pluginsToInstall?.length === 0) return undefined; - const updatePackageScripts = await prompt<{ updatePackageScripts: string }>([ - { - name: 'updatePackageScripts', - type: 'autocomplete', - message: `Do you want to start using Nx in your package.json scripts?`, - choices: [ - { - name: 'Yes', - }, - { - name: 'No', - }, - ], - initial: 0, - }, - ]).then((r) => r.updatePackageScripts === 'Yes'); - - return { - plugins: pluginsToInstall, - updatePackageScripts, - }; + const updatePackageScripts = + existsSync('package.json') && + (await prompt<{ updatePackageScripts: string }>([ + { + name: 'updatePackageScripts', + type: 'autocomplete', + message: `Do you want to start using Nx in your package.json scripts?`, + choices: [ + { + name: 'Yes', + }, + { + name: 'No', + }, + ], + initial: 0, + }, + ]).then((r) => r.updatePackageScripts === 'Yes')); + + return { plugins: pluginsToInstall, updatePackageScripts }; } diff --git a/scripts/commitizen.js b/scripts/commitizen.js index ce35aaae15b75..fe10aeb5c2e4e 100644 --- a/scripts/commitizen.js +++ b/scripts/commitizen.js @@ -29,6 +29,7 @@ const scopes = [ { value: 'vue', name: 'vue: anything Vue specific' }, { value: 'web', name: 'web: anything Web specific' }, { value: 'webpack', name: 'webpack: anything Webpack specific' }, + { value: 'gradle', name: 'gradle: anything Gradle specific'}, {value: 'module-federation', name: 'module-federation: anything Module Federation specific'}, ];