🚡 Nx Targets Elevated (Part Two)
Streamlining Target Centralization: A Structured Approach for Various Project Types
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:
- Nx official recommendations
- Domain-Driven Design from Manfred Steyer
- The Angular developer’s Nx handbook from Lars Gyrup Brink Nielsen
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!