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);
+ }
+
+}