Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nx-plugin): add plugin-lint executor #9697

Merged
merged 5 commits into from Jun 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/generated/devkit/index.md
Expand Up @@ -434,7 +434,13 @@ A plugin for Nx

### TargetConfiguration

• **TargetConfiguration**: `Object`
• **TargetConfiguration**<`T`\>: `Object`

#### Type parameters

| Name | Type |
| :--- | :---- |
| `T` | `any` |

---

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

Large diffs are not rendered by default.

34 changes: 33 additions & 1 deletion docs/generated/packages/nx-plugin.json
Expand Up @@ -50,7 +50,8 @@
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "tslint"],
"default": "eslint"
"default": "eslint",
"x-deprecated": "TSLint support is deprecated and will be removed"
},
"unitTestRunner": {
"type": "string",
Expand All @@ -73,6 +74,11 @@
"default": false,
"description": "Do not update tsconfig.json for development experience."
},
"skipLintChecks": {
"type": "boolean",
"default": false,
"description": "Do not eslint configuration for plugin json files."
},
"standaloneConfig": {
"description": "Split the project configuration into `<projectRoot>/project.json` rather than including it inside `workspace.json`.",
"type": "boolean"
Expand Down Expand Up @@ -315,6 +321,32 @@
"aliases": [],
"hidden": false,
"path": "/packages/nx-plugin/src/generators/executor/schema.json"
},
{
"name": "plugin-lint-checks",
"factory": "./src/generators/lint-checks/generator",
"schema": {
"$schema": "http://json-schema.org/schema",
"cli": "nx",
"$id": "PluginLint",
"title": "",
"type": "object",
"description": "Adds linting configuration to validate common json files for nx plugins.",
"properties": {
"projectName": {
"type": "string",
"description": "Which project should be the configuration be added to?",
"$default": { "$source": "projectName" }
}
},
"required": ["projectName"],
"presets": []
},
"description": "Adds linting configuration to validate common json files for nx plugins.",
"implementation": "/packages/nx-plugin/src/generators/lint-checks/generator.ts",
"aliases": [],
"hidden": false,
"path": "/packages/nx-plugin/src/generators/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
88 changes: 88 additions & 0 deletions e2e/nx-plugin/src/nx-plugin.test.ts
Expand Up @@ -185,6 +185,94 @@ 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');
const goodMigration = uniq('good-migration');
const badMigrationVersion = uniq('bad-version');
const missingMigrationVersion = uniq('missing-version');

// 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}`
);

runCLI(
`generate @nrwl/nx-plugin:migration ${badMigrationVersion} --project=${plugin} --packageVersion="invalid"`
);

runCLI(
`generate @nrwl/nx-plugin:migration ${missingMigrationVersion} --project=${plugin} --packageVersion="0.1.0"`
);

runCLI(
`generate @nrwl/nx-plugin:migration ${goodMigration} --project=${plugin} --packageVersion="0.1.0"`
);

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

updateFile(`libs/${plugin}/migrations.json`, (f) => {
const json = JSON.parse(f);
delete json.generators[missingMigrationVersion].version;
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);

expect(results).toContain(
`${missingMigrationVersion}: Missing required property - \`version\``
);
expect(results).toContain(
`${badMigrationVersion}: Version should be a valid semver`
);
expect(results).not.toContain(goodMigration);
});

describe('local plugins', () => {
const plugin = uniq('plugin');
beforeEach(() => {
Expand Down
1 change: 1 addition & 0 deletions nx.json
Expand Up @@ -20,6 +20,7 @@
"build-base",
"test",
"lint",
"lint-base",
"e2e",
"sitemap"
],
Expand Down
3 changes: 2 additions & 1 deletion packages/eslint-plugin-nx/package.json
Expand Up @@ -36,6 +36,7 @@
"@nrwl/workspace": "file:../workspace",
"@typescript-eslint/experimental-utils": "~5.24.0",
"chalk": "4.1.0",
"confusing-browser-globals": "^1.0.9"
"confusing-browser-globals": "^1.0.9",
"semver": "7.3.4"
}
}
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 nxPluginChecksRule, {
RULE_NAME as nxPluginChecksRuleName,
} from './rules/nx-plugin-checks';

// 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,
[nxPluginChecksRuleName]: nxPluginChecksRule,
...workspaceRules,
},
};
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