For local conformance rules, the resolution utilities from @nx/js are used in the same way they are for all other JavaScript/TypeScript files in Nx. Therefore, you can simply reference an adhoc JavaScript file or TypeScript file in your "rule" property (as long as the path is resolvable based on your package manager and/or tsconfig setup), and the rule will be loaded/transpiled as needed. The rule implementation file should also have a schema.json file next to it that defines the available rule options, if any.
In practice, writing your local conformance rules in an Nx generated library is the easiest way to organize them and ensure that they are easily resolvable via TypeScript. The library in question could also be an Nx plugin, but it does not have to be.
Generate a Conformance Rule
Section titled “Generate a Conformance Rule”To write your own conformance rule, run the @nx/conformance:create-rule generator and answer the prompts.
NX Generating @nx/conformance:create-rule
✔ What is the name of the rule? · local-conformance-rule-example✔ Which directory do you want to create the rule directory in? · packages/my-plugin/local-conformance-rule✔ What category does this rule belong to? · security✔ What is the description of the rule? · an example of a conformance ruleCREATE packages/my-plugin/local-conformance-rule/local-conformance-rule-example/index.tsCREATE packages/my-plugin/local-conformance-rule/local-conformance-rule-example/schema.jsonThe generated rule definition file should look like this:
import { createConformanceRule, ConformanceViolation } from '@nx/conformance';
export default createConformanceRule({ name: 'local-conformance-rule-example', category: 'security', description: 'an example of a conformance rule', implementation: async (context) => { const violations: ConformanceViolation[] = [];
return { severity: 'low', details: { violations, }, }; },});To enable the rule, you need to register it in the nx.json file.
{ "conformance": { "rules": [ { "rule": "./packages/my-plugin/local-conformance-rule/index.ts" } ] }}Note that the severity of the error is defined by the rule author and can be adjusted based on the specific violations that are found.
Understanding Rule Context
Section titled “Understanding Rule Context”The implementation function of the rule is passed a context object which contains:
tree: AReadOnlyConformanceTreethat can be used to read files from the workspace instead of directly from disk. Useful for unit testing rules as a test tree can be provided to the rule implementation (see Testing Conformance Rules).projectGraph: The Nx project graphfileMapCache: The Nx file map cacheruleOptions: The resolved rule configuration options based on the current workspace
Violation Interface
Section titled “Violation Interface”Violations must follow this interface:
interface ConformanceViolation { message: string; file?: string; // Used if the violation is attributed to a specific file sourceProject?: string; // Used if the violation is attributed to a specific project workspaceViolation?: boolean; // Used if the violation is attributed to the entire workspace}Automatic Project Inference
Section titled “Automatic Project Inference”violations.push({ message: 'File violates standards', file: 'libs/my-lib/src/problematic.ts', // sourceProject auto-inferred as 'my-lib'});Best Practices for Violations
Section titled “Best Practices for Violations”- Use
workspaceViolation: truefor issues affecting the entire workspace (global configs, workspace structure, etc.) - Use
sourceProjectonly for project-wide issues (missing configuration, structure problems) - Use
file(and optional explicitsourceProject) for violations tied to specific files in projects - Use
fileonly for files that may not belong to projects (CI configs, root files, etc.)
Conformance Rule Examples
Section titled “Conformance Rule Examples”The following examples demonstrate how to write rules that report violations at different scopes.
This rule checks to see if there is a root README.md file in the workspace, and if there is not, it reports on the workspace itself.
import { workspaceRoot } from '@nx/devkit';import { createConformanceRule, ConformanceViolation } from '@nx/conformance';import { join } from 'node:path';import { existsSync } from 'node:fs';
export default createConformanceRule<object>({ name: 'readme-file', category: 'maintainability', description: 'The workspace should have a root README.md file', implementation: async () => { const violations: ConformanceViolation[] = [];
const readmePath = join(workspaceRoot, 'README.md'); if (!existsSync(readmePath)) { violations.push({ message: 'The workspace should have a root README.md file', workspaceViolation: true, }); }
return { severity: 'low', details: { violations, }, }; },});The @nx/conformance:ensure-owners rule provides us an example of how to write a rule that reports on a project being in violation of the rule. The @nx/owners plugin adds an owners metadata property to every project node that has an owner in the project graph. This rule checks each project node metadata to make sure that each project has some owner defined.
import { ProjectGraphProjectNode } from '@nx/devkit';import { createConformanceRule, ConformanceViolation } from '@nx/conformance';
export default createConformanceRule({ name: 'ensure-owners', category: 'consistency', description: 'Ensure that all projects have owners defined via Nx Owners.', implementation: async (context) => { const violations: ConformanceViolation[] = [];
for (const node of Object.values( context.projectGraph.nodes ) as ProjectGraphProjectNode[]) { const metadata = node.data.metadata; if (!metadata?.owners || Object.keys(metadata.owners).length === 0) { violations.push({ sourceProject: node.name, message: `This project currently has no owners defined via Nx Owners.`, }); } }
return { severity: 'medium', details: { violations, }, }; },});This rule uses TypeScript AST processing to ensure that index.ts files use a client-side style of export syntax and server.ts files use a server-side style of export syntax.
import { createConformanceRule, ConformanceViolation } from '@nx/conformance';import { existsSync, readFileSync } from 'node:fs';import { join } from 'node:path';import { createSourceFile, isExportDeclaration, isStringLiteral, isToken, ScriptKind, ScriptTarget,} from 'typescript';
export default createConformanceRule({ name: 'server-client-public-api', category: 'consistency', description: 'Ensure server-only and client-only public APIs are not mixed', implementation: async ({ projectGraph }) => { const violations: ConformanceViolation[] = [];
for (const nodeId in projectGraph.nodes) { const node = projectGraph.nodes[nodeId];
const sourceRoot = node.data.root;
const indexPath = join(sourceRoot, 'src/index.ts'); const serverPath = join(sourceRoot, 'src/server.ts');
if (existsSync(indexPath)) { const fileContent = readFileSync(indexPath, 'utf8'); violations.push( ...processEntryPoint(fileContent, indexPath, nodeId, 'client') ); }
if (existsSync(serverPath)) { const fileContent = readFileSync(serverPath, 'utf8'); violations.push( ...processEntryPoint(fileContent, serverPath, nodeId, 'server') ); } }
return { severity: 'medium', details: { violations }, }; },});
export function processEntryPoint( fileContent: string, entryPoint: string, project: string, style: 'server' | 'client') { const violations: ConformanceViolation[] = [];
const sf = createSourceFile( entryPoint, fileContent, ScriptTarget.Latest, true, ScriptKind.TS );
let hasNotOnlyExports = false; sf.forEachChild((node) => { if (isExportDeclaration(node)) { const moduleSpecifier = node.moduleSpecifier && isStringLiteral(node.moduleSpecifier) ? node.moduleSpecifier.getText() : '';
if (isModuleSpecifierViolated(moduleSpecifier, style)) { if ( violations.find( (v) => v.file === entryPoint && v.sourceProject === project ) ) { // we already have a violation for this file and project, so we don't need to add another one return; }
violations.push({ message: style === 'client' ? 'Client-side only entry point cannot export from server-side modules' : 'Server-side only entry point can only export server-side modules ', file: entryPoint, sourceProject: project, }); } } else if (isToken(node) && node === sf.endOfFileToken) { // do nothing } else { hasNotOnlyExports = true; } });
if (hasNotOnlyExports) { violations.push({ message: `Entry point should only contain exported APIs`, file: entryPoint, sourceProject: project, }); }
return violations;}
function isModuleSpecifierViolated( moduleSpecifier: string, style: 'server' | 'client') { // should not get here. if this is the case, it's a grammar error in the source code. if (!moduleSpecifier) return false;
if (style === 'server' && !moduleSpecifier.includes('.server')) { return true; }
if (style === 'client' && moduleSpecifier.includes('.server')) { return true; }
return false;}Auto-fixing Violations with Fix Generators
Section titled “Auto-fixing Violations with Fix Generators”Rules can optionally implement a fixGenerator function that will be used to automatically fix violations.
When Fix Generators Run
Section titled “When Fix Generators Run”nx conformance: Evaluates rules and applies any available fix generators. Changes are written to disk and rules are evaluated once more to calculate how many violations were fixed.nx conformance:check: Evaluates rules only. Fix generators are not applied (useful for CI).
Fix generators are only ever applied for rules whose final status is not disabled.
Fix Generator Function Signature
Section titled “Fix Generator Function Signature”Fix generators are essentially standard Nx generators. They receive a WritableConformanceTree (an extension of the FsTree used in other Nx generators) and a schema containing violations, rule options, and optional extra data exposed by the rule implementation via result.details.fixGeneratorData.
type ConformanceRuleFixGenerator<RuleOptions> = ( tree: WritableConformanceTree, schema: { violations: ConformanceViolation[]; ruleOptions: RuleOptions; fixGeneratorData?: Record<string, unknown>; }) => Promise<void> | void;During rule evaluation (diagnostics phase) the tree is read-only. During the fix phase, the tree is writable for generators to modify files.
Passing Data from Rules to Fix Generators
Section titled “Passing Data from Rules to Fix Generators”There are two supported ways to pass data from your rule implementation to its fix generator:
Per-violation data: The exact
details.violationsarray returned by your rule is provided to the fix generator. Each violation can providefixGeneratorDatafor targeted fixes.violations.push({message: 'Missing license header',file: 'libs/my-lib/src/index.ts',fixGeneratorData: { header: '/* LICENSE */\n', missing: true },});Note: Before final results are reported, any
fixGeneratorDatafields are stripped out.Global data: Put shared data on
details.fixGeneratorData. If present, it will be passed asschema.fixGeneratorDatato the fix generator and then stripped from the final report.return {severity: 'low',details: {violations,fixGeneratorData: { dryRun: ruleOptions.addHeader === false },},};Useful for expensive precomputed lookups or workspace-wide context that applies to all violations.
Additional Notes
Section titled “Additional Notes”- Project filtering: If the rule is configured with
projects, the runner filters the violations accordingly before calling the fix generator. The generator receives only the filtered set. - File-to-project inference: If a violation specifies
filebut notsourceProject, the runner attempts to infer the owning project and will include that on the violation provided to the fix generator where possible. - Data privacy: Both
details.fixGeneratorDataand per-violationfixGeneratorDataare never included in the emitted report. They are only available to the fix generator.
Example Conformance Rule with Fix Generator
Section titled “Example Conformance Rule with Fix Generator”import { createConformanceRule } from '@nx/conformance';import type { ConformanceViolation } from '@nx/conformance';
type RuleOptions = { addHeader: boolean;};
export default createConformanceRule<RuleOptions>({ name: 'license-header', category: 'maintainability', description: 'Ensure files contain a license header', implementation: async ({ tree, ruleOptions }) => { const violations: ConformanceViolation[] = [];
for (const filePath of tree.children('libs/my-lib/src')) { if (!filePath.endsWith('.ts')) continue; const contents = tree.read(filePath, 'utf-8') ?? ''; if (!contents.startsWith('/* LICENSE */')) { violations.push({ message: 'Missing license header', file: `libs/my-lib/src/${filePath}`, fixGeneratorData: { header: '/* LICENSE */\n', missing: true }, }); } }
return { severity: 'low', details: { violations, fixGeneratorData: { dryRun: ruleOptions.addHeader === false }, }, }; }, fixGenerator: async (tree, { violations, ruleOptions, fixGeneratorData }) => { if (fixGeneratorData?.dryRun) return;
for (const v of violations) { if (!('file' in v) || !v.file) continue; const header = (v as any).fixGeneratorData?.header ?? '/* LICENSE */\n'; const existing = tree.read(v.file, 'utf-8') ?? ''; if (!existing.startsWith(header) && ruleOptions.addHeader !== false) { tree.write(v.file, header + existing); } } },});Best Practices for Fix Generators
Section titled “Best Practices for Fix Generators”- Idempotent: Generators should be safe to run multiple times without changing files after the first successful run.
- Minimal changes: Modify only what is necessary to address the reported violations.
- Respect options: Honor
ruleOptionsso users can tune behavior. - Avoid re-discovery: Prefer using the provided
violationsand optionalfixGeneratorDatarather than rescanning the workspace. - Clear boundaries: Keep heavy computation inside the rule implementation and pass the results via
fixGeneratorDatato the generator.
Share Conformance Rules Across Workspaces
Section titled “Share Conformance Rules Across Workspaces”If you have an Enterprise Nx Cloud contract, you can share your conformance rules across every repository in your organization. Read more in these articles: