Overview
After delving into this feature in my previous articles:
- 🚡 Nx Targets Elevated (Part One)
- 🚡 Nx Targets Elevated (Part Two)
- ✌️ Nx Plugin v2: Dynamic Project Configurations
I’m excited to discuss again one of my favorite Nx features: “The Inferred Project Configurations.” The Nx team now introduces a unified approach known as Nx Project Crystal 💎.
In this article, I’ll cover:
· The Origin
· Project Crystal in a Nutshell
· How it works
· Create Your Cyrstal Plugin
· A Multitude of Benefits
· Cautions
The Origin
To grasp the concept of Nx Project Crystal 💎, let’s revisit the Nx team’s journey in project configuration.
1. angular.json
“In the beginning, there was nothing”. The Nx team began defining a monorepo structure for apps and libs using angular.json
, tied to Angular CLI:
However, this initial approach was specific to Angular, lacking generality and extensibility.
2. workspace.json
A v2 schema introduced theworkspace.json
files*.* Some concepts were generalized such as the executors (instead of a builder) or the generators (instead of schematics).
The new format proved useful due to its extensibility and generality. However, maintaining a large codebase within a single file presented challenges. Consequently, the demand for greater flexibility and project-specific specifications increased.
3. project.json
Thus, the concept of distributed configuration emerged with the project.json
(or package.json) file. It maintained the same format but was divided and specified at the root of each project.
Defining a project has become straightforward — simply add that file, and you’re done. However, the proliferation of files implies challenging maintenance due to significant duplication. This situation necessitated the development of migration scripts to manage these configurations effectively.
Additionally, there emerged a requirement to support technologies not adhering to Nx’s standards, enabling the integration of Nx benefits into existing repositories and accommodating various technologies, including .Net
, among others.
4. Inferred configuration
Then, project inference appeared, introducing the possibility of assigning project configurations simply through glob pattern matching. An Nx plugin exposes specific functions that are automatically loaded by the Nx core, and voilà, you have dynamic configuration :)
It began with Plugin v1, which only assigned a list of targets for existing projects (see 🚡 Nx Targets Elevated).
Then came Plugin v2, offering the ability to dynamically add project nodes with full configuration, even if they do not exist in your codebase (see ✌️ Nx Plugin v2: Dynamic Project Configurations).
And finally, a consolidated approach to that concept was integrated into Nx:
Project Crystal in a Nutshell
The Nx Project Crystal offers the ability to utilize Nx plugins that automatically add tasks to your projects based on the configuration files of various tools.
I recommend watching the great video Nx — Project Crystal and reading the related article What if Nx Plugins Were More Like VSCode Extensions by Juri Strumpflohner.
The Nx team likes to compare it to a simple plugin that you’ll add to your favorite IDE. You don’t need to do anything else; the plugin activates functionalities automatically because it recognizes specific configurations or patterns in your workspace.
For example, if you want to use Vite to build your application, you simply need to add the @nx/vite plugin to your Nx workspace by using:
nx add @nx/vite
Automatically, all projects that contain a vite.config.*
file will have new tasks assigned. You will be able to use build
, preview
, test
, serve
and serve-static
directly:
This is true for Vite, but it is also applies to many other plugins, such as:
@nx/webpack →
build
,preview
,serve
andserve-static
@nx/cypress →
e2e
,e2e-ci
andcomponent-test
@nx/playwright →
e2e
,e2e-ci
@nx/eslint →
lint
@nx/expo →
start
,serve
,run-ios
,run-android
,export
,prebuild
,install
,build
andsubmit
@nx/jest →
test
@nx/next →
build
,dev
,start
andserve-static
@nx/nuxt →
build
,test
andserve
@nx/react-native →
start
,pod-install
,bundle
,run-ios
,run-android
,build-io
andbuild-android
@nx/remix →
build
,serve
,start
andtypecheck
@nx/storybook →
build-storybook
,storybook
,test-storybook
andstatic-storybook
Why don’t I see the main plugins like @nx/angular or @nx/react?
Your Angular/React project configurations have already been simplified through the removal of common targets like lint
or test
.
However, it’s challenging to generalize targets like build or serve because they involve many specifications unique to your app.
IMO, it’s only a matter of time before the Nx team proposes a solution for this :).
How it works
Let’s take a look behind the scenes to understand how we can benefit from this feature.
As you can see, Nx computes the project graph configuration by loading configurations from multiple places:
First, it iterates over the plugin list declared in your
nx.json
and calls thecreateNodes
function for each.Then, it reads
targetDefaults
in thenx.json
file and it applies the default configuration to the corresponding targets.Finally, it uses the configuration from the
project.json
(or package.json), if specified at the root of the project.
How do I know which task is available at the end?
Nx provided a solution for that too! You can explore your Nx workspace and see the consolidated configuration simply by running the command:
nx show project myreactapp --web
And you will be able to visualize all of the configurations for your project
This is also visible in the Nx console plugin in your IDE!
Create Your Cyrstal Plugin
Let’s explore how quickly you can implement your plugin, injecting your configurations and behaviors into your workspace.
Nx Plugin
The first step is to create an Nx plugin and configure it in your nx.json
.
It can be a simple *.ts
file located anywhere in your workspace that you register in your nx.json
:
{
"plugins": [..., "./tools/plugins/my-plugin.ts"],
}
Or you can use the plugin generator by executing the command:
nx g @nx/plugin:plugin my-plugin
And again, you’ll need to register your plugin in your nx.json
configuration file:
{
"plugins": [..., "@my-org/my-plugin"]
}
You can also provide options to your plugin by using the following syntax in your nx.josn
:
{
"plugins": [
...,
{
"plugin": "@my-org/my-plugin",
"options": {
"targetName": "build"
}
}
],
}
Export the createNodes from your plugin
Then, you can begin implementing your plugin. I highly recommend examining some existing Nx plugin implementations:
For simplicity, look at @nx/eslint or @nx/jest.
For more advanced task distribution, consider @nx/cypress or @nx/playwright.
For custom implementations, check out @nx-dotnet/core
I chose the @nx/vite plugin to illustrate the mechanism:
Many functions are omitted because they are specific to the plugin and not necessary to grasp the main structure of the plugin.
I divided the code into three main parts:
A. The plugin options contract: Typically used to define the names of the targets you wish to generate, but it can be used for anything.
B. The targets memoization: Since generating the configuration graph is resource-intensive, the memoization pattern is essential to prevent unnecessary computation when nothing changes.
C. The main createNodes
function: This function is invoked by Nx and returns new project configurations.
The
createNodes
function is a pair consisting of a glob pattern to determine when the plugin should be activated and a function that receives the activation context as a parameter and can return a project configuration.The
configFilePath
contains the path matching the glob pattern, representing the project’s root path.Even if the glob pattern matches, it’s possible to filter out and skip execution if it doesn’t meet a specific criterion. Here, the criterion is to assign tasks only to existing projects.
Since the options are optional, they simply provide a way to set default values for target names.
This forms the core of the function, generating all tasks or retrieving them from the cache if they exist.
It then creates and returns the project configuration to be incorporated into the global project configuration graph.
A Multitude of Benefits
You’ll gain from project inference and creating your plugin in several compelling use cases.
Remove duplication of configurations
Adhering to the “Don’t Repeat Yourself” principle becomes straightforward. If your workspace is cluttered with redundant project.json
configurations, creating plugins to inject common elements significantly eases long-term maintenance.
Centralize your workspace conventions
As I discussed in my previous article ⚡ The Super Power of Conventions with Nx, for teams managing large codebases, enhancing the development experience through established conventions is invaluable. These can be rules or best practices, encapsulated within an Nx plugin to uniformly apply across your workspace.
Speed up the Nx adoption on the existing repository!
That’s one of my favorite use cases, just add Nx and plugins to your existing repo, and you’ll access many functionalities automatically.
But before having the possibility to execute tasks, you need to ensure that Nx can see your projects. The first reflex is to create a project.json
file everywhere you want to declare a project.
However, it will be difficult if you have to define hundreds of project.json
with the same configuration. It can also be projects that are not even related to JavaScript projects. In that case, the creation of custom plugins will facilitate that definition in a short time.
For example, if you have a huge list of themes containing only *.scss
files like:
.
├── apps/
└── libs/
└── themes/
├── core/
├── blue/
│ ├── components/
│ ├── colors.scss
│ ├── variables.scss
│ └── index.scss
├── dark/
│ └── ...
├── light/
│ └── ...
├── yellow/
│ └── ...
└── pink/
└── ...
If you want to see the dependencies between the applications and the themes, you can simply create a plugin that will expose the themes as projects in Nx:
export const createNodes: CreateNodes = [
'libs/themes/**/index.scss',
(configFilePath, options, context) => {
const name = toProjectName(configFilePath);
return {
projects: {
[`theme-${name}`]: {
name,
root: dirname(configFilePath),
targets: {
build:{
// ...
}
}
},
},
};
},
];
Automatically, without having to create any project.json
file, you will see them in your dependency graph and gain the benefit of Nx features such as affected, caching, boundaries, etc.
Task Distributions on CI
Another interesting use case is the dynamic creation of tasks to parallelize executions that could be time-consuming.
By dividing long-running tasks into multiple tasks, you can benefit from the Nx Distribution Task Execution on CI and parallelize all tasks.
For example, Nx has implemented this approach for Cypress or Playwright by automatically splitting E2E tasks by file.
If you are interested in technical details, you can also read my article ⚡ Distributed e2e Task Execution with Nx for Playwright and Cypress
Less complex generators and migrations
When I started using the project inference, the main concern was “It will break the migration process of Nx!”
Indeed! But before Nx Project Crystal, project inference was only used for specific use cases with custom executors not covered by Nx migrations.
🤯 Previously, supporting advanced project configurations required creating complex generators for complex project.json
files.
Maintaining these was challenging. With every change, we needed to update the generator and create a migration script to apply modifications across all projects.
This was time-consuming and frustrating, especially when migrations were buggy.
😎 Now, with the project inference approach, you can centralize your specific configurations. If something changes, you simply update your plugins, and the changes are automatically applied to all projects.
No need for complex generators or migrations anymore!
Cautions
Plugins Order Matters!
As highlighted in the flow chart, configurations are not deeply merged, meaning if two plugins configure the same target name, only the last one will take precedence.
For instance, if your nx.json includes:
{
"plugins": ["@nx/cypress/plugin", "@nx/playwright/plugin"]
}
And a project contains both Cypress
and Playwright
tests, Nx will first invoke @nx/cypress
and then @nx/playwright
.
To address this, you can rename one of the targets using the plugin options in nx.json
:
{
"plugins": [
{
"plugin": "@nx/cypress/plugin",
"options": {
"targetName": "e2e-legacy"
}
},
"@nx/playwright/plugin"
]
}
This allows you to run both targets:
Configs Order Matters!
It’s also important to note that targetDefaults
configurations in your nx.json
take precedence over plugins.
For example, if a plugin returns a configuration like:
{
"my-app": {
"build": {
"executor": "@angular-devkit/build-angular:application",
"dependsOn": ["^build", "generate-api"]
},
"generate-api": {
"executor": "..."
}
}
}
But in your nx.json
, you’ve specified targetDefaults
like:
{
"targetDefaults": {
"build": {
"dependsOn": ["^build"]
}
}
}
Then your generate-api
target won’t be executed because the targetDefaults
will override the dependsOn
configuration.
I would recommend being specific in your targetDefaults
to prevent conflicts with the plugins.
{
"targetDefaults": {
"@angular-devkit/build-angular:application": {
"dependsOn": ["^build", "generate-api"]
}
}
}
Final Thoughts
The journey the Nx team embarked on to bring Project Crystal to life has been truly inspiring. From the early days of Angular-specific setups to the seamless approach offered by Project Crystal, every step has shown their dedication to making our lives as developers easier.
Looking ahead, the idea of Zero Configuration repositories sounds like an exciting leap forward. It promises a future where setting up and managing projects will be a breeze, giving us more time to focus on what we love: building awesome software.
So let’s embrace tools like Nx Project Crystal with open arms. They’re here to help us work smarter, not harder, and together, we can unlock endless possibilities for innovation and creativity in our development journeys.
Your feedback and suggestions are always welcome! 🚀 Stay Tuned!
Twitter — LinkedIn — Github
Top comments (0)