🚡 Nx Targets Elevated (Part Two)

Streamlining Target Centralization: A Structured Approach for Various Project Types

Jonathan Gelin
5 min readJul 25, 2023

That version of the Nx plugin will be deprecated in v18. Please check the new way of doing in my article 💎 Discovering Nx Project Crystal’s Magic

In the first part of that article, we explored how to centralize targets within a large codebase using an Nx plugin.

We learned about the essential role of the registerProjectTargets function, which helps manage targets efficiently whenever the run command is executed on a project.

Now, let’s dive into a more sophisticated approach by implementing naming conventions and creating a system to support different project types effectively.

Project Types and Naming Conventions

To streamline target centralization, adhering to conventions for repository architecture becomes essential. You can find more info in the article below.

By employing a consistent naming convention for each project and defining its type, we can significantly enhance the effectiveness of managing targets.

Consider the following valuable insights from the following recommended approaches:

Let’s establish a simple naming convention to illustrate this:

By aligning project names with this specific pattern, we can discern their project types effortlessly.

Feel free to adjust and adapt these conventions to suit your architecture better.

Target Routing

Having determined the project type from its name, we can now create a routing mechanism to associate each project type with its corresponding registerProjectTargets function. This ensures that whenever we need to retrieve the list of targets for a project, we can call the appropriate function based on its type.

Each time we need to retrieve the list of targets of a project, we will get the project type and call the corresponding function.

Implementation

Now, let’s delve into the implementation details and set up the necessary utilities and functions to achieve target centralization.

You can find the full example on my repository here: https://github.com/jogelin/register-nx-targets

Project Type Utility

To support our naming convention, we’ll create a utility to handle project types. This utility will help us categorize projects based on their names:

import { normalizePath } from 'nx/src/utils/path';

export type ProjectType =
| 'APP'
| 'E2E'
| 'LIB_DOMAIN'
| 'LIB_FEATURE'
| 'LIB_UTILS'
| 'LIB_UI';

const projectTypeRegExps: Record<ProjectType, RegExp> = {
APP: /^packages\/(.+)-app\/project.json$/,
E2E: /^packages\/(.+)-e2e\/project.json$/,
LIB_DOMAIN: /^packages\/domain-(.*)\/project.json$/,
LIB_FEATURE: /^packages\/feature-(.*)\/project.json$/,
LIB_UTILS: /^packages\/utils-(.*)\/project.json$/,
LIB_UI: /^packages\/ui-(.*)\/project.json$/,
};

export const getProjectType = (projectPath: string): ProjectType | undefined =>
Object.entries(projectTypeRegExps).find(([, regExp]) =>
regExp.test(normalizePath(projectPath))
)?.[0] as ProjectType;

export const getProjectRoot = (projectPath: string): string => {
const normalizedPath = normalizePath(projectPath);
return normalizedPath.replace('/project.json', '');
};

export const getProjectName = (projectPath: string): string =>
getProjectRoot(projectPath).split('/').pop();

Main Registering Project Targets Function

Now, let’s set up the main registerProjectTargets function that handles the registration of targets for different project types:

import { TargetConfiguration } from 'nx/src/config/workspace-json-project-json';
import { getProjectType, ProjectType } from './utils/project-utils';
import { registerAppTargets } from './application/register-targets';
import { registerE2ETargets } from './e2e/register-targets';
import { registerLibDomainTargets } from './library/domain/register-targets';
import { registerLibFeatureTargets } from './library/feature/register-targets';
import { registerLibUITargets } from './library/ui/register-targets';
import { registerLibUtilsTargets } from './library/utils/register-targets';

export const projectFilePatterns = ['project.json'];

const projectTypeToRegisterRouting: Record<
ProjectType,
(projectPath: string) => Record<string, TargetConfiguration>
> = {
APP: registerAppTargets,
E2E: registerE2ETargets,
LIB_DOMAIN: registerLibDomainTargets,
LIB_FEATURE: registerLibFeatureTargets,
LIB_UI: registerLibUITargets,
LIB_UTILS: registerLibUtilsTargets,
};

export function registerProjectTargets(
projectPath: string
): Record<string, TargetConfiguration> {
const projectType = getProjectType(projectPath);
const registerProjectTargetFn = projectTypeToRegisterRouting[projectType];

return registerProjectTargetFn ? registerProjectTargetFn(projectPath) : {};
}

Specific Registering Project Targets Function

Now, let’s implement a specialized function for a specific project type — the E2E type, in this case:

import { TargetConfiguration } from 'nx/src/config/workspace-json-project-json';
import { memoize } from '../../utils/memoize-utils';
import { getProjectRoot } from '../../utils/project-utils';

export function registerE2ETargets(
projectPath: string
): Record<string, TargetConfiguration> {
return generateTargetsMemoized(projectPath);
}

export const generateE2ETargets = (
projectPath: string
): Record<string, TargetConfiguration> => {
const projectRoot = getProjectRoot(projectPath);

return {
e2e: generateE2ETarget(projectRoot),
lint: generateLintTarget(projectRoot),
};
};

const generateTargetsMemoized = memoize(generateE2ETargets);

const generateE2ETarget = (projectRoot: string): TargetConfiguration => ({
executor: '@nx/cypress:cypress',
options: {
cypressConfig: `${projectRoot}/cypress.config.ts`,
devServerTarget: 'my-app:serve:development',
testingType: 'e2e',
},
configurations: {
production: {
devServerTarget: 'my-app:serve:production',
},
},
});

const generateLintTarget = (projectRoot: string): TargetConfiguration => ({
executor: '@nx/linter:eslint',
outputs: ['{options.outputFile}'],
options: {
lintFilePatterns: [`${projectRoot}/**/*.{js,ts}`],
},
});

And voila! You can now maintain your targets per project in one centralized location!

Conclusion

In the first part of that article, we explored the technical aspects of centralizing targets using an Nx plugin. By leveraging the registerProjectTargets function, we learned how to streamline target management, avoid duplication, and enhance development efficiency.

In this second part, we took a more sophisticated approach to optimize target centralization. By implementing a clear and structured naming convention for each project, we were able to categorize projects based on their types. This categorization enables us to route targets more effectively, providing a tailored configuration for each project type.

By introducing naming conventions, not only do we benefit target centralization, but we also unlock the potential for implementing multiple utilities across your repository. By adhering to consistent naming conventions, you can extend this approach to support various custom targets and configurations, all managed from a centralized location.

🚀 Stay Tuned!

Looking for some help? 🤝
Connect with me on Twitter • LinkedIn • Github

--

--

Jonathan Gelin

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