diff --git a/docs/external-generated/packages-metadata.json b/docs/external-generated/packages-metadata.json index 9b050f6234940..6d98bc4d5a26d 100644 --- a/docs/external-generated/packages-metadata.json +++ b/docs/external-generated/packages-metadata.json @@ -35,6 +35,17 @@ "path": "powerpack-conformance/documents/overview", "tags": [], "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": [ @@ -48,7 +59,17 @@ "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", "name": "powerpack-conformance", "packageName": "@nx/powerpack-conformance", diff --git a/docs/external-generated/packages/powerpack-conformance/documents/create-conformance-rule.md b/docs/external-generated/packages/powerpack-conformance/documents/create-conformance-rule.md new file mode 100644 index 0000000000000..4eb7718ac57aa --- /dev/null +++ b/docs/external-generated/packages/powerpack-conformance/documents/create-conformance-rule.md @@ -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({ + 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) diff --git a/docs/external-generated/packages/powerpack-conformance/documents/overview.md b/docs/external-generated/packages/powerpack-conformance/documents/overview.md index 67d0a6bdf9710..446120a35d4a2 100644 --- a/docs/external-generated/packages/powerpack-conformance/documents/overview.md +++ b/docs/external-generated/packages/powerpack-conformance/documents/overview.md @@ -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" %} -{ - "conformance": { - "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. +- [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) +- [Configure Conformance Rules in Nx Cloud](/ci/recipes/enterprise/conformance/configure-conformance-rules-in-nx-cloud) diff --git a/docs/external-generated/packages/powerpack-conformance/generators/create-rule.json b/docs/external-generated/packages/powerpack-conformance/generators/create-rule.json new file mode 100644 index 0000000000000..c2a30f6b4b9dd --- /dev/null +++ b/docs/external-generated/packages/powerpack-conformance/generators/create-rule.json @@ -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" +} diff --git a/docs/generated/manifests/menus.json b/docs/generated/manifests/menus.json index ec2ba492efb2c..ca0e84e1ebfea 100644 --- a/docs/generated/manifests/menus.json +++ b/docs/generated/manifests/menus.json @@ -10797,6 +10797,14 @@ "isExternal": false, "children": [], "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, @@ -10818,6 +10826,23 @@ ], "isExternal": 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, diff --git a/docs/generated/manifests/nx-api.json b/docs/generated/manifests/nx-api.json index cf5480dce8714..8498844afc34a 100644 --- a/docs/generated/manifests/nx-api.json +++ b/docs/generated/manifests/nx-api.json @@ -3707,6 +3707,17 @@ "path": "/nx-api/powerpack-conformance/documents/overview", "tags": [], "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", @@ -3722,7 +3733,17 @@ "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" }, "powerpack-enterprise-cloud": { diff --git a/docs/map.json b/docs/map.json index fd460981bdbb8..9e384f7fbbdc9 100644 --- a/docs/map.json +++ b/docs/map.json @@ -2680,6 +2680,12 @@ "id": "overview", "path": "/nx-api/powerpack-conformance", "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" } ] }, diff --git a/docs/nx-cloud/enterprise/conformance/publish-conformance-rules-to-nx-cloud.md b/docs/nx-cloud/enterprise/conformance/publish-conformance-rules-to-nx-cloud.md index 4980ad103cc0a..beb544cd3f7e5 100644 --- a/docs/nx-cloud/enterprise/conformance/publish-conformance-rules-to-nx-cloud.md +++ b/docs/nx-cloud/enterprise/conformance/publish-conformance-rules-to-nx-cloud.md @@ -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 ``` -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. - -``` -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({ - 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 -} +```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 ``` 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: diff --git a/docs/shared/packages/powerpack-conformance/create-conformance-rule.md b/docs/shared/packages/powerpack-conformance/create-conformance-rule.md new file mode 100644 index 0000000000000..4eb7718ac57aa --- /dev/null +++ b/docs/shared/packages/powerpack-conformance/create-conformance-rule.md @@ -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({ + 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) diff --git a/docs/shared/packages/powerpack-conformance/powerpack-conformance-plugin.md b/docs/shared/packages/powerpack-conformance/powerpack-conformance-plugin.md index 67d0a6bdf9710..446120a35d4a2 100644 --- a/docs/shared/packages/powerpack-conformance/powerpack-conformance-plugin.md +++ b/docs/shared/packages/powerpack-conformance/powerpack-conformance-plugin.md @@ -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" %} -{ - "conformance": { - "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. +- [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) +- [Configure Conformance Rules in Nx Cloud](/ci/recipes/enterprise/conformance/configure-conformance-rules-in-nx-cloud) diff --git a/docs/shared/reference/sitemap.md b/docs/shared/reference/sitemap.md index 9c07541de98af..b8c078c46f069 100644 --- a/docs/shared/reference/sitemap.md +++ b/docs/shared/reference/sitemap.md @@ -793,8 +793,11 @@ - [powerpack-conformance](/nx-api/powerpack-conformance) - [documents](/nx-api/powerpack-conformance/documents) - [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) - [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) - [generators](/nx-api/powerpack-enterprise-cloud/generators) - [init](/nx-api/powerpack-enterprise-cloud/generators/init)