Skip to content

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.

To write your own conformance rule, run the @nx/conformance:create-rule generator and answer the prompts.

nx g @nx/conformance:create-rule
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 rule
CREATE packages/my-plugin/local-conformance-rule/local-conformance-rule-example/index.ts
CREATE packages/my-plugin/local-conformance-rule/local-conformance-rule-example/schema.json

The generated rule definition file should look like this:

packages/my-plugin/local-conformance-rule/index.ts
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.

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

The implementation function of the rule is passed a context object which contains:

  • tree: A ReadOnlyConformanceTree that 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 graph
  • fileMapCache: The Nx file map cache
  • ruleOptions: The resolved rule configuration options based on the current workspace

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
}
violations.push({
message: 'File violates standards',
file: 'libs/my-lib/src/problematic.ts', // sourceProject auto-inferred as 'my-lib'
});
  1. Use workspaceViolation: true for issues affecting the entire workspace (global configs, workspace structure, etc.)
  2. Use sourceProject only for project-wide issues (missing configuration, structure problems)
  3. Use file (and optional explicit sourceProject) for violations tied to specific files in projects
  4. Use file only for files that may not belong to projects (CI configs, root files, etc.)

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,
},
};
},
});

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.

  • 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 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.

There are two supported ways to pass data from your rule implementation to its fix generator:

  1. Per-violation data: The exact details.violations array returned by your rule is provided to the fix generator. Each violation can provide fixGeneratorData for 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 fixGeneratorData fields are stripped out.

  2. Global data: Put shared data on details.fixGeneratorData. If present, it will be passed as schema.fixGeneratorData to 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.

  • 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 file but not sourceProject, 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.fixGeneratorData and per-violation fixGeneratorData are 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);
}
}
},
});
  • 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 ruleOptions so users can tune behavior.
  • Avoid re-discovery: Prefer using the provided violations and optional fixGeneratorData rather than rescanning the workspace.
  • Clear boundaries: Keep heavy computation inside the rule implementation and pass the results via fixGeneratorData to the generator.

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: