Nx Workspaces - Target Defaults
Posted on
Last update on
Target audience
This article is intended 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/eslint: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
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
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.