✌️ Nx Plugin v2: Dynamic Project Configurations

Unlocking the Full Potential of Nx: A Comprehensive Guide to Centralizing Project Configurations for Enhanced Efficiency

Jonathan Gelin
4 min readAug 29, 2023
Photo by Thomas Couillard on Unsplash

Please check the new way of doing in my article 💎 Discovering Nx Project Crystal’s Magic

In my earlier articles, Nx Targets Elevated (Part One) and Nx Targets Elevated (Part Two), I discussed how project target configurations could be centralized and dynamically assigned to a project by using naming conventions.

However, I was utilizing the NxPluginV1, and this version of the plugin has now been deprecated and removed in Nx v18. In this article, I’ll be suggesting the same approach but using the new interface of NxPluginV2.

How does it work?

When you initiate a target build for the my-app project, the run command will not only consider configurations specified in the associated project.json but also check for configurations provided by registered Nx plugins.

By merging all the related configurations, it efficiently calls a consolidated build target.

This means that we can define all reusable configuration within a local plugin instead of cluttering our project.json.

How to do it?

Create an Nx plugin

The initial step in centralizing targets is to create an Nx plugin. Execute the following command:

$ nx g @nx/plugin:plugin my-plugin

Don’t forget to register your plugin in your nx.json configuration file:

{
"$schema": "./node_modules/nx/schemas/nx-schema.json",
...
"plugins": ["@my-org/my-plugin"]
}

Implement CreateNodes in your plugin

To enable dynamic configuration, we need to implement and export the createdNodes function in the plugin's index.ts (NxPluginV2 API):

import { CreateNodes } from 'nx/src/utils/nx-plugin';
import { toProjectName } from 'nx/src/config/workspaces';
import { dirname } from 'path';

export const createNodes: CreateNodes = [
'packages/**/project.json',
(configFilePath, options, context) => {
const name = toProjectName(configFilePath);
return {
projects: {
[name]: {
name,
root: dirname(configFilePath),
targets: {
build:{
// ...
},
test:{
// ...
},
lint:{
// ...
}
}
},
},
};
},
];

As you can observe, the configuration mirrors that of your project.json target configuration.

However, instead of having static configurations, you can dynamically generate them using an Nx Plugin. This enables you to remove static configurations from your base project.json.

By utilizing the same root path and name, the related configurations can be merged together.

Dynamic configurations

The approach described above can be generalized to all project configurations in your Nx repository.

As I explained in my previous articles, Nx Target Elevated (Part Two) and The Super Power of Conventions with Nx, employing conventions for your projects can significantly aid in assigning dynamic configurations by just using the project file path.

Depending on how you want to group configurations, you can decide between having one plugin per type of configuration or a plugin to orchestrate configurations.

One plugin per type of configuration:

One recommended scenario is to group configurations in multiple plugins. In that way, you can include project configurations but also the generators and executors related.

Nx uses this approach by grouping executors and generators by technology type: @nx/angular, @nx/web, @nx/…

One plugin to orchestrate configurations:

However, if you want more advanced, more dynamic configurations, or if you want to centralize configurations in one place, you can use only one plugin to orchestrate and route all of the configurations.

Here is an example of how to orchestrate multiple configurations within one plugin:

type CreateProjectConfigFn = (projectRoot: string) => Omit<ProjectConfiguration, 'root'>;

const createAppProjectConfiguration: CreateProjectConfigFn = (projectRoot: string) => ({
targets: {
build: {
// ...
},
lint: {
// ...
},
test: {
// ...
},
},
});
const createFeatureProjectConfiguration: CreateProjectConfigFn = (projectRoot: string) => ({
// project configuration
});
const createE2EProjectConfiguration: CreateProjectConfigFn = (projectRoot: string) => ({
// project configuration
});

const filePathToConfigurationMapper: [projectFilePattern: string, createProjectConfigFn: CreateProjectConfigFn][] = [
['packages/**/*-app/project.json', createAppProjectConfiguration],
['packages/**/feature-*/project.json', createFeatureProjectConfiguration],
['packages/**/*-e2e/project.json', createE2EProjectConfiguration],
];

export const createNodes: CreateNodes = [
'packages/**/project.json',
(configFilePath, options, context) => {
const projectName = toProjectName(configFilePath);
const projectRoot = dirname(configFilePath);

const projectConfiguration = filePathToConfigurationMapper.reduce(
(config, [pattern, createProjectConfigFn]) =>
minimatch(configFilePath, pattern, { dot: true })
? { ...config, ...createProjectConfigFn(projectRoot) }
: config,
{}
);

// return projects only if custom configs found
return Object.keys(projectConfiguration).length > 0
? {
projects: {
[projectName]: {
name: projectName,
root: projectRoot,
...projectConfiguration,
},
},
}
: {};
},
];

Conclusion

As you can see, in the new version of the Nx plugin, you can now easily generate the entire configuration for the entire project.json!

By adopting this method, you can effortlessly maintain your architecture without duplicating configurations. Any modifications made to configurations will only need to be applied once, benefiting all projects using them.

You can decide to distribute them across multiple Nx plugins or use one Nx Plugin to govern them all.

Additionally, this streamlined approach simplifies generators, as they’ll generate fewer configurations.

🚀 Stay Tuned!

Looking for some help? 🤝
Connect with me on TwitterLinkedIn Github

--

--

Jonathan Gelin

Write & Share about SDLC/Architecture in Ts/Js, Nx, Angular, DevOps, XP, Micro Frontends, NodeJs, Cypress/Playwright, Storybook, Tailwind • Based in 🇪🇺