Skip to content

Commit

Permalink
feat(linter): add lint rule for nx-plugin validation
Browse files Browse the repository at this point in the history
  • Loading branch information
AgentEnder committed May 25, 2022
1 parent 46a075c commit 030bb71
Show file tree
Hide file tree
Showing 24 changed files with 861 additions and 89 deletions.
7 changes: 7 additions & 0 deletions docs/generated/devkit/index.md
Expand Up @@ -38,6 +38,7 @@ It only uses language primitives and immutable objects
### Other Interfaces

- [NxPlugin](../../devkit/index#nxplugin)
- [PackageJson](../../devkit/index#packagejson)

### Project Graph Interfaces

Expand Down Expand Up @@ -230,6 +231,12 @@ A plugin for Nx

---

### PackageJson

**PackageJson**: `Object`

---

## Project Graph Interfaces

### FileData
Expand Down
2 changes: 1 addition & 1 deletion docs/generated/packages/devkit.json

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions docs/generated/packages/nx-plugin.json
Expand Up @@ -315,6 +315,35 @@
"aliases": [],
"hidden": false,
"path": "/packages/nx-plugin/src/generators/executor/schema.json"
},
{
"name": "plugin-lint-checks",
"factory": "./src/generators/plugin-lint-checks/generator",
"schema": {
"$schema": "http://json-schema.org/schema",
"cli": "nx",
"$id": "PluginLint",
"title": "",
"type": "object",
"properties": {
"projectName": {
"type": "string",
"description": "Which project should be the target be added to?"
},
"targetName": {
"type": "string",
"description": "What should the target be called?",
"default": "plugin-lint"
}
},
"required": ["projectName", "targetName"],
"presets": []
},
"description": "Adds linting configuration to validate common json files for nx plugins.",
"implementation": "/packages/nx-plugin/src/generators/plugin-lint-checks/generator.ts",
"aliases": [],
"hidden": false,
"path": "/packages/nx-plugin/src/generators/plugin-lint-checks/schema.json"
}
],
"executors": [
Expand Down
3 changes: 2 additions & 1 deletion docs/packages.json
Expand Up @@ -188,7 +188,8 @@
"e2e-project",
"migration",
"generator",
"executor"
"executor",
"plugin-lint-checks"
]
}
},
Expand Down
61 changes: 61 additions & 0 deletions e2e/nx-plugin/src/nx-plugin.test.ts
Expand Up @@ -185,6 +185,67 @@ describe('Nx Plugin', () => {
});
}, 90000);

it('should catch invalid implementations, schemas, and version in lint', async () => {
const plugin = uniq('plugin');
const goodGenerator = uniq('good-generator');
const goodExecutor = uniq('good-executor');

// Generating the plugin results in a generator also called {plugin},
// as well as an executor called "build"
runCLI(`generate @nrwl/nx-plugin:plugin ${plugin} --linter=eslint`);

runCLI(
`generate @nrwl/nx-plugin:generator ${goodGenerator} --project=${plugin}`
);

runCLI(
`generate @nrwl/nx-plugin:executor ${goodExecutor} --project=${plugin}`
);

updateFile(`libs/${plugin}/generators.json`, (f) => {
const json = JSON.parse(f);
// @proj/plugin:plugin has an invalid implementation path
json.generators[plugin].factory =
json.generators[plugin].factory.substring(1);
// @proj/plugin:non-existant has a missing implementation path amd schema
json.generators['non-existant-generator'] = {};
return JSON.stringify(json);
});

updateFile(`libs/${plugin}/executors.json`, (f) => {
const json = JSON.parse(f);
// @proj/plugin:build has an invalid implementation path
json.executors['build'].factory =
json.executors['build'].factory.substring(1);
// @proj/plugin:non-existant has a missing implementation path amd schema
json.executors['non-existant-executor'] = {};
return JSON.stringify(json);
});

const results = runCLI(`lint ${plugin}`, { silenceError: true });
expect(results).toContain(
`${plugin}: Implementation path should point to a valid file`
);
expect(results).toContain(
`non-existant-generator: Missing required property - \`schema\``
);
expect(results).toContain(
`non-existant-generator: Missing required property - \`implementation\``
);
expect(results).not.toContain(goodGenerator);

expect(results).toContain(
`build: Implementation path should point to a valid file`
);
expect(results).toContain(
`non-existant-executor: Missing required property - \`schema\``
);
expect(results).toContain(
`non-existant-executor: Missing required property - \`implementation\``
);
expect(results).not.toContain(goodExecutor);
});

describe('local plugins', () => {
const plugin = uniq('plugin');
beforeEach(() => {
Expand Down
7 changes: 7 additions & 0 deletions nx.json
Expand Up @@ -20,6 +20,7 @@
"build-base",
"test",
"lint",
"lint-base",
"e2e",
"sitemap"
],
Expand All @@ -45,6 +46,12 @@
"target": "build-base",
"projects": "dependencies"
}
],
"lint": [
{
"target": "lint-base",
"projects": "self"
}
]
},
"workspaceLayout": {
Expand Down
2 changes: 1 addition & 1 deletion packages/angular/generators.json
Expand Up @@ -125,7 +125,7 @@
"description": "Generate a Module Federation configuration for a given Angular application."
},
"setup-tailwind": {
"factory": "./src/generators/setup-tailwind/compat",
"factory": "./src/generators/setup-tailwind/setup-tailwind.compat",
"schema": "./src/generators/setup-tailwind/schema.json",
"description": "Configures Tailwind CSS for an application or a buildable/publishable library."
},
Expand Down
2 changes: 2 additions & 0 deletions packages/devkit/index.ts
Expand Up @@ -52,6 +52,8 @@ export type {
ProjectTargetConfigurator,
} from 'nx/src/utils/nx-plugin';

export type { PackageJson } from 'nx/src/utils/package-json';

/**
* @category Workspace
*/
Expand Down
5 changes: 5 additions & 0 deletions packages/eslint-plugin-nx/src/index.ts
Expand Up @@ -11,6 +11,10 @@ import enforceModuleBoundaries, {
RULE_NAME as enforceModuleBoundariesRuleName,
} from './rules/enforce-module-boundaries';

import nxPluginSchemaRule, {
RULE_NAME as nxPluginSchemaRuleName,
} from './rules/nx-plugin-schema';

// Resolve any custom rules that might exist in the current workspace
import { workspaceRules } from './resolve-workspace-rules';

Expand All @@ -27,6 +31,7 @@ module.exports = {
},
rules: {
[enforceModuleBoundariesRuleName]: enforceModuleBoundaries,
[nxPluginSchemaRuleName]: nxPluginSchemaRule,
...workspaceRules,
},
};
41 changes: 7 additions & 34 deletions packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.ts
Expand Up @@ -3,8 +3,6 @@ import {
joinPathFragments,
normalizePath,
ProjectGraphExternalNode,
readCachedProjectGraph,
readNxJson,
} from '@nrwl/devkit';
import {
DepConstraint,
Expand All @@ -21,10 +19,8 @@ import {
isAbsoluteImportIntoAnotherProject,
isAngularSecondaryEntrypoint,
isDirectDependency,
isTerminalRun,
MappedProjectGraph,
MappedProjectGraphNode,
mapProjectGraphFiles,
matchImportWithWildcard,
onlyLoadChildren,
stringifyTags,
Expand All @@ -40,13 +36,16 @@ import {
findFilesInCircularPath,
} from '@nrwl/workspace/src/utils/graph-utils';
import { isRelativePath } from '@nrwl/workspace/src/utilities/fileutils';
import * as chalk from 'chalk';
import { basename, dirname, relative } from 'path';
import {
getBarrelEntryPointByImportScope,
getBarrelEntryPointProjectNode,
getRelativeImportPath,
} from '../utils/ast-utils';
import {
ensureGlobalProjectGraph,
readProjectGraph,
} from '../utils/project-graph-utils';

type Options = [
{
Expand Down Expand Up @@ -148,40 +147,14 @@ export default createESLintRule<Options, MessageIds>({
const projectPath = normalizePath(
(global as any).projectPath || workspaceRoot
);
/**
* Only reuse graph when running from terminal
* Enforce every IDE change to get a fresh nxdeps.json
*/
if (!(global as any).projectGraph || !isTerminalRun()) {
const nxJson = readNxJson();
(global as any).workspaceLayout = nxJson.workspaceLayout;

/**
* Because there are a number of ways in which the rule can be invoked (executor vs ESLint CLI vs IDE Plugin),
* the ProjectGraph may or may not exist by the time the lint rule is invoked for the first time.
*/
try {
(global as any).projectGraph = mapProjectGraphFiles(
readCachedProjectGraph()
);
} catch {
const WARNING_PREFIX = `${chalk.reset.keyword('orange')('warning')}`;
const RULE_NAME_SUFFIX = `${chalk.reset.dim(
'@nrwl/nx/enforce-module-boundaries'
)}`;
process.stdout
.write(`${WARNING_PREFIX} No cached ProjectGraph is available. The rule will be skipped.
If you encounter this error as part of running standard \`nx\` commands then please open an issue on https://github.com/nrwl/nx
${RULE_NAME_SUFFIX}\n`);
}
}

if (!(global as any).projectGraph) {
const projectGraph = readProjectGraph(RULE_NAME);

if (projectGraph) {
return {};
}

const workspaceLayout = (global as any).workspaceLayout;
const projectGraph = (global as any).projectGraph as MappedProjectGraph;

if (!(global as any).targetProjectLocator) {
(global as any).targetProjectLocator = new TargetProjectLocator(
Expand Down

0 comments on commit 030bb71

Please sign in to comment.