UNPKG

@nx/eslint

Version:

The ESLint plugin for Nx contains executors, generators and utilities used for linting JavaScript/TypeScript projects within an Nx workspace.

255 lines (254 loc) • 12.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.lintProjectGenerator = lintProjectGenerator; exports.lintProjectGeneratorInternal = lintProjectGeneratorInternal; const devkit_1 = require("@nx/devkit"); const eslint_file_1 = require("../utils/eslint-file"); const path_1 = require("path"); const init_1 = require("../init/init"); const init_migration_1 = require("../init/init-migration"); const project_configuration_1 = require("nx/src/generators/utils/project-configuration"); const flat_config_1 = require("../../utils/flat-config"); const ast_utils_1 = require("../utils/flat-config/ast-utils"); const config_file_1 = require("../../utils/config-file"); const plugin_1 = require("../utils/plugin"); const versions_1 = require("../../utils/versions"); const setup_root_eslint_1 = require("./setup-root-eslint"); const ts_solution_setup_1 = require("@nx/js/src/utils/typescript/ts-solution-setup"); function lintProjectGenerator(tree, options) { return lintProjectGeneratorInternal(tree, { addPlugin: false, ...options }); } async function lintProjectGeneratorInternal(tree, options) { const nxJson = (0, devkit_1.readNxJson)(tree); options.eslintConfigFormat ??= 'mjs'; const addPluginDefault = process.env.NX_ADD_PLUGINS !== 'false' && nxJson.useInferencePlugins !== false; options.addPlugin ??= addPluginDefault; const tasks = []; const initTask = await (0, init_1.lintInitGenerator)(tree, { skipPackageJson: options.skipPackageJson, addPlugin: options.addPlugin, eslintConfigFormat: options.eslintConfigFormat, }); tasks.push(initTask); const rootEsLintTask = (0, setup_root_eslint_1.setupRootEsLint)(tree, { unitTestRunner: options.unitTestRunner, skipPackageJson: options.skipPackageJson, rootProject: options.rootProject, eslintConfigFormat: options.eslintConfigFormat, }); tasks.push(rootEsLintTask); const projectConfig = (0, devkit_1.readProjectConfiguration)(tree, options.project); let lintFilePatterns = options.eslintFilePatterns; if (!lintFilePatterns && options.rootProject && projectConfig.root === '.') { lintFilePatterns = ['./src']; } if (lintFilePatterns && lintFilePatterns.length && !lintFilePatterns.includes('{projectRoot}') && isBuildableLibraryProject(tree, projectConfig)) { lintFilePatterns.push(`{projectRoot}/package.json`); } const hasPlugin = (0, plugin_1.hasEslintPlugin)(tree); if (hasPlugin && !options.addExplicitTargets) { if (lintFilePatterns && lintFilePatterns.length && lintFilePatterns.some((p) => !['./src', '{projectRoot}', projectConfig.root].includes(p))) { projectConfig.targets ??= {}; projectConfig.targets['lint'] = { command: `eslint ${lintFilePatterns .join(' ') .replace('{projectRoot}', projectConfig.root)}`, }; } } else { projectConfig.targets ??= {}; projectConfig.targets['lint'] = { executor: '@nx/eslint:lint', }; if (lintFilePatterns && lintFilePatterns.length) { // only add lintFilePatterns if they are explicitly defined projectConfig.targets['lint'].options = { lintFilePatterns, }; } } // we are adding new project which is not the root project or // companion e2e app so we should check if migration to // monorepo style is needed if (!options.rootProject) { const projects = {}; (0, project_configuration_1.getProjects)(tree).forEach((v, k) => (projects[k] = v)); const graph = await (0, devkit_1.createProjectGraphAsync)(); if (isMigrationToMonorepoNeeded(tree, graph)) { // we only migrate project configurations that have been created const filteredProjects = []; Object.entries(projects).forEach(([name, project]) => { if (name !== options.project) { filteredProjects.push(project); } }); const migrateTask = (0, init_migration_1.migrateConfigToMonorepoStyle)(filteredProjects, tree, options.unitTestRunner, options.eslintConfigFormat, options.keepExistingVersions); tasks.push(migrateTask); } } // our root `.eslintrc` is already the project config, so we should not override it // additionally, the companion e2e app would have `rootProject: true` // so we need to check for the root path as well if (!options.rootProject || projectConfig.root !== '.') { const addDependencyChecks = options.addPackageJsonDependencyChecks || isBuildableLibraryProject(tree, projectConfig); createEsLintConfiguration(tree, options, projectConfig, options.setParserOptionsProject, options.rootProject, addDependencyChecks); if (addDependencyChecks) { tasks.push((0, devkit_1.addDependenciesToPackageJson)(tree, {}, { 'jsonc-eslint-parser': versions_1.jsoncEslintParserVersion }, undefined, true)); } } // Buildable libs need source analysis enabled for linting `package.json`. if (isBuildableLibraryProject(tree, projectConfig) && !isJsAnalyzeSourceFilesEnabled(tree)) { (0, devkit_1.updateJson)(tree, 'nx.json', (json) => { json.pluginsConfig ??= {}; json.pluginsConfig['@nx/js'] ??= {}; json.pluginsConfig['@nx/js'].analyzeSourceFiles = true; return json; }); } (0, devkit_1.updateProjectConfiguration)(tree, options.project, projectConfig); if (!options.skipFormat) { await (0, devkit_1.formatFiles)(tree); } return (0, devkit_1.runTasksInSerial)(...tasks); } function createEsLintConfiguration(tree, options, projectConfig, setParserOptionsProject, rootProject, addDependencyChecks) { // we are only extending root for non-standalone projects or their complementary e2e apps const extendedRootConfig = rootProject ? undefined : (0, eslint_file_1.findEslintFile)(tree); const pathToRootConfig = extendedRootConfig ? `${(0, devkit_1.offsetFromRoot)(projectConfig.root)}${extendedRootConfig}` : undefined; if (extendedRootConfig) { // We do not want to mix the formats // if the base file extension is `.mjs` we should use `mjs` for the new file // or if base the file extension is `.cjs` then the format should be `cjs` const fileExtension = (0, path_1.extname)(extendedRootConfig); if (fileExtension === '.mjs' || fileExtension === '.cjs') { options.eslintConfigFormat = fileExtension.slice(1); } else { options.eslintConfigFormat = (0, eslint_file_1.determineEslintConfigFormat)(tree.read(extendedRootConfig, 'utf-8')); } } const overrides = (0, flat_config_1.useFlatConfig)(tree) ? // For flat configs, we don't need to generate different overrides for each file. Users should add their own overrides as needed. [] : [ { files: ['*.ts', '*.tsx', '*.js', '*.jsx'], /** * NOTE: We no longer set parserOptions.project by default when creating new projects. * * We have observed that users rarely add rules requiring type-checking to their Nx workspaces, and therefore * do not actually need the capabilites which parserOptions.project provides. When specifying parserOptions.project, * typescript-eslint needs to create full TypeScript Programs for you. When omitting it, it can perform a simple * parse (and AST tranformation) of the source files it encounters during a lint run, which is much faster and much * less memory intensive. * * In the rare case that users attempt to add rules requiring type-checking to their setup later on (and haven't set * parserOptions.project), the executor will attempt to look for the particular error typescript-eslint gives you * and provide feedback to the user. */ parserOptions: !setParserOptionsProject ? undefined : { project: [`${projectConfig.root}/tsconfig.*?.json`], }, /** * Having an empty rules object present makes it more obvious to the user where they would * extend things from if they needed to */ rules: {}, }, { files: ['*.ts', '*.tsx'], rules: {}, }, { files: ['*.js', '*.jsx'], rules: {}, }, ]; if (addDependencyChecks) { overrides.push({ files: ['*.json'], parser: 'jsonc-eslint-parser', rules: { '@nx/dependency-checks': [ 'error', { // With flat configs, we don't want to include imports in the eslint js/cjs/mjs files to be checked ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs}'], }, ], }, }); } if ((0, flat_config_1.useFlatConfig)(tree)) { const nodes = []; const importMap = new Map(); if (extendedRootConfig) { importMap.set(pathToRootConfig, 'baseConfig'); nodes.push((0, ast_utils_1.generateSpreadElement)('baseConfig')); } overrides.forEach((override) => { nodes.push((0, ast_utils_1.generateFlatOverride)(override, options.eslintConfigFormat)); }); const nodeList = (0, ast_utils_1.createNodeList)(importMap, nodes, options.eslintConfigFormat); const content = (0, ast_utils_1.stringifyNodeList)(nodeList); tree.write((0, path_1.join)(projectConfig.root, `eslint.config.${options.eslintConfigFormat}`), content); } else { (0, devkit_1.writeJson)(tree, (0, path_1.join)(projectConfig.root, `.eslintrc.json`), { extends: extendedRootConfig ? [pathToRootConfig] : undefined, // Include project files to be linted since the global one excludes all files. ignorePatterns: ['!**/*'], overrides, }); } } function isJsAnalyzeSourceFilesEnabled(tree) { const nxJson = (0, devkit_1.readJson)(tree, 'nx.json'); const jsPluginConfig = nxJson.pluginsConfig?.['@nx/js']; return (jsPluginConfig?.analyzeSourceFiles ?? nxJson.extends !== 'nx/presets/npm.json'); } function isBuildableLibraryProject(tree, projectConfig) { return ((0, ts_solution_setup_1.getProjectType)(tree, projectConfig.root, projectConfig.projectType) === 'library' && projectConfig.targets?.build && !!projectConfig.targets.build); } /** * Detect based on the state of lint target configuration of the root project * if we should migrate eslint configs to monorepo style */ function isMigrationToMonorepoNeeded(tree, graph) { // the base config is already created, migration has been done if ([config_file_1.baseEsLintConfigFile, ...config_file_1.BASE_ESLINT_CONFIG_FILENAMES].some((f) => tree.exists(f))) { return false; } const nodes = Object.values(graph.nodes); // get root project const rootProject = nodes.find((p) => p.data.root === '.'); if (!rootProject || !rootProject.data.targets) { return false; } for (const targetConfig of Object.values(rootProject.data.targets ?? {})) { if (['@nx/eslint:lint', '@nx/linter:eslint'].includes(targetConfig.executor) || (targetConfig.executor === 'nx:run-commands' && targetConfig.options?.command && targetConfig.options?.command.startsWith('eslint '))) { return true; } } return false; }