docs(core): create conformance rule recipe (#29406)

- [Create a Conformance
Rule](https://nx-dev-git-docs-conformance-rule-recipe-nrwl.vercel.app/nx-api/powerpack-conformance/documents/create-conformance-rule)
recipe

Blocked until the `create-rule` generator is merged and released
This commit is contained in:
Isaac Mann 2025-01-07 07:05:56 -05:00 committed by GitHub
parent 6948249ac7
commit 82751a1446
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 766 additions and 135 deletions

View File

@ -35,6 +35,17 @@
"path": "powerpack-conformance/documents/overview", "path": "powerpack-conformance/documents/overview",
"tags": [], "tags": [],
"originalFilePath": "shared/packages/powerpack-conformance/powerpack-conformance-plugin" "originalFilePath": "shared/packages/powerpack-conformance/powerpack-conformance-plugin"
},
{
"id": "create-conformance-rule",
"name": "Create a Conformance Rule",
"description": "A Nx Powerpack plugin which allows users to write and apply rules for your entire workspace that help with consistency, maintainability, reliability and security.",
"file": "external-generated/packages/powerpack-conformance/documents/create-conformance-rule",
"itemList": [],
"isExternal": false,
"path": "powerpack-conformance/documents/create-conformance-rule",
"tags": [],
"originalFilePath": "shared/packages/powerpack-conformance/create-conformance-rule"
} }
], ],
"executors": [ "executors": [
@ -48,7 +59,17 @@
"type": "executor" "type": "executor"
} }
], ],
"generators": [], "generators": [
{
"description": "Create a new conformance rule",
"file": "external-generated/packages/powerpack-conformance/generators/create-rule.json",
"hidden": false,
"name": "create-rule",
"originalFilePath": "/libs/nx-packages/powerpack-conformance/src/generators/create-rule/schema.json",
"path": "powerpack-conformance/generators/create-rule",
"type": "generator"
}
],
"githubRoot": "https://github.com/nrwl/nx/blob/master", "githubRoot": "https://github.com/nrwl/nx/blob/master",
"name": "powerpack-conformance", "name": "powerpack-conformance",
"packageName": "@nx/powerpack-conformance", "packageName": "@nx/powerpack-conformance",

View File

@ -0,0 +1,308 @@
# Create a Conformance Rule
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.
Therefore, 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/powerpack-conformance:create-rule` generator and answer the prompts.
```text {% command="nx g @nx/powerpack-conformance:create-rule" %}
NX Generating @nx/powerpack-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 reporter do you want to use for this rule? · project-reporter
✔ 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:
```ts {% fileName="packages/my-plugin/local-conformance-rule/index.ts" %}
import {
createConformanceRule,
ProjectViolation,
} from '@nx/powerpack-conformance';
export default createConformanceRule({
name: 'local-conformance-rule-example',
category: 'security',
description: 'an example of a conformance rule',
reporter: 'project-reporter',
implementation: async (context) => {
const violations: ProjectViolation[] = [];
return {
severity: 'low',
details: {
violations,
},
};
},
});
```
To enable the rule, you need to register it in the `nx.json` file.
```json {% fileName="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.
## Conformance Rule Examples
There are three types of reporters that a rule can use.
- `project-reporter` - The rule evaluates an entire project at a time.
- `project-files-reporter` - The rule evaluates a single project file at a time.
- `non-project-files-reporter` - The rule evaluates files that don't belong to any project.
{% tabs %}
{% tab label="project-reporter" %}
The `@nx/powerpack-conformance:ensure-owners` rule provides us an example of how to write a `project-reporter` rule. The `@nx/powerpack-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.
```ts
import { ProjectGraphProjectNode } from '@nx/devkit';
import {
createConformanceRule,
ProjectViolation,
} from '@nx/powerpack-conformance';
export default createConformanceRule({
name: 'ensure-owners',
category: 'consistency',
description: 'Ensure that all projects have owners defined via Nx Owners.',
reporter: 'project-reporter',
implementation: async (context) => {
const violations: ProjectViolation[] = [];
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,
},
};
},
});
```
{% /tab %}
{% tab label="project-files-reporter" %}
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.
```ts
import {
createConformanceRule,
ProjectFilesViolation,
} from '@nx/powerpack-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',
reporter: 'project-files-reporter',
implementation: async ({ projectGraph }) => {
const violations: ProjectFilesViolation[] = [];
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: ProjectFilesViolation[] = [];
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;
}
```
{% /tab %}
{% tab label="non-project-files-reporter" %}
This rule checks the root `package.json` file and ensures that if the `tmp` package is included as a dependency, it has a minimum version of 0.2.3.
```ts
import { readJsonFile, workspaceRoot } from '@nx/devkit';
import {
createConformanceRule,
NonProjectFilesViolation,
} from '@nx/powerpack-conformance';
import { join } from 'node:path';
import { satisfies } from 'semver';
export default createConformanceRule<object>({
name: 'package-tmp-0.2.3',
category: 'maintainability',
description: 'The tmp dependency should be a minimum version of 0.2.3',
reporter: 'non-project-files-reporter',
implementation: async () => {
const violations: NonProjectFilesViolation[] = [];
const applyViolationIfApplicable = (version: string | undefined) => {
if (version && !satisfies(version, '>=0.2.3')) {
violations.push({
message: 'The "tmp" package must be version "0.2.3" or higher',
file: 'package.json',
});
}
};
const workspaceRootPackageJson = await readJsonFile(
join(workspaceRoot, 'package.json')
);
applyViolationIfApplicable(workspaceRootPackageJson.dependencies?.['tmp']);
applyViolationIfApplicable(
workspaceRootPackageJson.devDependencies?.['tmp']
);
return {
severity: 'low',
details: {
violations,
},
};
},
});
```
{% /tab %}
{% /tabs %}
## 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:
- [Publish Conformance Rules to Nx Cloud](/ci/recipes/enterprise/conformance/publish-conformance-rules-to-nx-cloud)
- [Configure Conformance Rules in Nx Cloud](/ci/recipes/enterprise/conformance/configure-conformance-rules-in-nx-cloud)

View File

@ -166,51 +166,10 @@ Set the `rule` property to: `@nx/powerpack-conformance/ensure-owners`
} }
``` ```
## Custom Conformance Rules ## Next Steps
To write your own conformance rule, specify a relative path to a TypeScript or JavaScript file as the rule name: For more information about the conformance plugin, consult the following articles:
```json {% fileName="nx.json" %} - [Create a Conformance Rule](/nx-api/powerpack-conformance/documents/create-conformance-rule)
{ - [Publish Conformance Rules to Nx Cloud](/ci/recipes/enterprise/conformance/publish-conformance-rules-to-nx-cloud)
"conformance": { - [Configure Conformance Rules in Nx Cloud](/ci/recipes/enterprise/conformance/configure-conformance-rules-in-nx-cloud)
"rules": [
{
"rule": "./tools/local-conformance-rule.ts"
}
]
}
}
```
The rule definition file should look like this:
```ts {% fileName="tools/local-conformance-rule.ts" %}
import { createConformanceRule } from '@nx/powerpack-conformance';
const rule = createConformanceRule({
name: 'local-conformance-rule-example',
description: 'The description of the rule',
category: 'security', // `consistency`, `maintainability`, `reliability` or `security`
reporter: 'project-reporter', // `project-reporter` or `project-files-reporter`
implementation: async (context) => {
const { projectGraph, ruleOptions } = context;
// Your rule logic goes here
return {
severity: 'low', // 'high', 'medium' or 'low'
details: {
violations: [
// Return an empty array if the rule passes
{
sourceProject: 'my-project',
message: 'This is an informative error message.',
},
],
},
};
},
});
export default rule;
```
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.

View File

@ -0,0 +1,59 @@
{
"name": "create-rule",
"factory": "./src/generators/create-rule/create-rule",
"schema": {
"$schema": "http://json-schema.org/schema",
"id": "NxPowerpackConformanceCreateRule",
"title": "Create a new conformance rule",
"type": "object",
"cli": "nx",
"properties": {
"name": {
"type": "string",
"description": "The name of the rule.",
"$default": { "$source": "argv", "index": 0 },
"x-prompt": "What is the name of the rule?",
"x-priority": "important"
},
"directory": {
"type": "string",
"description": "A directory where the rule directory is created.",
"x-prompt": "Which directory do you want to create the rule directory in?",
"x-priority": "important"
},
"category": {
"type": "string",
"enum": ["consistency", "maintainability", "reliability", "security"],
"description": "The category of the rule.",
"x-prompt": "What category does this rule belong to?",
"x-priority": "important"
},
"reporter": {
"type": "string",
"enum": [
"project-reporter",
"project-files-reporter",
"non-project-files-reporter"
],
"description": "The reporter of the rule.",
"x-prompt": "What reporter do you want to use for this rule?",
"x-priority": "important"
},
"description": {
"type": "string",
"description": "The description of the rule.",
"x-prompt": "What is the description of the rule?",
"x-priority": "important"
}
},
"additionalProperties": false,
"required": ["name", "directory", "category", "reporter"],
"presets": []
},
"description": "Create a new conformance rule",
"implementation": "/libs/nx-packages/powerpack-conformance/src/generators/create-rule/create-rule.ts",
"aliases": [],
"hidden": false,
"path": "/libs/nx-packages/powerpack-conformance/src/generators/create-rule/schema.json",
"type": "generator"
}

View File

@ -10797,6 +10797,14 @@
"isExternal": false, "isExternal": false,
"children": [], "children": [],
"disableCollapsible": false "disableCollapsible": false
},
{
"name": "Create a Conformance Rule",
"path": "/nx-api/powerpack-conformance/documents/create-conformance-rule",
"id": "create-conformance-rule",
"isExternal": false,
"children": [],
"disableCollapsible": false
} }
], ],
"isExternal": false, "isExternal": false,
@ -10818,6 +10826,23 @@
], ],
"isExternal": false, "isExternal": false,
"disableCollapsible": false "disableCollapsible": false
},
{
"id": "generators",
"path": "/nx-api/powerpack-conformance/generators",
"name": "generators",
"children": [
{
"id": "create-rule",
"path": "/nx-api/powerpack-conformance/generators/create-rule",
"name": "create-rule",
"children": [],
"isExternal": false,
"disableCollapsible": false
}
],
"isExternal": false,
"disableCollapsible": false
} }
], ],
"isExternal": false, "isExternal": false,

View File

@ -3707,6 +3707,17 @@
"path": "/nx-api/powerpack-conformance/documents/overview", "path": "/nx-api/powerpack-conformance/documents/overview",
"tags": [], "tags": [],
"originalFilePath": "shared/packages/powerpack-conformance/powerpack-conformance-plugin" "originalFilePath": "shared/packages/powerpack-conformance/powerpack-conformance-plugin"
},
"/nx-api/powerpack-conformance/documents/create-conformance-rule": {
"id": "create-conformance-rule",
"name": "Create a Conformance Rule",
"description": "A Nx Powerpack plugin which allows users to write and apply rules for your entire workspace that help with consistency, maintainability, reliability and security.",
"file": "external-generated/packages/powerpack-conformance/documents/create-conformance-rule",
"itemList": [],
"isExternal": false,
"path": "/nx-api/powerpack-conformance/documents/create-conformance-rule",
"tags": [],
"originalFilePath": "shared/packages/powerpack-conformance/create-conformance-rule"
} }
}, },
"root": "/libs/nx-packages/powerpack-conformance", "root": "/libs/nx-packages/powerpack-conformance",
@ -3722,7 +3733,17 @@
"type": "executor" "type": "executor"
} }
}, },
"generators": {}, "generators": {
"/nx-api/powerpack-conformance/generators/create-rule": {
"description": "Create a new conformance rule",
"file": "external-generated/packages/powerpack-conformance/generators/create-rule.json",
"hidden": false,
"name": "create-rule",
"originalFilePath": "/libs/nx-packages/powerpack-conformance/src/generators/create-rule/schema.json",
"path": "/nx-api/powerpack-conformance/generators/create-rule",
"type": "generator"
}
},
"path": "/nx-api/powerpack-conformance" "path": "/nx-api/powerpack-conformance"
}, },
"powerpack-enterprise-cloud": { "powerpack-enterprise-cloud": {

View File

@ -2680,6 +2680,12 @@
"id": "overview", "id": "overview",
"path": "/nx-api/powerpack-conformance", "path": "/nx-api/powerpack-conformance",
"file": "shared/packages/powerpack-conformance/powerpack-conformance-plugin" "file": "shared/packages/powerpack-conformance/powerpack-conformance-plugin"
},
{
"name": "Create a Conformance Rule",
"id": "create-conformance-rule",
"path": "/nx-api/powerpack-conformance",
"file": "shared/packages/powerpack-conformance/create-conformance-rule"
} }
] ]
}, },

View File

@ -8,48 +8,10 @@ Let's create a custom rule which we can then publish to Nx Cloud. We will first
nx generate @nx/js:library cloud-conformance-rules nx generate @nx/js:library cloud-conformance-rules
``` ```
The Nx Cloud distribution mechanism expects each rule to be created in a named subdirectory in the `src/` directory of our new project, and each rule directory to contain an `index.ts` and a `schema.json` file. The Nx Cloud distribution mechanism expects each rule to be created in a named subdirectory in the `src/` directory of our new project, and each rule directory to contain an `index.ts` and a `schema.json` file. You can read more about [creating a conformance rule](/nx-api/powerpack-conformance/documents/create-conformance-rule) in the dedicated guide. For this recipe, we'll generate a default rule to use in the publishing process.
E.g. ```shell
nx g @nx/powerpack-conformance:create-rule --name=test-cloud-rule --directory=cloud-conformance-rules/src/test-cloud-rule --category=reliability --description="A test cloud rule" --reporter=non-project-files-reporter
```
cloud-conformance-rules/
├── src/
│ ├── test-cloud-rule/
│ │ ├── index.ts // Our rule implementation
│ │ └── schema.json // The schema definition for the options supported by our rule
```
Our simple rule implementation in `test-cloud-rule/index.ts`, that will currently not report any violations, might look like this:
```ts
import { createConformanceRule } from '@nx/powerpack-conformance';
export default createConformanceRule<object>({
name: 'test-cloud-rule',
category: 'reliability',
description: 'A test cloud rule',
reporter: 'non-project-files-reporter',
implementation: async () => {
return {
severity: 'low',
details: {
violations: [],
},
};
},
});
```
And because we do not yet have any options that we want to support for our rule, our `schema.json` file will looks like this (using the [JSON Schema](https://json-schema.org/) format):
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {},
"additionalProperties": false
}
``` ```
We now have a valid implementation of a rule and we are ready to build it and publish it to Nx Cloud. The [`@nx/powerpack-conformance` plugin](/nx-api/powerpack-conformance) provides a [dedicated executor called `bundle-rules`](/nx-api/powerpack-conformance/executors/bundle-rules) for creating appropriate build artifacts for this purpose, so we will wire that executor up to a new build target in our `cloud-conformance-rules` project's `project.json` file: We now have a valid implementation of a rule and we are ready to build it and publish it to Nx Cloud. The [`@nx/powerpack-conformance` plugin](/nx-api/powerpack-conformance) provides a [dedicated executor called `bundle-rules`](/nx-api/powerpack-conformance/executors/bundle-rules) for creating appropriate build artifacts for this purpose, so we will wire that executor up to a new build target in our `cloud-conformance-rules` project's `project.json` file:

View File

@ -0,0 +1,308 @@
# Create a Conformance Rule
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.
Therefore, 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/powerpack-conformance:create-rule` generator and answer the prompts.
```text {% command="nx g @nx/powerpack-conformance:create-rule" %}
NX Generating @nx/powerpack-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 reporter do you want to use for this rule? · project-reporter
✔ 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:
```ts {% fileName="packages/my-plugin/local-conformance-rule/index.ts" %}
import {
createConformanceRule,
ProjectViolation,
} from '@nx/powerpack-conformance';
export default createConformanceRule({
name: 'local-conformance-rule-example',
category: 'security',
description: 'an example of a conformance rule',
reporter: 'project-reporter',
implementation: async (context) => {
const violations: ProjectViolation[] = [];
return {
severity: 'low',
details: {
violations,
},
};
},
});
```
To enable the rule, you need to register it in the `nx.json` file.
```json {% fileName="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.
## Conformance Rule Examples
There are three types of reporters that a rule can use.
- `project-reporter` - The rule evaluates an entire project at a time.
- `project-files-reporter` - The rule evaluates a single project file at a time.
- `non-project-files-reporter` - The rule evaluates files that don't belong to any project.
{% tabs %}
{% tab label="project-reporter" %}
The `@nx/powerpack-conformance:ensure-owners` rule provides us an example of how to write a `project-reporter` rule. The `@nx/powerpack-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.
```ts
import { ProjectGraphProjectNode } from '@nx/devkit';
import {
createConformanceRule,
ProjectViolation,
} from '@nx/powerpack-conformance';
export default createConformanceRule({
name: 'ensure-owners',
category: 'consistency',
description: 'Ensure that all projects have owners defined via Nx Owners.',
reporter: 'project-reporter',
implementation: async (context) => {
const violations: ProjectViolation[] = [];
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,
},
};
},
});
```
{% /tab %}
{% tab label="project-files-reporter" %}
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.
```ts
import {
createConformanceRule,
ProjectFilesViolation,
} from '@nx/powerpack-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',
reporter: 'project-files-reporter',
implementation: async ({ projectGraph }) => {
const violations: ProjectFilesViolation[] = [];
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: ProjectFilesViolation[] = [];
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;
}
```
{% /tab %}
{% tab label="non-project-files-reporter" %}
This rule checks the root `package.json` file and ensures that if the `tmp` package is included as a dependency, it has a minimum version of 0.2.3.
```ts
import { readJsonFile, workspaceRoot } from '@nx/devkit';
import {
createConformanceRule,
NonProjectFilesViolation,
} from '@nx/powerpack-conformance';
import { join } from 'node:path';
import { satisfies } from 'semver';
export default createConformanceRule<object>({
name: 'package-tmp-0.2.3',
category: 'maintainability',
description: 'The tmp dependency should be a minimum version of 0.2.3',
reporter: 'non-project-files-reporter',
implementation: async () => {
const violations: NonProjectFilesViolation[] = [];
const applyViolationIfApplicable = (version: string | undefined) => {
if (version && !satisfies(version, '>=0.2.3')) {
violations.push({
message: 'The "tmp" package must be version "0.2.3" or higher',
file: 'package.json',
});
}
};
const workspaceRootPackageJson = await readJsonFile(
join(workspaceRoot, 'package.json')
);
applyViolationIfApplicable(workspaceRootPackageJson.dependencies?.['tmp']);
applyViolationIfApplicable(
workspaceRootPackageJson.devDependencies?.['tmp']
);
return {
severity: 'low',
details: {
violations,
},
};
},
});
```
{% /tab %}
{% /tabs %}
## 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:
- [Publish Conformance Rules to Nx Cloud](/ci/recipes/enterprise/conformance/publish-conformance-rules-to-nx-cloud)
- [Configure Conformance Rules in Nx Cloud](/ci/recipes/enterprise/conformance/configure-conformance-rules-in-nx-cloud)

View File

@ -166,51 +166,10 @@ Set the `rule` property to: `@nx/powerpack-conformance/ensure-owners`
} }
``` ```
## Custom Conformance Rules ## Next Steps
To write your own conformance rule, specify a relative path to a TypeScript or JavaScript file as the rule name: For more information about the conformance plugin, consult the following articles:
```json {% fileName="nx.json" %} - [Create a Conformance Rule](/nx-api/powerpack-conformance/documents/create-conformance-rule)
{ - [Publish Conformance Rules to Nx Cloud](/ci/recipes/enterprise/conformance/publish-conformance-rules-to-nx-cloud)
"conformance": { - [Configure Conformance Rules in Nx Cloud](/ci/recipes/enterprise/conformance/configure-conformance-rules-in-nx-cloud)
"rules": [
{
"rule": "./tools/local-conformance-rule.ts"
}
]
}
}
```
The rule definition file should look like this:
```ts {% fileName="tools/local-conformance-rule.ts" %}
import { createConformanceRule } from '@nx/powerpack-conformance';
const rule = createConformanceRule({
name: 'local-conformance-rule-example',
description: 'The description of the rule',
category: 'security', // `consistency`, `maintainability`, `reliability` or `security`
reporter: 'project-reporter', // `project-reporter` or `project-files-reporter`
implementation: async (context) => {
const { projectGraph, ruleOptions } = context;
// Your rule logic goes here
return {
severity: 'low', // 'high', 'medium' or 'low'
details: {
violations: [
// Return an empty array if the rule passes
{
sourceProject: 'my-project',
message: 'This is an informative error message.',
},
],
},
};
},
});
export default rule;
```
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.

View File

@ -793,8 +793,11 @@
- [powerpack-conformance](/nx-api/powerpack-conformance) - [powerpack-conformance](/nx-api/powerpack-conformance)
- [documents](/nx-api/powerpack-conformance/documents) - [documents](/nx-api/powerpack-conformance/documents)
- [Overview](/nx-api/powerpack-conformance/documents/overview) - [Overview](/nx-api/powerpack-conformance/documents/overview)
- [Create a Conformance Rule](/nx-api/powerpack-conformance/documents/create-conformance-rule)
- [executors](/nx-api/powerpack-conformance/executors) - [executors](/nx-api/powerpack-conformance/executors)
- [bundle-rules](/nx-api/powerpack-conformance/executors/bundle-rules) - [bundle-rules](/nx-api/powerpack-conformance/executors/bundle-rules)
- [generators](/nx-api/powerpack-conformance/generators)
- [create-rule](/nx-api/powerpack-conformance/generators/create-rule)
- [powerpack-enterprise-cloud](/nx-api/powerpack-enterprise-cloud) - [powerpack-enterprise-cloud](/nx-api/powerpack-enterprise-cloud)
- [generators](/nx-api/powerpack-enterprise-cloud/generators) - [generators](/nx-api/powerpack-enterprise-cloud/generators)
- [init](/nx-api/powerpack-enterprise-cloud/generators/init) - [init](/nx-api/powerpack-enterprise-cloud/generators/init)