UNPKG

@nx/eslint-plugin

Version:

The eslint-plugin package is an ESLint plugin that contains a collection of recommended ESLint rule configurations which you can extend from in your own ESLint configs, as well as an Nx-specific lint rule called enforce-module-boundaries.

432 lines (431 loc) • 18.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RULE_NAME = void 0; exports.checkCollectionFileNode = checkCollectionFileNode; exports.checkCollectionNode = checkCollectionNode; exports.validateEntry = validateEntry; exports.validateImplementationNode = validateImplementationNode; exports.validatePackageGroup = validatePackageGroup; exports.validateVersionJsonExpression = validateVersionJsonExpression; const tslib_1 = require("tslib"); const utils_1 = require("@typescript-eslint/utils"); const devkit_1 = require("@nx/devkit"); const js_1 = require("@nx/js"); const internal_1 = require("@nx/js/src/internal"); const path = tslib_1.__importStar(require("path")); const semver_1 = require("semver"); const project_graph_utils_1 = require("../utils/project-graph-utils"); const runtime_lint_utils_1 = require("../utils/runtime-lint-utils"); const DEFAULT_OPTIONS = { generatorsJson: 'generators.json', executorsJson: 'executors.json', migrationsJson: 'migrations.json', packageJson: 'package.json', allowedVersionStrings: ['*', 'latest', 'next'], tsConfig: 'tsconfig.lib.json', }; exports.RULE_NAME = 'nx-plugin-checks'; exports.default = utils_1.ESLintUtils.RuleCreator(() => ``)({ name: exports.RULE_NAME, meta: { docs: { description: 'Checks common nx-plugin configuration files for validity', }, schema: [ { type: 'object', properties: { generatorsJson: { type: 'string', description: "The path to the project's generators.json file, relative to the project root", }, executorsJson: { type: 'string', description: "The path to the project's executors.json file, relative to the project root", }, migrationsJson: { type: 'string', description: "The path to the project's migrations.json file, relative to the project root", }, packageJson: { type: 'string', description: "The path to the project's package.json file, relative to the project root", }, allowedVersionStrings: { type: 'array', description: 'A list of specifiers that are valid for versions within package group. Defaults to ["*", "latest", "next"]', items: { type: 'string' }, }, tsConfig: { type: 'string', description: 'The path to the tsconfig file used to build the plugin. Defaults to "tsconfig.lib.json".', }, }, additionalProperties: false, }, ], type: 'problem', messages: { invalidSchemaPath: 'Schema path should point to a valid file', invalidImplementationPath: '{{ key }}: Implementation path should point to a valid file', invalidImplementationModule: '{{ key }}: Unable to find export {{ identifier }} in implementation module', unableToReadImplementationExports: '{{ key }}: Unable to read exports for implementation module', invalidVersion: '{{ key }}: Version should be a valid semver', noGeneratorsOrSchematicsFound: 'Unable to find `generators` or `schematics` property', noExecutorsOrBuildersFound: 'Unable to find `executors` or `builders` property', valueShouldBeObject: '{{ key }} should be an object', missingRequiredSchema: '{{ key }}: Missing required property - `schema`', missingImplementation: '{{ key }}: Missing required property - `implementation`', missingVersion: '{{ key }}: Missing required property - `version`', }, }, defaultOptions: [DEFAULT_OPTIONS], create(context) { // jsonc-eslint-parser adds this property to parserServices where appropriate if (!(0, runtime_lint_utils_1.getParserServices)(context).isJSON) { return {}; } const { projectGraph, projectRootMappings } = (0, project_graph_utils_1.readProjectGraph)(exports.RULE_NAME); const sourceFilePath = (0, runtime_lint_utils_1.getSourceFilePath)(context.filename ?? context.getFilename(), devkit_1.workspaceRoot); const sourceProject = (0, runtime_lint_utils_1.findProject)(projectGraph, projectRootMappings, sourceFilePath); // If source is not part of an nx workspace, return. if (!sourceProject) { return {}; } const options = normalizeOptions(sourceProject, context.options[0]); context.options[0] = options; const { generatorsJson, executorsJson, migrationsJson, packageJson } = options; if (![generatorsJson, executorsJson, migrationsJson, packageJson].includes(sourceFilePath)) { return {}; } if (!global.tsProjectRegistered) { (0, internal_1.registerTsProject)((0, js_1.getRootTsConfigPath)()); global.tsProjectRegistered = true; } return { ['JSONExpressionStatement > JSONObjectExpression'](node) { if (sourceFilePath === generatorsJson) { checkCollectionFileNode(node, 'generator', context, options); } else if (sourceFilePath === migrationsJson) { checkCollectionFileNode(node, 'migration', context, options); } else if (sourceFilePath === executorsJson) { checkCollectionFileNode(node, 'executor', context, options); } else if (sourceFilePath === packageJson) { validatePackageGroup(node, context); } }, }; }, }); function normalizeOptions(sourceProject, options) { let rootDir; let outDir; const base = { ...DEFAULT_OPTIONS, ...options }; let runtimeTsConfig; if (sourceProject.data.targets?.build?.executor === '@nx/js:tsc') { rootDir = sourceProject.data.targets.build.options.rootDir; outDir = sourceProject.data.targets.build.options.outputPath; } if (!rootDir && !outDir) { try { runtimeTsConfig = require.resolve(path.join(devkit_1.workspaceRoot, sourceProject.data.root, base.tsConfig)); const tsConfig = (0, devkit_1.readJsonFile)(runtimeTsConfig); rootDir ??= tsConfig.compilerOptions?.rootDir ? path.join(sourceProject.data.root, tsConfig.compilerOptions.rootDir) : undefined; outDir ??= tsConfig.compilerOptions?.outDir ? path.join(sourceProject.data.root, tsConfig.compilerOptions.outDir) : undefined; } catch { // nothing } } const pathPrefix = sourceProject.data.root !== '.' ? `${sourceProject.data.root}/` : ''; return { ...base, executorsJson: base.executorsJson ? `${pathPrefix}${base.executorsJson}` : undefined, generatorsJson: base.generatorsJson ? `${pathPrefix}${base.generatorsJson}` : undefined, migrationsJson: base.migrationsJson ? `${pathPrefix}${base.migrationsJson}` : undefined, packageJson: base.packageJson ? `${pathPrefix}${base.packageJson}` : undefined, rootDir, outDir, }; } function checkCollectionFileNode(baseNode, mode, context, options) { const schematicsRootNode = baseNode.properties.find((x) => x.key.type === 'JSONLiteral' && x.key.value === 'schematics'); const generatorsRootNode = baseNode.properties.find((x) => x.key.type === 'JSONLiteral' && x.key.value === 'generators'); const executorsRootNode = baseNode.properties.find((x) => x.key.type === 'JSONLiteral' && x.key.value === 'executors'); const buildersRootNode = baseNode.properties.find((x) => x.key.type === 'JSONLiteral' && x.key.value === 'builders'); if (!schematicsRootNode && !generatorsRootNode && mode !== 'executor') { context.report({ messageId: 'noGeneratorsOrSchematicsFound', node: baseNode, }); return; } if (!executorsRootNode && !buildersRootNode && mode === 'executor') { context.report({ messageId: 'noExecutorsOrBuildersFound', node: baseNode, }); return; } const collectionNodes = [ { collectionNode: schematicsRootNode, key: 'schematics' }, { collectionNode: generatorsRootNode, key: 'generators' }, { collectionNode: executorsRootNode, key: 'executors' }, { collectionNode: buildersRootNode, key: 'builders' }, ].filter(({ collectionNode }) => !!collectionNode); for (const { collectionNode, key } of collectionNodes) { if (collectionNode.value.type !== 'JSONObjectExpression') { context.report({ messageId: 'valueShouldBeObject', data: { key }, node: schematicsRootNode, }); } else { checkCollectionNode(collectionNode.value, mode, context, options); } } } function checkCollectionNode(baseNode, mode, context, options) { const entries = baseNode.properties; for (const entryNode of entries) { if (entryNode.value.type !== 'JSONObjectExpression') { context.report({ messageId: 'valueShouldBeObject', data: { key: entryNode.key.value }, node: entryNode, }); } else if (entryNode.key.type === 'JSONLiteral') { validateEntry(entryNode.value, entryNode.key.value.toString(), mode, context, options); } } } function validateEntry(baseNode, key, mode, context, options) { const schemaNode = baseNode.properties.find((x) => x.key.type === 'JSONLiteral' && x.key.value === 'schema'); if (mode !== 'migration' && !schemaNode) { context.report({ messageId: 'missingRequiredSchema', data: { key, }, node: baseNode, }); } else if (schemaNode) { if (schemaNode.value.type !== 'JSONLiteral' || typeof schemaNode.value.value !== 'string') { context.report({ messageId: 'invalidSchemaPath', node: schemaNode.value, }); } else { let validJsonFound = false; const schemaFilePath = path.join(path.dirname(context.filename ?? context.getFilename()), schemaNode.value.value); try { (0, devkit_1.readJsonFile)(schemaFilePath); validJsonFound = true; } catch { try { // Try to map back to source, which will be the case with TS solution setup. (0, devkit_1.readJsonFile)(schemaFilePath.replace(options.outDir, options.rootDir)); validJsonFound = true; } catch { // nothing, will be reported below } } if (!validJsonFound) { context.report({ messageId: 'invalidSchemaPath', node: schemaNode.value, }); } } } const implementationNode = baseNode.properties.find((x) => x.key.type === 'JSONLiteral' && (x.key.value === 'implementation' || x.key.value === 'factory')); if (!implementationNode) { context.report({ messageId: 'missingImplementation', data: { key, }, node: baseNode, }); } else { validateImplementationNode(implementationNode, key, context, options); } if (mode === 'migration') { const versionNode = baseNode.properties.find((x) => x.key.type === 'JSONLiteral' && x.key.value === 'version'); if (!versionNode) { context.report({ messageId: 'missingVersion', data: { key, }, node: baseNode, }); } else if (versionNode.value.type !== 'JSONLiteral' || typeof versionNode.value.value !== 'string') { context.report({ messageId: 'invalidVersion', data: { key, }, node: versionNode.value, }); } else { const specifiedVersion = versionNode.value.value; if (!(0, semver_1.valid)(specifiedVersion)) { context.report({ messageId: 'invalidVersion', data: { key, }, node: versionNode.value, }); } } } } function validateImplementationNode(implementationNode, key, context, options) { if (implementationNode.value.type !== 'JSONLiteral' || typeof implementationNode.value.value !== 'string') { context.report({ messageId: 'invalidImplementationPath', data: { key, }, node: implementationNode.value, }); } else { const [implementationPath, identifier] = implementationNode.value.value.split('#'); let resolvedPath; const modulePath = path.join(path.dirname(context.filename ?? context.getFilename()), implementationPath); try { resolvedPath = require.resolve(modulePath); } catch { try { resolvedPath = require.resolve(modulePath.replace(options.outDir, options.rootDir)); } catch { // nothing, will be reported below } } if (!resolvedPath) { context.report({ messageId: 'invalidImplementationPath', data: { key, }, node: implementationNode.value, }); } if (identifier) { try { const m = require(resolvedPath); if (!(identifier in m && typeof m[identifier] === 'function')) { context.report({ messageId: 'invalidImplementationModule', node: implementationNode.value, data: { identifier, key, }, }); } } catch { context.report({ messageId: 'unableToReadImplementationExports', node: implementationNode.value, data: { key, }, }); } } } } function validatePackageGroup(baseNode, context) { const migrationsNode = baseNode.properties.find((x) => x.key.type === 'JSONLiteral' && x.value.type === 'JSONObjectExpression' && (x.key.value === 'nx-migrations' || x.key.value === 'ng-update' || x.key.value === 'migrations'))?.value; const packageGroupNode = migrationsNode?.properties.find((x) => x.key.type === 'JSONLiteral' && x.key.value === 'packageGroup'); if (packageGroupNode) { // Package group is defined as an array if (packageGroupNode.value.type === 'JSONArrayExpression') { // Look at entries which are an object const members = packageGroupNode.value.elements.filter((x) => x.type === 'JSONObjectExpression'); // validate that the version property exists and is valid for (const member of members) { const versionPropertyNode = member.properties.find((x) => x.key.type === 'JSONLiteral' && x.key.value === 'version'); const packageNode = member.properties.find((x) => x.key.type === 'JSONLiteral' && x.key.value === 'package'); const key = packageNode?.value?.value ?? 'unknown'; if (versionPropertyNode) { if (!validateVersionJsonExpression(versionPropertyNode.value, context)) context.report({ messageId: 'invalidVersion', data: { key }, node: versionPropertyNode.value, }); } else { context.report({ messageId: 'missingVersion', data: { key }, node: member, }); } } // Package group is defined as an object (Record<PackageName, Version>) } else if (packageGroupNode.value.type === 'JSONObjectExpression') { const properties = packageGroupNode.value.properties; // For each property, ensure its value is a valid version for (const propertyNode of properties) { if (!validateVersionJsonExpression(propertyNode.value, context)) { context.report({ messageId: 'invalidVersion', data: { key: propertyNode.key.value, }, node: propertyNode.value, }); } } } } } function validateVersionJsonExpression(node, context) { return (node && node.type === 'JSONLiteral' && typeof node.value === 'string' && ((0, semver_1.valid)(node.value) || context.options[0]?.allowedVersionStrings.includes(node.value))); }