Nx Workspaces - Target Defaults

Nx Workspaces - Target Defaults

Posted on
Last update on

Target audience

This article is intented to help anybody using Nx workspaces. These instructions are based on the latest version of Nx, at the time of writing 16.5.5. They could work for earlier / later versions, but might need some tweaking in the configuration.

Introduction

Each project (app or library) in an Nx workspace comes with different commands, or targets, that can be executed. For example to build, test or lint your project, just to give the 3 most common examples.

If you use task executors, all apps and libraries in an Nx workspace come with a project.json file that holds configuration for these commands for each individual project. If you use npm scripts, the configuration will mostly be in the package.json file of the projects. Anyway, Nx will always merge the two files to come to the full project configuration.

Because every project, app or library, has similar configuration for its targets, it can sometimes become cumbersome to keep the configuration up-to-date across all of them. Especially if you have a lot of libraries, which is very common. Instead of defining the same configuration acros all of our projects, we can leverage target defaults so that we don't need to maintain the same configuration everywhere.

Example Setup

Let's start with an example setup where we have different feature libraries. Each feature library holds the source code for a specific section of our application. Let's keep it simple and use a CMS as an example. In this CMS we will be managing products, sales and clients.

Library setup

So we have 3 feature libraries; feature-products, feature-sales and feature-clients. For each of these libraries we generated a Storybook setup. So all of them have the same project.json that looks something like this:

./libs/feature-products/project.json

{
    "name": "feature-products",
    "$schema": "../../../node_modules/nx/schemas/project-schema.json",
    "projectType": "library",
    "sourceRoot": "libs/feature-products/src",
    "prefix": "sv-fp",
    "targets": {
    "test": {
        "executor": "@nx/jest:jest",
        "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
        "options": {
            "jestConfig": "libs/feature-products/jest.config.ts",
            "passWithNoTests": true
        }
    },
    "lint": {
        "executor": "@nx/linter:eslint",
        "options": {
            "lintFilePatterns": [
                "libs/feature-products/**/*.ts",
                "libs/feature-products/**/*.html"
            ]
        }
    },
    "storybook": {
        "executor": "@storybook/angular:start-storybook",
        "options": {
            "port": 4400,
            "configDir": "libs/feature-products/.storybook",
            "browserTarget": "feature-products:build-storybook",
            "compodoc": false
        },
        "configurations": {
            "ci": {
                "quiet": true
            }
        }
    },
    "build-storybook": {
        "executor": "@storybook/angular:build-storybook",
        "outputs": ["{options.outputDir}"],
        "options": {
            "outputDir": "dist/feature-reports",
            "configDir": "libs/feature-products/.storybook",
            "browserTarget": "feature-products:build-storybook",
            "compodoc": false
        },
        "configurations": {
            "ci": {
                "quiet": true
            }
        }
    },
    "tags": ["type:feature"]
}

Adding shared styles

UI Design system project library

Each of these libraries obviously have their own set of css styling, but like with any other project there are some shared styles, for example typography or color-schemes.

Imagine those styles are defined within the "Design System" UI library, ui-design-system, located on the same level as the feature libraries. The UI library setup looks like the screenshot shown.

When running the Storybook setup we want to be able to load these styles the same way as we would be running the application integrating all of the feature and UI libraries. So we extend the Storybook configuration a bit more, by adding the styles and stylePreprocessorOptions options to the build-storybook target in the project.json of all our feature libraries:

./libs/feature-products/project.json

{
    // ...
    "build-storybook": {
        "executor": "@storybook/angular:build-storybook",
        "outputs": ["{options.outputDir}"],
        "options": {
            "styles": [
                "libs/ui-design-system/src/lib/styles/_storybook.scss",
                "libs/ui-design-system/src/lib/styles/_main.scss"
            ],
            "stylePreprocessorOptions": {
                "includePaths": ["libs/ui-design-system/src/lib/styles"]
            },
            "outputDir": "dist/storybook/feature-products",
            "configDir": "libs/feature-products/.storybook",
            "browserTarget": "feature-products:build-storybook",
            "compodoc": false
        },
        "configurations": {
            "ci": {
                "quiet": true
            }
        }
    },
    "tags": ["type:feature"]
}

On line 7-10 we import some scss files with styles that should be loaded globally. It contains for example the typography and basic styles. On line 11-13 we define the import / include paths for the @import statements to use as a base for resolving files.

Using Target Defaults

Imagine having several, 10 or more, applications and hundreds, if not thousands, of feature libraries in your monorepository where we constantly need to add and keep this configuration up-to-date. Nearly impossible to not forget it at some places at a first try!

Target defaults to the rescue! Instead of defining this same configuration in each project.json we can define it once in the nx.json configuration file (line 13-21):

./nx.json

{
    // ..
    "$schema": "./node_modules/nx/schemas/nx-schema.json",
    "targetDefaults": {
      "build-storybook": {
          "inputs": [
              "default",
              "^production",
              "{workspaceRoot}/.storybook/**/*",
              "{projectRoot}/.storybook/**/*",
              "{projectRoot}/tsconfig.storybook.json"
          ],
          "options": {
              "styles": [
                  "libs/ui-design-system/src/lib/styles/kor/_storybook.scss",
                  "libs/ui-design-system/src/lib/styles/kor/_main.scss"
              ],
              "stylePreprocessorOptions": {
                  "includePaths": ["libs/ui-design-system/src/lib/styles"]
              }
          }
      },
      // ...
    },
    // ...
}

Nx will now use the default configuration provided if not overwritten at the library / application level.

Another example

Another, and even more simple example, would be to override the default port to serve your applications to 5000. To achieve this, the only changes needed to your nx.json file are the following:

./nx.json

{
    // ..
    "$schema": "./node_modules/nx/schemas/nx-schema.json",
    "targetDefaults": {
        "serve": {
            "options": {
                "port": 5000
            }
        },
        // ...
    },
    // ...
}

Every application will now be served on port 5000 by default, unless you overwrite it in the project.json of the specific application.

Conclusion

Repeating yourself is generally considered a bad practice in clean code principles. Especially when the tools give you the power to define what you had to repeat for each project on a more global / default level. Just always remember to keep it as simple as you can!

In this specific case, we pulled default configuration of Storybook up into the global nx.json, which we beforehand added to the project.json for each library separately. This makes it easier to maintain for all existing projects (apps and libraries) and will be automatically applied to new projects as well.

Further reading

  1. Nx Docs: Project Configuration
  2. Nx Docs: nx.json > Target Defaults

Contents

  1. Target audience
  2. Introduction
  3. Example Setup
  4. Library setup
  5. Adding shared styles
  6. Using Target Defaults
  7. Another example
  8. Conclusion
  9. Further reading

By reading this article I hope you can find a solution for your problem. If it still seems a little bit unclear, you can hire me for helping you solve your specific problem or use case. Sometimes even just a quick code review or second opinion can make a great difference.