Skip to content

Commit

Permalink
feat(nx-spring-boot): add install executor + make build depend on it
Browse files Browse the repository at this point in the history
This executor publishes project's artifacts (i.e jar file) to local Maven repository (`~/.m2/repository`).
This allows dependant projects to properly resolve the dependency during their own builds.

Besides, a task dependency was added to `build` executor, so that Nx will automatically call this `install` executor
on dependencies, when running the `build` task from the dependant project, meaning you don't have to worry about it.

Closes #65, #66 and #71
  • Loading branch information
tinesoft committed Jul 18, 2022
1 parent 005ad95 commit 68e1a5e
Show file tree
Hide file tree
Showing 12 changed files with 217 additions and 44 deletions.
85 changes: 55 additions & 30 deletions e2e/nx-spring-boot-e2e/tests/nx-spring-boot.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ describe('nx-spring-boot e2e', () => {
`generate @nxrocks/nx-spring-boot:new ${prjName}`
);

const resultBuildInfo= await runNxCommandAsync(`buildInfo ${prjName}`);
expect(resultBuildInfo.stdout).toContain(`Executing command: ${isWin ? 'mvnw.cmd' : './mvnw'} spring-boot:build-info`)
const resultBuild= await runNxCommandAsync(`build ${prjName}`);
expect(resultBuild.stdout).toContain(`Executing command: ${isWin ? 'mvnw.cmd' : './mvnw'} package`)

expect(() =>
checkFilesExist(`apps/${prjName}/mvnw`,`apps/${prjName}/pom.xml`, `apps/${prjName}/HELP.md`)
Expand All @@ -51,7 +51,11 @@ describe('nx-spring-boot e2e', () => {

}, 200000);

it('should create nx-spring-boot with given options', async() => {
it.each`
projectType
${'application'}
${'library'}
`(`should create a spring-boot '$projectType' with given options`, async ({ projectType }) => {
const prjName = uniq('nx-spring-boot');
const buildSystem = 'maven-project';
const javaVersion = '11';
Expand All @@ -60,23 +64,24 @@ describe('nx-spring-boot e2e', () => {
const artifactId = 'api' ;
const description = 'My sample app';
//const version = '1.2.3'; https://github.com/nrwl/nx/issues/10786
const prjDir = projectType === 'application' ? 'apps' : 'libs';

await runNxCommandAsync(
`generate @nxrocks/nx-spring-boot:new ${prjName} --projectType application --buildSystem=${buildSystem} --packageName=${packageName} --groupId=${groupId} --artifactId=${artifactId} --description="${description}" --javaVersion=${javaVersion}`
`generate @nxrocks/nx-spring-boot:new ${prjName} --projectType ${projectType} --buildSystem=${buildSystem} --packageName=${packageName} --groupId=${groupId} --artifactId=${artifactId} --description="${description}" --javaVersion=${javaVersion}`
);

const resultBuildInfo= await runNxCommandAsync(`buildInfo ${prjName}`);
expect(resultBuildInfo.stdout).toContain(`Executing command: ${isWin ? 'mvnw.cmd' : './mvnw'} spring-boot:build-info`)
const resultBuild= await runNxCommandAsync(`build ${prjName}`);
expect(resultBuild.stdout).toContain(`Executing command: ${isWin ? 'mvnw.cmd' : './mvnw'} package`)

expect(() =>
checkFilesExist(
`apps/${prjName}/mvnw`,
`apps/${prjName}/pom.xml`,
`apps/${prjName}/HELP.md`,
`apps/${prjName}/src/main/java/com/tinesoft/api/${names(prjName).className}Application.java`)
`${prjDir}/${prjName}/mvnw`,
`${prjDir}/${prjName}/pom.xml`,
`${prjDir}/${prjName}/HELP.md`,
`${prjDir}/${prjName}/src/main/java/com/tinesoft/api/${names(prjName).className}Application.java`)
).not.toThrow();

const pomXml = readFile(`apps/${prjName}/pom.xml`);
const pomXml = readFile(`${prjDir}/${prjName}/pom.xml`);
expect(pomXml).toContain(`<groupId>${groupId}</groupId>`);
expect(pomXml).toContain(`<artifactId>${artifactId}</artifactId>`);
expect(pomXml).toContain(`<name>${prjName}</name>`);
Expand All @@ -89,33 +94,38 @@ describe('nx-spring-boot e2e', () => {
const execPermission = '755';
expect(
lstatSync(
tmpProjPath(`apps/${prjName}/mvnw`)
tmpProjPath(`${prjDir}/${prjName}/mvnw`)
).mode & octal(execPermission)
).toEqual(octal(execPermission));
}
}, 200000);

describe('--buildSystem=gradle-project', () => {
it('should create a gradle spring-boot project', async() => {
it.each`
projectType
${'application'}
${'library'}
`(`should create a gradle spring-boot '$projectType'`, async ({ projectType }) => {
const prjName = uniq('nx-spring-boot');
const prjDir = projectType === 'application' ? 'apps' : 'libs';

await runNxCommandAsync(
`generate @nxrocks/nx-spring-boot:new ${prjName} --projectType application --buildSystem gradle-project`
`generate @nxrocks/nx-spring-boot:new ${prjName} --projectType ${projectType} --buildSystem gradle-project`
);

const resultBuildInfo= await runNxCommandAsync(`buildInfo ${prjName}`);
expect(resultBuildInfo.stdout).toContain(`Executing command: ${isWin ? 'gradlew.bat' : './gradlew'} bootBuildInfo`)
const resultBuild= await runNxCommandAsync(`build ${prjName}`);
expect(resultBuild.stdout).toContain(`Executing command: ${isWin ? 'gradlew.bat' : './gradlew'} build`)

expect(() =>
checkFilesExist(`apps/${prjName}/gradlew`,`apps/${prjName}/build.gradle`, `apps/${prjName}/HELP.md`)
checkFilesExist(`${prjDir}/${prjName}/gradlew`,`${prjDir}/${prjName}/build.gradle`, `${prjDir}/${prjName}/HELP.md`)
).not.toThrow();

// make sure the build wrapper file is executable (*nix only)
if(!isWin) {
const execPermission = '755';
expect(
lstatSync(
tmpProjPath(`apps/${prjName}/gradlew`)
tmpProjPath(`${prjDir}/${prjName}/gradlew`)
).mode & octal(execPermission)
).toEqual(octal(execPermission));
}
Expand All @@ -124,26 +134,31 @@ describe('nx-spring-boot e2e', () => {
});

describe('--buildSystem=gradle-project and --language=kotlin', () => {
it('should create a gradle spring-boot project with kotlin', async() => {
it.each`
projectType
${'application'}
${'library'}
`(`should create a gradle spring-boot '$projectType' with kotlin`, async ({ projectType }) => {
const prjName = uniq('nx-spring-boot');
const prjDir = projectType === 'application' ? 'apps' : 'libs';

await runNxCommandAsync(
`generate @nxrocks/nx-spring-boot:new ${prjName} --projectType application --buildSystem gradle-project --language kotlin`
`generate @nxrocks/nx-spring-boot:new ${prjName} --projectType ${projectType} --buildSystem gradle-project --language kotlin`
);

const resultBuildInfo= await runNxCommandAsync(`buildInfo ${prjName}`);
expect(resultBuildInfo.stdout).toContain(`Executing command: ${isWin ? 'gradlew.bat' : './gradlew'} bootBuildInfo`)
const resultBuild= await runNxCommandAsync(`build ${prjName}`);
expect(resultBuild.stdout).toContain(`Executing command: ${isWin ? 'gradlew.bat' : './gradlew'} build`)

expect(() =>
checkFilesExist(`apps/${prjName}/gradlew`,`apps/${prjName}/build.gradle.kts`, `apps/${prjName}/HELP.md`)
checkFilesExist(`${prjDir}/${prjName}/gradlew`,`${prjDir}/${prjName}/build.gradle.kts`, `${prjDir}/${prjName}/HELP.md`)
).not.toThrow();

// make sure the build wrapper file is executable (*nix only)
if(!isWin) {
const execPermission = '755';
expect(
lstatSync(
tmpProjPath(`apps/${prjName}/gradlew`)
tmpProjPath(`${prjDir}/${prjName}/gradlew`)
).mode & octal(execPermission)
).toEqual(octal(execPermission));
}
Expand All @@ -152,27 +167,37 @@ describe('nx-spring-boot e2e', () => {


describe('--directory', () => {
it('should create src in the specified directory', async() => {
it.each`
projectType
${'application'}
${'library'}
`(`should create src in the specified directory when generating a '$projectType'`, async ({ projectType }) => {
const prjName = uniq('nx-spring-boot');
const prjDir = projectType === 'application' ? 'apps' : 'libs';

await runNxCommandAsync(
`generate @nxrocks/nx-spring-boot:new ${prjName} --projectType application --directory subdir`
`generate @nxrocks/nx-spring-boot:new ${prjName} --projectType ${projectType} --directory subdir`
);
expect(() =>
checkFilesExist(`apps/subdir/${prjName}/mvnw`,`apps/subdir/${prjName}/pom.xml`, `apps/subdir/${prjName}/HELP.md`)
checkFilesExist(`${prjDir}/subdir/${prjName}/mvnw`,`${prjDir}/subdir/${prjName}/pom.xml`, `${prjDir}/subdir/${prjName}/HELP.md`)
).not.toThrow();

}, 200000);
});

describe('--tags', () => {
it('should add tags to nx.json', async() => {
it.each`
projectType
${'application'}
${'library'}
`(`should add tags to nx.json' when generating a '$projectType'`, async ({ projectType }) => {
const prjName = uniq('nx-spring-boot');
const prjDir = projectType === 'application' ? 'apps' : 'libs';

await runNxCommandAsync(
`generate @nxrocks/nx-spring-boot:new ${prjName} --projectType application --tags e2etag,e2ePackage`
`generate @nxrocks/nx-spring-boot:new ${prjName} --projectType ${projectType} --tags e2etag,e2ePackage`
);
const project = readJson(`apps/${prjName}/project.json`);
const project = readJson(`${prjDir}/${prjName}/project.json`);
expect(project.tags).toEqual(['e2etag', 'e2ePackage']);

}, 200000);
Expand Down
7 changes: 7 additions & 0 deletions packages/nx-spring-boot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ Here the list of available executors:
| `format` | `ignoreWrapper:boolean`, `args: string[]` | Format the project using [Spotless](https://github.com/diffplug/spotless) plugin for Maven or Gradle |
| `format-check` | `ignoreWrapper:boolean`, `args: string[]` | Check whether the project is well formatted 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` |
| `install` | `ignoreWrapper:boolean`, `args: string[]` | Installs the project's artifacts to local Maven repository (in `~/.m2/repository`) using either `./mvnw\|mvn install` or `./gradlew\|gradle publishToMavenLocal` |
| `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 @@ -197,6 +198,12 @@ You can pass in additional arguments by editing the related section in the `work
nx build your-boot-app
```

### Install the project's artifacts to local Maven repository (in ~/.m2/repository) - ('install' Executor)

```
nx install your-boot-app
```

### Building the OCI Image - ('buildImage' Executor)

```
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"
},
"install": {
"implementation": "./src/executors/install/executor",
"schema": "./src/executors/install/schema.json",
"description": "Executor to install the project's artifacts (i.e jar or war) to the local Maven repository"
},
"format": {
"implementation": "./src/executors/format/executor",
"schema": "./src/executors/format/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',
'install': 'publishToMavenLocal',
'format': 'spotlessApply',
'format-check': 'spotlessCheck',
'buildImage': 'bootBuildImage',
Expand All @@ -18,6 +19,7 @@ export const MAVEN_BOOT_COMMAND_MAPPER: BuilderCommandAliasMapper = {
'test': 'test',
'clean': 'clean',
'build': 'package',
'install': 'install',
'format': 'spotless:apply',
'format-check': 'spotless:check',
'buildImage': 'spring-boot:build-image',
Expand Down
47 changes: 47 additions & 0 deletions packages/nx-spring-boot/src/executors/install/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 'jest-mock';

import { installExecutor } from './executor';
import { InstallExecutorOptions } 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, 'install');
const options: InstallExecutorOptions = {
root: 'apps/bootapp'
};

describe('Install Executor', () => {

beforeEach(async () => {
jest.spyOn(logger, 'info');
jest.spyOn(cp, 'execSync');
});

afterEach(() => {
jest.resetAllMocks();
});

it.each`
ignoreWrapper | buildSystem | buildFile | execute
${true} | ${'maven'} | ${'pom.xml'} | ${'mvn install '}
${true} | ${'gradle'} | ${'build.gradle'} | ${'gradle publishToMavenLocal '}
${false} | ${'maven'} | ${'pom.xml'} | ${MAVEN_WRAPPER_EXECUTABLE + ' install '}
${false} | ${'gradle'} | ${'build.gradle'} | ${GRADLE_WRAPPER_EXECUTABLE + ' publishToMavenLocal '}
`('should execute a $buildSystem build and ignoring wrapper : $ignoreWrapper', async ({ ignoreWrapper, buildFile, execute }) => {
mocked(fsUtility.fileExists).mockImplementation((filePath: string) => filePath.indexOf(buildFile) !== -1);

await installExecutor({ ...options, ignoreWrapper }, mockContext);

expectExecutorCommandRanWith(execute, mockContext, options);
});

});
11 changes: 11 additions & 0 deletions packages/nx-spring-boot/src/executors/install/executor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ExecutorContext } from '@nrwl/devkit'
import * as path from 'path'
import { InstallExecutorOptions } from './schema'
import { runBootPluginCommand } from '../../utils/boot-utils'

export async function installExecutor(options: InstallExecutorOptions, context: ExecutorContext){
const root = path.resolve(context.root, options.root);
return runBootPluginCommand('install', options.args, { cwd : root, ignoreWrapper: options.ignoreWrapper});
}

export default installExecutor;
6 changes: 6 additions & 0 deletions packages/nx-spring-boot/src/executors/install/schema.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

export interface InstallExecutorOptions {
root: string;
ignoreWrapper?: boolean;
args?: string[];
}
24 changes: 24 additions & 0 deletions packages/nx-spring-boot/src/executors/install/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"$schema": "http://json-schema.org/schema",
"title": "Install 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": []
}
16 changes: 9 additions & 7 deletions packages/nx-spring-boot/src/generators/project/generator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,15 @@ describe('project generator', () => {

expect(logger.info).toHaveBeenNthCalledWith(2, `Extracting Spring Boot project zip to '${workspaceRoot}/${rootDir}/${options.name}'...`);


if (buildSystem === 'gradle-project') {

if (projectType === 'library') {
expect(logger.debug).toHaveBeenCalledWith(`Disabling 'spring-boot' gradle plugin on a library project...`);
expect(logger.debug).toHaveBeenNthCalledWith(1,`Disabling 'spring-boot' gradle plugin on a library project...`);
} else {
expect(logger.debug).toHaveBeenCalledWith(`Adding 'buildInfo' task to the build.gradle file...`);
expect(logger.debug).toHaveBeenNthCalledWith(1,`Adding 'buildInfo' task to the build.gradle file...`);
}
expect(logger.debug).toHaveBeenNthCalledWith(2,`Adding 'maven-publish' plugin...`);
}

if (buildSystem === 'maven-project' && projectType === 'library') {
Expand All @@ -171,16 +173,16 @@ describe('project generator', () => {
const projectDir = projectType === 'application' ? 'apps' : 'libs';
expect(project.root).toBe(`${projectDir}/${options.name}`);

const commands = ['build', 'format', 'format-check', 'test', 'clean']
const bootOnlyCommands = ['run', 'serve', 'buildImage', 'buildInfo'];
const commands = ['build', 'install', 'format', 'format-check', 'test', 'clean']
const appOnlyCommands = ['run', 'serve', 'buildImage', 'buildInfo'];

if (projectType === 'application') {
commands.push(...bootOnlyCommands);
commands.push(...appOnlyCommands);
}

commands.forEach(cmd => {
expect(project.targets[cmd].executor).toBe(`${NX_SPRING_BOOT_PKG}:${cmd}`);
if(cmd === 'build') {
if(['build', 'install'].includes(cmd)) {
expect(project.targets[cmd].outputs).toEqual([`${project.root}/target`]);
}
});
Expand Down
Loading

0 comments on commit 68e1a5e

Please sign in to comment.