Reduce Repetitive Configuration

Nx can help you dramatically reduce the lines of configuration code that you need to maintain.

Lets say you have three libraries in your repository - lib1, lib2 and lib3. The folder structure looks like this:

1repo/ 2โ”œโ”€โ”€ libs/ 3โ”‚ โ””โ”€โ”€ lib1/ 4โ”‚ โ”‚ โ”œโ”€โ”€ tsconfig.lib.json 5โ”‚ โ”‚ โ””โ”€โ”€ project.json 6โ”‚ โ””โ”€โ”€ lib2/ 7โ”‚ โ”‚ โ”œโ”€โ”€ tsconfig.lib.json 8โ”‚ โ”‚ โ””โ”€โ”€ project.json 9โ”‚ โ””โ”€โ”€ lib3/ 10โ”‚ โ”œโ”€โ”€ tsconfig.lib.json 11โ”‚ โ””โ”€โ”€ project.json 12โ””โ”€โ”€ nx.json 13

Initial Configuration Settings

All three libraries have a similar project configuration. Here is what their project.json files look like:

libs/lib1/project.json
1{ 2 "name": "lib1", 3 "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 "sourceRoot": "libs/lib1/src", 5 "projectType": "library", 6 "targets": { 7 "build": { 8 "executor": "@nx/js:tsc", 9 "outputs": ["{options.outputPath}"], 10 "options": { 11 "outputPath": "dist/libs/lib1", 12 "main": "libs/lib1/src/index.ts", 13 "tsConfig": "libs/lib1/tsconfig.lib.json", 14 "assets": ["libs/lib1/*.md", "libs/lib1/src/images/*"] 15 } 16 }, 17 "lint": { 18 "executor": "@nx/eslint:lint", 19 "outputs": ["{options.outputFile}"], 20 "options": { 21 "lintFilePatterns": ["libs/lib1/**/*.ts"] 22 } 23 }, 24 "test": { 25 "executor": "@nx/jest:jest", 26 "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], 27 "options": { 28 "jestConfig": "libs/lib1/jest.config.ts", 29 "passWithNoTests": true 30 }, 31 "configurations": { 32 "ci": { 33 "ci": true, 34 "codeCoverage": true 35 } 36 } 37 } 38 }, 39 "tags": [] 40} 41

If you scan through these three files, they look very similar. The only differences aside from the project paths are that lib1 has different assets defined for the build target and lib2 has a testTimeout set for the test target.

Reduce Configuration with targetDefaults

Let's use the targetDefaults property in nx.json to reduce some of this duplicate configuration code.

nx.json
1{ 2 "targetDefaults": { 3 "build": { 4 "executor": "@nx/js:tsc", 5 "outputs": ["{options.outputPath}"], 6 "options": { 7 "outputPath": "dist/{projectRoot}", 8 "main": "{projectRoot}/src/index.ts", 9 "tsConfig": "{projectRoot}/tsconfig.lib.json", 10 "assets": ["{projectRoot}/*.md"] 11 } 12 }, 13 "lint": { 14 "executor": "@nx/eslint:lint", 15 "outputs": ["{options.outputFile}"], 16 "options": { 17 "lintFilePatterns": ["{projectRoot}/**/*.ts"] 18 } 19 }, 20 "test": { 21 "executor": "@nx/jest:jest", 22 "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], 23 "options": { 24 "jestConfig": "{projectRoot}/jest.config.ts", 25 "passWithNoTests": true 26 }, 27 "configurations": { 28 "ci": { 29 "ci": true, 30 "codeCoverage": true 31 } 32 } 33 } 34 } 35} 36

Now the project.json files can be reduced to this:

libs/lib1/project.json
1{ 2 "name": "lib1", 3 "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 "sourceRoot": "libs/lib1/src", 5 "projectType": "library", 6 "targets": { 7 "build": { 8 "options": { 9 "assets": ["libs/lib1/*.md", "libs/lib1/src/images/*"] 10 } 11 }, 12 "lint": {}, 13 "test": {} 14 }, 15 "tags": [] 16} 17
Target defaults

This recipe assumes every target with the same name uses the same executor. If you have targets with the same name using different executors and you're providing target defaults for executor options, don't place the executor options under a default target using the target name as the key. Instead, separate target default configurations can be added using the executors as the keys, each with their specific configuration.

Ramifications

This change adds 33 lines of code to nx.json and removes 84 lines of code from the project.json files. That's a net reduction of 51 lines of code. And you'll get more benefits from this strategy the more projects you have in your repo.

Reducing lines of code is nice, but just like using the DRY principle in code, there are other benefits:

  • You can easily change the default settings for the whole repository in one location.
  • When looking at a single project, it is clear how it differs from the defaults.
Don't Over Do It

You need to be careful to only put configuration settings in the targetDefaults that are actually defaults for the whole repository. If you have to make exceptions for most of the projects in your repository, then that setting probably should not be a default.