Skip to content

Commit 08f3857

Browse files
isaacplmannFrozenPandaz
authored andcommitted
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 (cherry picked from commit 82751a1)
1 parent c9b14a2 commit 08f3857

File tree

11 files changed

+766
-135
lines changed

11 files changed

+766
-135
lines changed

docs/external-generated/packages-metadata.json

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@
3535
"path": "powerpack-conformance/documents/overview",
3636
"tags": [],
3737
"originalFilePath": "shared/packages/powerpack-conformance/powerpack-conformance-plugin"
38+
},
39+
{
40+
"id": "create-conformance-rule",
41+
"name": "Create a Conformance Rule",
42+
"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.",
43+
"file": "external-generated/packages/powerpack-conformance/documents/create-conformance-rule",
44+
"itemList": [],
45+
"isExternal": false,
46+
"path": "powerpack-conformance/documents/create-conformance-rule",
47+
"tags": [],
48+
"originalFilePath": "shared/packages/powerpack-conformance/create-conformance-rule"
3849
}
3950
],
4051
"executors": [
@@ -48,7 +59,17 @@
4859
"type": "executor"
4960
}
5061
],
51-
"generators": [],
62+
"generators": [
63+
{
64+
"description": "Create a new conformance rule",
65+
"file": "external-generated/packages/powerpack-conformance/generators/create-rule.json",
66+
"hidden": false,
67+
"name": "create-rule",
68+
"originalFilePath": "/libs/nx-packages/powerpack-conformance/src/generators/create-rule/schema.json",
69+
"path": "powerpack-conformance/generators/create-rule",
70+
"type": "generator"
71+
}
72+
],
5273
"githubRoot": "https://github.com/nrwl/nx/blob/master",
5374
"name": "powerpack-conformance",
5475
"packageName": "@nx/powerpack-conformance",
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
# Create a Conformance Rule
2+
3+
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.
4+
5+
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.
6+
7+
To write your own conformance rule, run the `@nx/powerpack-conformance:create-rule` generator and answer the prompts.
8+
9+
```text {% command="nx g @nx/powerpack-conformance:create-rule" %}
10+
NX Generating @nx/powerpack-conformance:create-rule
11+
12+
✔ What is the name of the rule? · local-conformance-rule-example
13+
✔ Which directory do you want to create the rule directory in? · packages/my-plugin/local-conformance-rule
14+
✔ What category does this rule belong to? · security
15+
✔ What reporter do you want to use for this rule? · project-reporter
16+
✔ What is the description of the rule? · an example of a conformance rule
17+
CREATE packages/my-plugin/local-conformance-rule/local-conformance-rule-example/index.ts
18+
CREATE packages/my-plugin/local-conformance-rule/local-conformance-rule-example/schema.json
19+
```
20+
21+
The generated rule definition file should look like this:
22+
23+
```ts {% fileName="packages/my-plugin/local-conformance-rule/index.ts" %}
24+
import {
25+
createConformanceRule,
26+
ProjectViolation,
27+
} from '@nx/powerpack-conformance';
28+
29+
export default createConformanceRule({
30+
name: 'local-conformance-rule-example',
31+
category: 'security',
32+
description: 'an example of a conformance rule',
33+
reporter: 'project-reporter',
34+
implementation: async (context) => {
35+
const violations: ProjectViolation[] = [];
36+
37+
return {
38+
severity: 'low',
39+
details: {
40+
violations,
41+
},
42+
};
43+
},
44+
});
45+
```
46+
47+
To enable the rule, you need to register it in the `nx.json` file.
48+
49+
```json {% fileName="nx.json" %}
50+
{
51+
"conformance": {
52+
"rules": [
53+
{
54+
"rule": "./packages/my-plugin/local-conformance-rule/index.ts"
55+
}
56+
]
57+
}
58+
}
59+
```
60+
61+
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.
62+
63+
## Conformance Rule Examples
64+
65+
There are three types of reporters that a rule can use.
66+
67+
- `project-reporter` - The rule evaluates an entire project at a time.
68+
- `project-files-reporter` - The rule evaluates a single project file at a time.
69+
- `non-project-files-reporter` - The rule evaluates files that don't belong to any project.
70+
71+
{% tabs %}
72+
{% tab label="project-reporter" %}
73+
74+
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.
75+
76+
```ts
77+
import { ProjectGraphProjectNode } from '@nx/devkit';
78+
import {
79+
createConformanceRule,
80+
ProjectViolation,
81+
} from '@nx/powerpack-conformance';
82+
83+
export default createConformanceRule({
84+
name: 'ensure-owners',
85+
category: 'consistency',
86+
description: 'Ensure that all projects have owners defined via Nx Owners.',
87+
reporter: 'project-reporter',
88+
implementation: async (context) => {
89+
const violations: ProjectViolation[] = [];
90+
91+
for (const node of Object.values(
92+
context.projectGraph.nodes
93+
) as ProjectGraphProjectNode[]) {
94+
const metadata = node.data.metadata;
95+
if (!metadata?.owners || Object.keys(metadata.owners).length === 0) {
96+
violations.push({
97+
sourceProject: node.name,
98+
message: `This project currently has no owners defined via Nx Owners.`,
99+
});
100+
}
101+
}
102+
103+
return {
104+
severity: 'medium',
105+
details: {
106+
violations,
107+
},
108+
};
109+
},
110+
});
111+
```
112+
113+
{% /tab %}
114+
{% tab label="project-files-reporter" %}
115+
116+
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.
117+
118+
```ts
119+
import {
120+
createConformanceRule,
121+
ProjectFilesViolation,
122+
} from '@nx/powerpack-conformance';
123+
import { existsSync, readFileSync } from 'node:fs';
124+
import { join } from 'node:path';
125+
import {
126+
createSourceFile,
127+
isExportDeclaration,
128+
isStringLiteral,
129+
isToken,
130+
ScriptKind,
131+
ScriptTarget,
132+
} from 'typescript';
133+
134+
export default createConformanceRule({
135+
name: 'server-client-public-api',
136+
category: 'consistency',
137+
description: 'Ensure server-only and client-only public APIs are not mixed',
138+
reporter: 'project-files-reporter',
139+
implementation: async ({ projectGraph }) => {
140+
const violations: ProjectFilesViolation[] = [];
141+
142+
for (const nodeId in projectGraph.nodes) {
143+
const node = projectGraph.nodes[nodeId];
144+
145+
const sourceRoot = node.data.root;
146+
147+
const indexPath = join(sourceRoot, 'src/index.ts');
148+
const serverPath = join(sourceRoot, 'src/server.ts');
149+
150+
if (existsSync(indexPath)) {
151+
const fileContent = readFileSync(indexPath, 'utf8');
152+
violations.push(
153+
...processEntryPoint(fileContent, indexPath, nodeId, 'client')
154+
);
155+
}
156+
157+
if (existsSync(serverPath)) {
158+
const fileContent = readFileSync(serverPath, 'utf8');
159+
violations.push(
160+
...processEntryPoint(fileContent, serverPath, nodeId, 'server')
161+
);
162+
}
163+
}
164+
165+
return {
166+
severity: 'medium',
167+
details: { violations },
168+
};
169+
},
170+
});
171+
172+
export function processEntryPoint(
173+
fileContent: string,
174+
entryPoint: string,
175+
project: string,
176+
style: 'server' | 'client'
177+
) {
178+
const violations: ProjectFilesViolation[] = [];
179+
180+
const sf = createSourceFile(
181+
entryPoint,
182+
fileContent,
183+
ScriptTarget.Latest,
184+
true,
185+
ScriptKind.TS
186+
);
187+
188+
let hasNotOnlyExports = false;
189+
sf.forEachChild((node) => {
190+
if (isExportDeclaration(node)) {
191+
const moduleSpecifier =
192+
node.moduleSpecifier && isStringLiteral(node.moduleSpecifier)
193+
? node.moduleSpecifier.getText()
194+
: '';
195+
196+
if (isModuleSpecifierViolated(moduleSpecifier, style)) {
197+
if (
198+
violations.find(
199+
(v) => v.file === entryPoint && v.sourceProject === project
200+
)
201+
) {
202+
// we already have a violation for this file and project, so we don't need to add another one
203+
return;
204+
}
205+
206+
violations.push({
207+
message:
208+
style === 'client'
209+
? 'Client-side only entry point cannot export from server-side modules'
210+
: 'Server-side only entry point can only export server-side modules ',
211+
file: entryPoint,
212+
sourceProject: project,
213+
});
214+
}
215+
} else if (isToken(node) && node === sf.endOfFileToken) {
216+
// do nothing
217+
} else {
218+
hasNotOnlyExports = true;
219+
}
220+
});
221+
222+
if (hasNotOnlyExports) {
223+
violations.push({
224+
message: `Entry point should only contain exported APIs`,
225+
file: entryPoint,
226+
sourceProject: project,
227+
});
228+
}
229+
230+
return violations;
231+
}
232+
233+
function isModuleSpecifierViolated(
234+
moduleSpecifier: string,
235+
style: 'server' | 'client'
236+
) {
237+
// should not get here. if this is the case, it's a grammar error in the source code.
238+
if (!moduleSpecifier) return false;
239+
240+
if (style === 'server' && !moduleSpecifier.includes('.server')) {
241+
return true;
242+
}
243+
244+
if (style === 'client' && moduleSpecifier.includes('.server')) {
245+
return true;
246+
}
247+
248+
return false;
249+
}
250+
```
251+
252+
{% /tab %}
253+
{% tab label="non-project-files-reporter" %}
254+
255+
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.
256+
257+
```ts
258+
import { readJsonFile, workspaceRoot } from '@nx/devkit';
259+
import {
260+
createConformanceRule,
261+
NonProjectFilesViolation,
262+
} from '@nx/powerpack-conformance';
263+
import { join } from 'node:path';
264+
import { satisfies } from 'semver';
265+
266+
export default createConformanceRule<object>({
267+
name: 'package-tmp-0.2.3',
268+
category: 'maintainability',
269+
description: 'The tmp dependency should be a minimum version of 0.2.3',
270+
reporter: 'non-project-files-reporter',
271+
implementation: async () => {
272+
const violations: NonProjectFilesViolation[] = [];
273+
const applyViolationIfApplicable = (version: string | undefined) => {
274+
if (version && !satisfies(version, '>=0.2.3')) {
275+
violations.push({
276+
message: 'The "tmp" package must be version "0.2.3" or higher',
277+
file: 'package.json',
278+
});
279+
}
280+
};
281+
282+
const workspaceRootPackageJson = await readJsonFile(
283+
join(workspaceRoot, 'package.json')
284+
);
285+
applyViolationIfApplicable(workspaceRootPackageJson.dependencies?.['tmp']);
286+
applyViolationIfApplicable(
287+
workspaceRootPackageJson.devDependencies?.['tmp']
288+
);
289+
290+
return {
291+
severity: 'low',
292+
details: {
293+
violations,
294+
},
295+
};
296+
},
297+
});
298+
```
299+
300+
{% /tab %}
301+
{% /tabs %}
302+
303+
## Share Conformance Rules Across Workspaces
304+
305+
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:
306+
307+
- [Publish Conformance Rules to Nx Cloud](/ci/recipes/enterprise/conformance/publish-conformance-rules-to-nx-cloud)
308+
- [Configure Conformance Rules in Nx Cloud](/ci/recipes/enterprise/conformance/configure-conformance-rules-in-nx-cloud)

0 commit comments

Comments
 (0)