@nx/eslint
Version:
255 lines (254 loc) • 12.2 kB
JavaScript
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;
}
;