Skip to content

Commit

Permalink
feat(nx-spring-boot): add format executor
Browse files Browse the repository at this point in the history
You can now use `nx run your-app:run` to format the code.
  • Loading branch information
tinesoft committed Feb 10, 2022
1 parent 35e4d76 commit b5362ae
Show file tree
Hide file tree
Showing 11 changed files with 242 additions and 31 deletions.
3 changes: 1 addition & 2 deletions e2e/nx-spring-boot-e2e/tests/nx-spring-boot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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}`
Expand Down
20 changes: 11 additions & 9 deletions packages/nx-spring-boot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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`<sup>*</sup>| `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`<sup>*</sup> | `ignoreWrapper:boolean`, | Generates a `build-info.properties` using either `./mvnw\|mvn spring-boot:build-info` or `./gradlew\|gradle bootBuildInfo` |
| `buildImage`<sup>*</sup> | `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` |
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions packages/nx-spring-boot/executors.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions packages/nx-spring-boot/src/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const GRADLE_BOOT_COMMAND_MAPPER : BuilderCommandAliasMapper = {
'test': 'test',
'clean': 'clean',
'build': 'build',
'format': 'spotlessApply',
'buildImage': 'bootBuildImage',
'buildInfo': 'bootBuildInfo'
}
Expand All @@ -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'
}
Expand Down
47 changes: 47 additions & 0 deletions packages/nx-spring-boot/src/executors/format/executor.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});

});
17 changes: 17 additions & 0 deletions packages/nx-spring-boot/src/executors/format/executor.ts
Original file line number Diff line number Diff line change
@@ -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;
6 changes: 6 additions & 0 deletions packages/nx-spring-boot/src/executors/format/schema.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

export interface FormatExecutorOptions {
root: string;
ignoreWrapper?: boolean;
args?: string[];
}
24 changes: 24 additions & 0 deletions packages/nx-spring-boot/src/executors/format/schema.json
Original file line number Diff line number Diff line change
@@ -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": []
}
111 changes: 95 additions & 16 deletions packages/nx-spring-boot/src/generators/project/generator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>`;

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<typeof fetch>);
Expand All @@ -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));
Expand Down Expand Up @@ -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);
Expand All @@ -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]);

Expand Down
18 changes: 14 additions & 4 deletions packages/nx-spring-boot/src/generators/project/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}

Expand Down
Loading

0 comments on commit b5362ae

Please sign in to comment.