diff --git a/e2e/nx-spring-boot-e2e/tests/nx-spring-boot.test.ts b/e2e/nx-spring-boot-e2e/tests/nx-spring-boot.test.ts index 0947bfed..2e76f967 100644 --- a/e2e/nx-spring-boot-e2e/tests/nx-spring-boot.test.ts +++ b/e2e/nx-spring-boot-e2e/tests/nx-spring-boot.test.ts @@ -21,7 +21,6 @@ describe('nx-spring-boot e2e', () => { verdaccioRegistry = await spawnVerdaccioRegistry(verdaccioPort); enableVerdaccioRegistry(verdaccioPort); - process.env.NX_E2E_SKIP_BUILD_CLEANUP = 'true'; await buildAndPublishPackages(verdaccioPort); ensureNxProject('@nxrocks/nx-spring-boot', 'dist/packages/nx-spring-boot'); @@ -35,7 +34,7 @@ describe('nx-spring-boot e2e', () => { } }); - it.only('should create nx-spring-boot with default options', async() => { + it('should create nx-spring-boot with default options', async() => { const prjName = uniq('nx-spring-boot'); await runNxCommandAsync( `generate @nxrocks/nx-spring-boot:new ${prjName}` diff --git a/packages/nx-spring-boot/README.md b/packages/nx-spring-boot/README.md index d3dbf9c8..034e284f 100644 --- a/packages/nx-spring-boot/README.md +++ b/packages/nx-spring-boot/README.md @@ -21,6 +21,7 @@ Here is a list of some of the coolest features of the plugin: - ✅ Generation of Spring Boot applications/libraries based on **Spring Initializr** API - ✅ Building, packaging, testing, etc your Spring Boot projects +- ✅ Code formatting using the excellent [**Spotless**](https://github.com/diffplug/spotless) plugin for Maven or Gradle - ✅ Integration with Nx's **dependency graph** (through `nx dep-graph` or `nx affected:dep-graph`): this allows you to **visualize** the dependencies of any Spring Boot's `Maven`/`Gradle` applications or libraries inside your workspace, just like Nx natively does it for JS/TS-based projects! ![Nx Spring Boot dependency graph](https://raw.githubusercontent.com/tinesoft/nxrocks/develop/images/nx-spring-boot-dep-graph.png) @@ -103,11 +104,12 @@ Once your app is generated, you can now use buidlers to manage it. Here the list of available executors: -| Executor | Arguments | Description | +| Executor | Arguments | Description | | --------------- | ------------------------------------------ | ------------------------------------------ | | `run` \| `serve`*| `ignoreWrapper:boolean`, `args: string[]` | Runs the project using either `./mvnw\|mvn spring-boot:run` or `./gradlew\|gradle bootRun` | | `test` | `ignoreWrapper:boolean`, `args: string[]` | Tests the project using either `./mvnw\|mvn test` or `./gradlew\|gradle test` | | `clean` | `ignoreWrapper:boolean`, `args: string[]` | Cleans the project using either `./mvnw\|mvn clean` or `./gradlew\|gradle clean` | +| `format` | `ignoreWrapper:boolean`, `args: string[]` | Format the project using [Spotless](https://github.com/diffplug/spotless) plugin for Maven or Gradle | | `build` | `ignoreWrapper:boolean`, `args: string[]` | Packages the project into an executable Jar using either `./mvnw\|mvn package` or `./gradlew\|gradle build` | | `buildInfo`* | `ignoreWrapper:boolean`, | Generates a `build-info.properties` using either `./mvnw\|mvn spring-boot:build-info` or `./gradlew\|gradle bootBuildInfo` | | `buildImage`* | `ignoreWrapper:boolean`, `args: string[]` | Generates an [OCI Image](https://github.com/opencontainers/image-spec) using either `./mvnw\|mvn spring-boot:build-image` or `./gradlew\|gradle bootBuildImage` | @@ -151,16 +153,10 @@ You can pass in additional arguments by editing the related section in the `work } ``` -### Building the Jar - ('buildJar' Executor) +### Building the Jar or War - ('build' Executor) ``` -nx buildJar your-boot-app -``` - -### Building the War - ('buildWar' Executor) - -``` -nx buildWar your-boot-app +nx build your-boot-app ``` ### Building the OCI Image - ('buildImage' Executor) @@ -205,6 +201,12 @@ nx test your-boot-app nx clean your-boot-app ``` +### Formatting the project - ('format' Executor) + +``` +nx run your-boot-app:format +``` + ## Compatibility with Nx Every Nx plugin relies on the underlying Nx Workspace/DevKit it runs on. This table provides the compatibility matrix between major versions of Nx workspace and this plugin. diff --git a/packages/nx-spring-boot/executors.json b/packages/nx-spring-boot/executors.json index 6a510e8f..54ad7402 100644 --- a/packages/nx-spring-boot/executors.json +++ b/packages/nx-spring-boot/executors.json @@ -25,6 +25,11 @@ "schema": "./src/executors/build/schema.json", "description": "Executor to build the project's Jar or War" }, + "format": { + "implementation": "./src/executors/format/executor", + "schema": "./src/executors/format/schema.json", + "description": "Executor to format the project's files using Spotless plugin" + }, "buildImage": { "implementation": "./src/executors/build-image/executor", "schema": "./src/executors/build-image/schema.json", diff --git a/packages/nx-spring-boot/src/core/constants.ts b/packages/nx-spring-boot/src/core/constants.ts index 474a8ebb..91367083 100644 --- a/packages/nx-spring-boot/src/core/constants.ts +++ b/packages/nx-spring-boot/src/core/constants.ts @@ -5,6 +5,7 @@ export const GRADLE_BOOT_COMMAND_MAPPER : BuilderCommandAliasMapper = { 'test': 'test', 'clean': 'clean', 'build': 'build', + 'format': 'spotlessApply', 'buildImage': 'bootBuildImage', 'buildInfo': 'bootBuildInfo' } @@ -16,6 +17,7 @@ export const MAVEN_BOOT_COMMAND_MAPPER: BuilderCommandAliasMapper = { 'test': 'test', 'clean': 'clean', 'build': 'package', + 'format': 'spotless:apply', 'buildImage': 'spring-boot:build-image', 'buildInfo': 'spring-boot:build-info' } diff --git a/packages/nx-spring-boot/src/executors/format/executor.spec.ts b/packages/nx-spring-boot/src/executors/format/executor.spec.ts new file mode 100644 index 00000000..d987ede7 --- /dev/null +++ b/packages/nx-spring-boot/src/executors/format/executor.spec.ts @@ -0,0 +1,47 @@ +import { logger } from '@nrwl/devkit'; +import { mocked } from 'ts-jest/utils'; + +import { formatExecutor } from './executor'; +import { FormatExecutorOptions } from './schema'; +import { GRADLE_WRAPPER_EXECUTABLE, MAVEN_WRAPPER_EXECUTABLE, NX_SPRING_BOOT_PKG } from '@nxrocks/common'; +import { expectExecutorCommandRanWith, mockExecutorContext } from '@nxrocks/common/testing'; + +//first, we mock +jest.mock('child_process'); +jest.mock('@nrwl/workspace/src/utils/fileutils'); + +//then, we import +import * as fsUtility from '@nrwl/workspace/src/utils/fileutils'; +import * as cp from 'child_process'; + +const mockContext = mockExecutorContext(NX_SPRING_BOOT_PKG, 'format'); +const options: FormatExecutorOptions = { + root: 'apps/bootapp' +}; + +describe('Format Executor', () => { + + beforeEach(async () => { + jest.spyOn(logger, 'info'); + jest.spyOn(cp, 'execSync'); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it.each` + ignoreWrapper | buildSystem | formatFile | execute + ${true} | ${'maven'} | ${'pom.xml'} | ${'mvn spotless:apply '} + ${true} | ${'gradle'} | ${'build.gradle'} | ${'gradle spotlessApply '} + ${false} | ${'maven'} | ${'pom.xml'} | ${MAVEN_WRAPPER_EXECUTABLE + ' spotless:apply '} + ${false} | ${'gradle'} | ${'build.gradle'} | ${GRADLE_WRAPPER_EXECUTABLE + ' spotlessApply '} + `('should execute a $buildSystem format and ignoring wrapper : $ignoreWrapper', async ({ ignoreWrapper, formatFile, execute }) => { + mocked(fsUtility.fileExists).mockImplementation((filePath: string) => filePath.indexOf(formatFile) !== -1); + + await formatExecutor({ ...options, ignoreWrapper }, mockContext); + + expectExecutorCommandRanWith(execute, mockContext, options); + }); + +}); diff --git a/packages/nx-spring-boot/src/executors/format/executor.ts b/packages/nx-spring-boot/src/executors/format/executor.ts new file mode 100644 index 00000000..4353b46a --- /dev/null +++ b/packages/nx-spring-boot/src/executors/format/executor.ts @@ -0,0 +1,17 @@ +import { ExecutorContext } from '@nrwl/devkit' +import * as path from 'path' +import { FormatExecutorOptions } from './schema' +import { runBootPluginCommand } from '../../utils/boot-utils' + +export async function formatExecutor(options: FormatExecutorOptions, context: ExecutorContext){ + const root = path.resolve(context.root, options.root); + const result = runBootPluginCommand('format', options.args, { cwd : root, ignoreWrapper: options.ignoreWrapper}); + + if (!result.success) { + throw new Error(); + } + + return result; +} + +export default formatExecutor; \ No newline at end of file diff --git a/packages/nx-spring-boot/src/executors/format/schema.d.ts b/packages/nx-spring-boot/src/executors/format/schema.d.ts new file mode 100644 index 00000000..8764a440 --- /dev/null +++ b/packages/nx-spring-boot/src/executors/format/schema.d.ts @@ -0,0 +1,6 @@ + +export interface FormatExecutorOptions { + root: string; + ignoreWrapper?: boolean; + args?: string[]; +} \ No newline at end of file diff --git a/packages/nx-spring-boot/src/executors/format/schema.json b/packages/nx-spring-boot/src/executors/format/schema.json new file mode 100644 index 00000000..54d37c24 --- /dev/null +++ b/packages/nx-spring-boot/src/executors/format/schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/schema", + "title": "Format executor", + "description": "", + "cli": "nx", + "type": "object", + "properties": { + "root": { + "description": "The project root", + "type": "string" + }, + "ignoreWrapper": { + "description": "Whether or not to use the embedded wrapper (`mvnw`or `gradlew`) to perfom build operations", + "type": "boolean", + "default": false + }, + "args": { + "description": "The argument to be passed to the underlying Spring Boot command", + "type": "array", + "default": [] + } + }, + "required": [] +} \ No newline at end of file diff --git a/packages/nx-spring-boot/src/generators/project/generator.spec.ts b/packages/nx-spring-boot/src/generators/project/generator.spec.ts index cf8a1efc..7bdd13b1 100644 --- a/packages/nx-spring-boot/src/generators/project/generator.spec.ts +++ b/packages/nx-spring-boot/src/generators/project/generator.spec.ts @@ -13,16 +13,84 @@ jest.mock('node-fetch'); import fetch from 'node-fetch'; const { Response } = jest.requireActual('node-fetch'); -import { NX_SPRING_BOOT_PKG} from '@nxrocks/common'; +import { NX_SPRING_BOOT_PKG } from '@nxrocks/common'; import { mockZipEntries, syncToAsyncIterable } from '@nxrocks/common/testing'; +const POM_XML = ` + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.6.2 + + + com.example + demo + 0.0.1-SNAPSHOT + demo + Demo project for Spring Boot + + 11 + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + +`; + +const BUILD_GRADLE = +`plugins { + id 'org.springframework.boot' version '2.6.2' + id 'io.spring.dependency-management' version '1.0.11.RELEASE' + id 'groovy' +} + +group = 'com.example' +version = '0.0.1-SNAPSHOT' +sourceCompatibility = '11' + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter' + implementation 'org.codehaus.groovy:groovy' + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +test { + useJUnitPlatform() +}`; + describe('project generator', () => { let tree: Tree; const options: ProjectGeneratorOptions = { name: 'bootapp', projectType: 'application', - springInitializerUrl: 'https://start.spring.io' + springInitializerUrl: 'https://start.spring.io', + language: 'java' }; const mockedFetch = (fetch as jest.MockedFunction); @@ -41,19 +109,17 @@ describe('project generator', () => { }); it.each` - projectType | buildSystem | buildFile | wrapperName - ${'application'} | ${'maven-project'} | ${'pom.xml'} | ${'mvnw'} - ${'application'} | ${'gradle-project'} | ${'build.gradle'} | ${'gradlew'} - ${'library'} | ${'maven-project'} | ${'pom.xml'} | ${'mvnw'} - ${'library'} | ${'gradle-project'} | ${'build.gradle'} | ${'gradlew'} - `(`should download a spring boot '$projectType' build with $buildSystem`, async ({ projectType, buildSystem, buildFile, wrapperName }) => { + projectType | buildSystem | buildFile | buildFileContent | wrapperName + ${'application'} | ${'maven-project'} | ${'pom.xml'} | ${POM_XML} | ${'mvnw'} + ${'application'} | ${'gradle-project'} | ${'build.gradle'} | ${BUILD_GRADLE} | ${'gradlew'} + ${'library'} | ${'maven-project'} | ${'pom.xml'} | ${POM_XML} | ${'mvnw'} + ${'library'} | ${'gradle-project'} | ${'build.gradle'} | ${BUILD_GRADLE} | ${'gradlew'} + `(`should download a spring boot '$projectType' build with $buildSystem`, async ({ projectType, buildSystem, buildFile, buildFileContent, wrapperName }) => { const rootDir = projectType === 'application' ? 'apps' : 'libs'; - const downloadUrl = `${options.springInitializerUrl}/starter.zip?type=${buildSystem}&name=${options.name}`; - - tree.write(`/${rootDir}/${options.name}/${buildFile}`, ''); + const downloadUrl = `${options.springInitializerUrl}/starter.zip?type=${buildSystem}&language=${options.language}&name=${options.name}`; - const zipFiles = [`${buildFile}`, `${wrapperName}`, 'README.md',]; + const zipFiles = [ { filePath: buildFile, fileContent: buildFileContent}, wrapperName, 'README.md',]; const starterZip = mockZipEntries(zipFiles); // mock the zip content returned by the real call to Spring Initializer jest.spyOn(mockedResponse.body, 'pipe').mockReturnValue(syncToAsyncIterable(starterZip)); @@ -89,15 +155,22 @@ describe('project generator', () => { it.each` projectType | subDir - ${'application'} | ${'apps'} - ${'library'} | ${'libs'} + ${'application'} | ${'apps'} + ${'library'} | ${'libs'} `(`should update workspace.json for '$projectType'`, async ({ projectType, subDir }) => { + + const zipFiles = [{ filePath: 'pom.xml', fileContent: POM_XML }, 'mvnw', 'README.md',]; + const starterZip = mockZipEntries(zipFiles); + // mock the zip content returned by the real call to Spring Initializer + jest.spyOn(mockedResponse.body, 'pipe').mockReturnValue(syncToAsyncIterable(starterZip)); + await projectGenerator(tree, { ...options, projectType }); + const project = readProjectConfiguration(tree, options.name); expect(project.root).toBe(`${subDir}/${options.name}`); - const commands = ['test', 'clean'] - const bootOnlyCommands = ['run', 'serve', 'buildJar', 'buildWar', 'buildImage', 'buildInfo']; + const commands = ['build', 'format', 'test', 'clean'] + const bootOnlyCommands = ['run', 'serve', 'buildImage', 'buildInfo']; if (projectType === 'application') { commands.push(...bootOnlyCommands); @@ -109,7 +182,13 @@ describe('project generator', () => { }); it('should add plugin to nx.json', async () => { + const zipFiles = [{ filePath: 'pom.xml', fileContent: POM_XML }, 'mvnw', 'README.md',]; + const starterZip = mockZipEntries(zipFiles); + // mock the zip content returned by the real call to Spring Initializer + jest.spyOn(mockedResponse.body, 'pipe').mockReturnValue(syncToAsyncIterable(starterZip)); + await projectGenerator(tree, options); + const nxJson = readJson(tree, 'nx.json'); expect(nxJson.plugins).toEqual([NX_SPRING_BOOT_PKG]); diff --git a/packages/nx-spring-boot/src/generators/project/generator.ts b/packages/nx-spring-boot/src/generators/project/generator.ts index c8bb7cc6..987e7767 100644 --- a/packages/nx-spring-boot/src/generators/project/generator.ts +++ b/packages/nx-spring-boot/src/generators/project/generator.ts @@ -2,14 +2,15 @@ import { Tree, addProjectConfiguration, } from '@nrwl/devkit'; import { ProjectGeneratorOptions } from './schema'; import { normalizeOptions, generateBootProject, addBuilInfoTask, disableBootJarTask, removeBootMavenPlugin } from './lib'; import { addPluginToNxJson, NX_SPRING_BOOT_PKG } from '@nxrocks/common'; +import { addFormattingWithSpotless } from './lib/add-formatting-with-spotless'; export async function projectGenerator(tree: Tree, options: ProjectGeneratorOptions) { const normalizedOptions = normalizeOptions(tree, options); const targets = {}; - const commands = ['test', 'clean']; - const bootOnlyCommands = ['run', 'serve', 'buildJar', 'buildWar', 'buildImage', 'buildInfo']; + const commands = ['build', 'test', 'clean', 'format']; + const bootOnlyCommands = ['run', 'serve', 'buildImage', 'buildInfo']; if (options.projectType === 'application') { //only 'application' projects should have 'boot' related commands commands.push(...bootOnlyCommands); @@ -32,11 +33,20 @@ export async function projectGenerator(tree: Tree, options: ProjectGeneratorOpti }); await generateBootProject(tree, normalizedOptions); + addBuilInfoTask(tree, normalizedOptions); - disableBootJarTask(tree, normalizedOptions); - removeBootMavenPlugin(tree, normalizedOptions); + if(normalizedOptions.projectType === 'library') { + if(normalizedOptions.buildSystem === 'gradle-project') { + disableBootJarTask(tree, normalizedOptions); + } + else if (normalizedOptions.buildSystem === 'maven-project') { + removeBootMavenPlugin(tree, normalizedOptions); + } + } + addFormattingWithSpotless(tree, normalizedOptions); + addPluginToNxJson(NX_SPRING_BOOT_PKG,tree); } diff --git a/packages/nx-spring-boot/src/generators/project/lib/add-formatting-with-spotless.ts b/packages/nx-spring-boot/src/generators/project/lib/add-formatting-with-spotless.ts new file mode 100644 index 00000000..b97ea24f --- /dev/null +++ b/packages/nx-spring-boot/src/generators/project/lib/add-formatting-with-spotless.ts @@ -0,0 +1,20 @@ +import { + readJson, + Tree +} from '@nrwl/devkit'; +import { addSpotlessGradlePlugin, addSpotlessMavenPlugin } from '@nxrocks/common'; +import { NormalizedSchema } from '../schema'; + +export function addFormattingWithSpotless(tree: Tree, options: NormalizedSchema) { + + const nxJson = readJson(tree, 'nx.json'); + const gitBaseBranch = nxJson.affected?.defaultBase || 'master'; + + if (options.buildSystem === 'gradle-project') { + addSpotlessGradlePlugin(tree, options.projectRoot, options.language, +(options.javaVersion), gitBaseBranch); + } + else { + addSpotlessMavenPlugin(tree, options.projectRoot, options.language, +(options.javaVersion), gitBaseBranch); + } + +}