⛔ Target Exclusions in Nx Project Crystal

Exploring Workarounds and Custom Solutions for Excluding Targets in the Early Days of Nx 18

Jonathan Gelin
JavaScript in Plain English

--

UPDATE 2024–04-03:

An official way now exists to exclude or include plugins for a specific project:

If we take the example below, you can now use the approach:

{
"plugins": [
{
"plugin": "@nx/jest/plugin",
"options": {
"targetName": "test",
"exclude": ["my-lib-e2e/**/*"]
}
}
]
}

Since the release of Nx 18, many repositories have started the adoption of Nx Project Crystal. As it’s still the beginning, the feature to exclude some projects from the dynamic target assignment is not supported yet.

In this article, I want to share workarounds and ideas until the Nx team implements something internally.

If you’re looking to understand how Nx assigns targets to projects dynamically, I first recommend reading my article:

Workarounds

Let’s specify the context by mentioning that you have a project named my-lib-e2e and you don't want that project to have a test target even if it contains a jest.config.ts. However, you still want to get the benefit of the @nx/jest/plugin for your other 1000 Nx projects 😋.

Manually Override the “test” Target with nx:noop

In your my-lib-e2e/project.json:

{
"name": "my-lib-e2e",
"targets": {
"test": {}
}
}

Given that project.json takes precedence over all plugins, the inferred test target from the @nx/jest/plugin will be overridden.

Dynamically Override Any Target with nx:noop

You can also develop a plugin to automatically assign nx:noop on any target. For instance, I crafted a plugin located at tools/nx-plugins/targets-noopify.ts:

import { CreateNodes, ProjectConfiguration, readJsonFile } from '@nx/devkit';
import { dirname, join } from 'node:path';

interface TargetsNoopifyOptions {
projectName: string;
targets: string[];
}

export const createNodes: CreateNodes<TargetsNoopifyOptions> = [
'**/project.json',
async (configFilePath, options, { workspaceRoot }) => {
const projectRoot = dirname(configFilePath);

// get project name from porject.json
const projectJson = readJsonFile<ProjectConfiguration>(
join(workspaceRoot, configFilePath)
);
const projectName = projectJson.name;

// check if the target of the project should be noopified
const targetsToStub = options[projectName];
if (!targetsToStub) {
return {};
}

// override the targets
const targets = targetsToStub.reduce(
(acc, targetName) => ({
...acc,
[targetName]: {},
}),
{}
);

return {
projects: {
[projectRoot]: {
root: projectRoot,
targets: targets,
},
},
};
},
];

This plugin should then be listed last in nx.json:

{
"plugins": [
{
"plugin": "@nx/jest/plugin",
"options": {
"targetName": "test"
}
},
// MAKE SURE THIS COMES AFTER THE PLUGIN YOU WISH TO OVERRIDE
{
"plugin": "./tools/nx-plugins/targets-noopify.ts",
"options": {
"my-lib-e2e": ["test"]
}
}
]
}

Create Your Custom @nx/jest/plugin and Skip the Projects

Alternatively, you could implement your custom Jest plugin that filters projects. Here’s an example plugin tools/nx-plugins/skip-jest-plugin.ts:

import {
CreateNodes,
joinPathFragments,
ProjectConfiguration,
readJsonFile,
} from '@nx/devkit';
import { dirname, join } from 'node:path';
import {
JestPluginOptions,
createNodes as createJestNodes,
} from '@nx/jest/plugin';
import { readdirSync } from 'fs';

export const createNodes: CreateNodes<
JestPluginOptions & { skipProjects: string[] }
> = [
createJestNodes[0],
async (configFilePath, options, context) => {
const projectRoot = dirname(configFilePath);

const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot));
if (!siblingFiles.includes('project.json')) {
return {};
}

const path = joinPathFragments(projectRoot, 'project.json');
const projectJson = readJsonFile<ProjectConfiguration>(path);
const projectName = projectJson.name;

if (options.skipProjects.includes(projectName)) {
return {};
}

return createJestNodes[1](configFilePath, options, context);
},
];

And then, in the nx.json, it should replace @nx/jest/plugin with:

{
"plugins": [
// HERE, REPLACE "@nx/jest/plugin" WITH YOUR CUSTOM PLUGIN
{
"plugin": "./tools/nx-plugins/skip-jest-plugin.ts",
"options": {
"targetName": "test",
"skipProjects": ["my-lib-e2e"]
}
}
]
}

Ideas

As Max Kless commented, they are open to ideas, so I jumped into it 🙂

Skip projects per plugin

This is related to the last workaround described above but integrated into Project Crystal. For example, you would have an nx.json like:

{
"plugins": [
{
"plugin": "@nx/jest/plugin",
"options": {
"targetName": "test",
"skipProjects": ["my-lib-e2e"] // Could be a pattern instead
}
}
]
}

✅ Specific per plugin
⛔ Duplication in each plugin
⛔ If the plugin adds multiple targets, it is not possible to filter only one

Add “projects” property on targetDefaults

This strategy takes a leaf from the Nx Release workflow, applies it to targetDefaults for more granular control:

{
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"targetDefaults": {
"test": {
"cache": true,
"inputs": ["production", "^production"],
"projects": ["*", "!my-lib-e2e"]
},
}
}

✅ Specific per target
✅ Centralized
✅ Re-use existing from Release
⛔ Cannot be specified by executor because the uses Nx Command
⛔ It does not reflect what you can specify in your Project Configuration

In Conclusion

Got more ideas? Feel free to share, and I’ll eagerly incorporate them. Let’s keep the conversation going and make Nx Project Crystal even more versatile.

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

--

--

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