💎 Discovering Nx Project Crystal’s Magic
Tags: nx, react, angular, typescript
Published: 2024-02-26 17:01:51

---

## **Overview**

After delving into this feature in my previous articles:

* [🚡 Nx Targets Elevated (Part One)](https://jgelin.medium.com/nx-targets-elevated-part-one-f421062818e6)
* [🚡 Nx Targets Elevated (Part Two)](https://jgelin.medium.com/nx-targets-elevated-part-two-9edc56cf1a00)
* [✌️ Nx Plugin v2: Dynamic Project Configurations](https://jgelin.medium.com/%EF%B8%8F-nx-plugin-v2-dynamic-project-configurations-6055ba700105)

I’m excited to discuss again one of my favorite Nx features: **“The Inferred Project Configurations.”** The [Nx](https://medium.com/u/2817fb68583?source=post_page-----7f42faf2a135--------------------------------) team now introduces a unified approach known as [**Nx Project Crystal**](https://nx.dev/concepts/inferred-tasks) 💎.

**In this article, I’ll cover:**

· [The Origin](https://medium.com/@jgelin/discovering-nx-project-crystals-magic-7f42faf2a135#c775)
· [Project Crystal in a Nutshell](https://medium.com/@jgelin/discovering-nx-project-crystals-magic-7f42faf2a135#ea77)
· [How it works](https://medium.com/@jgelin/discovering-nx-project-crystals-magic-7f42faf2a135#7f8a)
· [Create Your Cyrstal Plugin](https://medium.com/@jgelin/discovering-nx-project-crystals-magic-7f42faf2a135#8a48)
· [A Multitude of Benefits](https://medium.com/@jgelin/discovering-nx-project-crystals-magic-7f42faf2a135#083b)
· [Cautions](https://medium.com/@jgelin/discovering-nx-project-crystals-magic-7f42faf2a135#aa78)

## **The Origin**

To grasp the concept of [**Nx Project Crystal**](https://nx.dev/concepts/inferred-tasks) 💎, let’s revisit the [Nx](https://medium.com/u/2817fb68583?source=post_page-----7f42faf2a135--------------------------------) team’s journey in project configuration.

### **1\. angular.json**

***“In the beginning, there was nothing”.*** The [Nx](https://medium.com/u/2817fb68583?source=post_page-----7f42faf2a135--------------------------------) team began defining a monorepo structure for apps and libs using [`angular.json`](https://angular.io/guide/workspace-config), tied to [Angular CLI](https://angular.io/cli):

![angular.json](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yu3i0tzu6qizs9hyxpva.png)

However, this initial approach was specific to Angular, lacking generality and extensibility.

### **2\. workspace.json**

A v2 schema introduced the[`workspace.json`](https://nx.dev/deprecated/workspace-json#workspacejson) files**.** Some concepts were generalized such as the [executors (instead of a builder) or the generators (instead of schematics).](https://nx.dev/concepts/more-concepts/nx-and-angular#executors-vs-builders-generators-vs-schematics)

![workspace.json](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i4snxsbg4hwwseyzwm5w.png)

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`](https://nx.dev/core-tutorial/02-create-cli#projectjson-syntax) (or package.json) file. It maintained the same format but was divided and specified at the root of each project.

![project.json](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nc6pzrf05ywgtvqduih0.png)

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](https://nx.dev/extending-nx/tutorials/create-plugin) exposes specific functions that are automatically loaded by the Nx core, and voilà, you have dynamic configuration :)

It began with [Plugin v1](https://nx.dev/deprecated/v1-nx-plugin-api), which only assigned a list of targets for existing projects (see [🚡 Nx Targets Elevated](https://jgelin.medium.com/nx-targets-elevated-part-one-f421062818e6)).

![Inferred configuration 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qqktsq8xreru0qyhqca5.png)

Then came [Plugin v2](https://nx.dev/extending-nx/recipes/project-graph-plugins), 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](https://jgelin.medium.com/%EF%B8%8F-nx-plugin-v2-dynamic-project-configurations-6055ba700105)).

![Inferred configuration 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/edriuce44tp49s1i0ypj.png)

And finally, a consolidated approach to that concept was integrated into Nx:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ztohwen80jbzo9087wu7.png)

## **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*](https://www.youtube.com/watch?v=wADNsVItnsM) *and reading the related article* [*What if Nx Plugins Were More Like VSCode Extensions*](https://blog.nrwl.io/what-if-nx-plugins-were-more-like-vscode-extensions-dcdad140ae09) *by* [Juri Strumpflohner](https://medium.com/u/71b649cf0618?source=post_page-----7f42faf2a135--------------------------------).

The [Nx](https://medium.com/u/2817fb68583?source=post_page-----7f42faf2a135--------------------------------) 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](https://vitejs.dev/) to build your application, you simply need to add the [@nx/vite plugin](https://nx.dev/nx-api/vite/documents/overview#how-nxvite-infers-tasks) to your Nx workspace by using:

```bash
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:

![inject vite target](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r6vp6wle72gbwt5qarn5.png)

This is true for Vite, but it is also applies to many other plugins, such as:

* [@nx/webpack](https://nx.dev/nx-api/webpack#how-nxwebpack-infers-tasks) → `build`, `preview`, `serve` and `serve-static`
    
* [@nx/cypress](https://nx.dev/nx-api/cypress/documents/overview#how-nxcypress-infers-tasks) → `e2e`, `e2e-ci` and `component-test`
    
* [@nx/playwright](https://nx.dev/nx-api/playwright/documents/overview#how-nxplaywright-infers-tasks) → `e2e`, `e2e-ci`
    
* [@nx](https://nx.dev/nx-api/detox/documents/overview#how-nxdetox-infers-tasks)/[detox](https://nx.dev/nx-api/detox/documents/overview#how-nxdetox-infers-tasks) → `build`, `start` and `test`
    
* [@nx/eslint](https://nx.dev/nx-api/eslint/documents/overview#how-nxeslint-infers-tasks) → `lint`
    
* [@nx/expo](https://nx.dev/nx-api/expo/documents/overview#how-nxexpo-infers-tasks) → `start`, `serve`, `run-ios`, `run-android`, `export`, `prebuild`, `install`, `build` and `submit`
    
* [@nx/jest](https://nx.dev/nx-api/jest/documents/overview#how-nxjest-infers-tasks) → `test`
    
* [@nx/next](https://nx.dev/nx-api/next/documents/overview#how-nxnext-infers-tasks) → `build`, `dev`, `start` and `serve-static`
    
* [@nx/nuxt](https://nx.dev/nx-api/nuxt/documents/overview#how-nxnuxt-infers-tasks) → `build`, `test` and `serve`
    
* [@nx/react-native](https://nx.dev/nx-api/react-native/documents/overview#how-nxreactnative-infers-tasks) → `start`, `pod-install`, `bundle`, `run-ios`, `run-android`, `build-io` and `build-android`
    
* [@nx/remix](https://nx.dev/nx-api/remix/documents/overview#how-nxremix-infers-tasks) → `build`, `serve`, `start` and `typecheck`
    
* [@nx/storybook](https://nx.dev/nx-api/storybook/documents/overview#how-nxstorybook-infers-tasks) → `build-storybook`, `storybook`, `test-storybook` and `static-storybook`
    

**Why don’t I see the main plugins like** [**@nx/angular**](https://nx.dev/nx-api/angular/documents/overview) **or** [**@nx/react**](https://nx.dev/nx-api/react/documents/overview)**?**

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](https://medium.com/u/2817fb68583?source=post_page-----7f42faf2a135--------------------------------) 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.

![How it works](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/go6qldso543j0bi7djeb.png)


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 the `createNodes` function for each.
    
* Then, it reads `targetDefaults` in the `nx.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](https://nx.dev/features/explore-graph) and see the consolidated configuration simply by running the command:

```bash
nx show project myreactapp --web
```

And you will be able to visualize all of the configurations for your project

![UI configs](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dor861hsxj4lg9qw72q3.png)

> *This is also visible in the* [*Nx console plugin*](https://nx.dev/recipes/nx-console) *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](https://nx.dev/plugins/tutorials/create-plugin#create-a-local-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`](https://nx.dev/plugins/recipes/project-inference-plugins#adding-plugins-to-workspace):

```json
{
  "plugins": [..., "./tools/plugins/my-plugin.ts"],
}
```

Or you can use the plugin generator by executing the command:

```bash
nx g @nx/plugin:plugin my-plugin
```

And again, you’ll need to [register your plugin in your `nx.json`](https://nx.dev/plugins/recipes/project-inference-plugins#adding-plugins-to-workspace) configuration file:

```json
{
  "plugins": [..., "@my-org/my-plugin"]
}
```

You can also provide options to your plugin by using the following syntax in your `nx.josn`:

```json
{
  "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](https://github.com/nrwl/nx/blob/master/packages/eslint/src/plugins/plugin.ts) or [@nx/jest](https://github.com/nrwl/nx/blob/master/packages/jest/src/plugins/plugin.ts).
    
* For more advanced task distribution, consider [@nx/cypress](https://github.com/nrwl/nx/blob/master/packages/cypress/src/plugins/plugin.ts) or [@nx/playwright](https://github.com/nrwl/nx/blob/master/packages/playwright/src/plugins/plugin.ts).
    
* For custom implementations, check out [@nx-dotnet/core](https://github.com/nx-dotnet/nx-dotnet/blob/84f35f133e35b1aa60b66d183bf8a3b50db6517a/packages/core/src/graph/create-nodes.ts#L97)
    

I chose the [@nx/vite](https://github.com/nrwl/nx/blob/master/packages/vite/src/plugins/plugin.ts) plugin to illustrate the mechanism:

![vite create target](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d2p55mb0gz0cykfr4ebu.png)

> *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**](https://en.wikipedia.org/wiki/Memoization) **pattern** is essential to prevent unnecessary computation when nothing changes.

**C. The main** [`createNodes`](https://nx.dev/extending-nx/recipes/project-graph-plugins) **function**: This function is invoked by Nx and returns new project configurations.

1. 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.
    
2. The `configFilePath` contains the path matching the glob pattern, representing the project’s root path.
    
3. 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.
    
4. Since the **options are optional**, they simply provide a way to set default values for target names.
    
5. This forms the core of the function, **generating all tasks or retrieving them from the cache** if they exist.
    
6. 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**](https://en.wikipedia.org/wiki/Don%27t_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](https://jgelin.medium.com/the-super-power-of-conventions-with-nx-8d418150b679), 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:

```plaintext
.
├── 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:

```typescript
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](https://nx.dev/ci/features/distribute-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](https://nx.dev/ci/features/split-e2e-tasks).

> *If you are interested in technical details, you can also read my article* [*⚡ Distributed e2e Task Execution with Nx for Playwright and Cypress*](https://medium.com/@jgelin/nx-distribute-e2e-task-execution-for-playwright-and-cypress-e3aa8811842a)

### **Less complex generators and migrations**

When I started using the project inference, the main concern was **“It will break the** [**migration process of Nx**](https://nx.dev/extending-nx/recipes/migration-generators)**!”**

**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**](https://nx.dev/concepts/inferred-tasks#plugin-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:

```plaintext
{
  "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`.

![overlap](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mu4c5sk1oahzyd6m1s7d.png)


**To address this**, you can rename one of the targets using the plugin options in `nx.json`:

```json
{
  "plugins": [
    {
      "plugin": "@nx/cypress/plugin",
      "options": {
        "targetName": "e2e-legacy"
      }
    },
    "@nx/playwright/plugin"
  ]
}
```

This allows you to run both targets:

![solution](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c21115h2rve94cdgq7zc.png)

### [**Configs Order Matters**](https://nx.dev/concepts/inferred-tasks#overriding-inferred-task-configuration)**!**

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:

```json
{
  "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:

```json
{
  "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.

```json
{
  "targetDefaults": {
    "@angular-devkit/build-angular:application": {
      "dependsOn": ["^build", "generate-api"]
    }
  }
}
```

## **Final Thoughts**

The journey the [Nx](https://medium.com/u/2817fb68583?source=post_page-----7f42faf2a135--------------------------------) 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**](https://twitter.com/jonathan_gelin) — [**LinkedIn**](https://www.linkedin.com/in/jonathan-gelin/) **—** [**Github**](https://github.com/jogelin)