281 lines
8.2 KiB
TypeScript

import type {
NxJsonConfiguration,
ProjectConfiguration,
Tree,
} from '@nx/devkit';
import {
formatFiles,
offsetFromRoot,
readJson,
readProjectConfiguration,
updateJson,
updateProjectConfiguration,
writeJson,
} from '@nx/devkit';
import { Linter as LinterEnum } from '../utils/linter';
import {
baseEsLintConfigFile,
baseEsLintFlatConfigFile,
findEslintFile,
} from '../utils/eslint-file';
import { join } from 'path';
import { lintInitGenerator } from '../init/init';
import type { Linter } from 'eslint';
import {
findLintTarget,
migrateConfigToMonorepoStyle,
} from '../init/init-migration';
import { getProjects } from 'nx/src/generators/utils/project-configuration';
import { useFlatConfig } from '../../utils/flat-config';
import {
createNodeList,
generateFlatOverride,
generateSpreadElement,
stringifyNodeList,
} from '../utils/flat-config/ast-utils';
interface LintProjectOptions {
project: string;
linter?: LinterEnum;
eslintFilePatterns?: string[];
tsConfigPaths?: string[];
skipFormat: boolean;
setParserOptionsProject?: boolean;
skipPackageJson?: boolean;
unitTestRunner?: string;
rootProject?: boolean;
}
export async function lintProjectGenerator(
tree: Tree,
options: LintProjectOptions
) {
const installTask = lintInitGenerator(tree, {
linter: options.linter,
unitTestRunner: options.unitTestRunner,
skipPackageJson: options.skipPackageJson,
rootProject: options.rootProject,
});
const projectConfig = readProjectConfiguration(tree, options.project);
projectConfig.targets['lint'] = {
executor: '@nx/eslint:lint',
outputs: ['{options.outputFile}'],
};
let lintFilePatterns = options.eslintFilePatterns;
if (!lintFilePatterns && options.rootProject && projectConfig.root === '.') {
lintFilePatterns = ['./src'];
}
if (lintFilePatterns && lintFilePatterns.length) {
if (
isBuildableLibraryProject(projectConfig) &&
!lintFilePatterns.includes('{projectRoot}')
) {
lintFilePatterns.push(`{projectRoot}/package.json`);
}
// 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 = {} as any;
getProjects(tree).forEach((v, k) => (projects[k] = v));
if (isMigrationToMonorepoNeeded(projects, tree)) {
// we only migrate project configurations that have been created
const filteredProjects = [];
Object.entries(projects).forEach(([name, project]) => {
if (name !== options.project) {
filteredProjects.push(project);
}
});
migrateConfigToMonorepoStyle(
filteredProjects,
tree,
options.unitTestRunner
);
}
}
// 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 !== '.') {
createEsLintConfiguration(
tree,
projectConfig,
options.setParserOptionsProject,
options.rootProject
);
}
// Buildable libs need source analysis enabled for linting `package.json`.
if (
isBuildableLibraryProject(projectConfig) &&
!isJsAnalyzeSourceFilesEnabled(tree)
) {
updateJson(tree, 'nx.json', (json) => {
json.pluginsConfig ??= {};
json.pluginsConfig['@nx/js'] ??= {};
json.pluginsConfig['@nx/js'].analyzeSourceFiles = true;
return json;
});
}
updateProjectConfiguration(tree, options.project, projectConfig);
if (!options.skipFormat) {
await formatFiles(tree);
}
return installTask;
}
function createEsLintConfiguration(
tree: Tree,
projectConfig: ProjectConfiguration,
setParserOptionsProject: boolean,
rootProject: boolean
) {
// we are only extending root for non-standalone projects or their complementary e2e apps
const extendedRootConfig = rootProject ? undefined : findEslintFile(tree);
const pathToRootConfig = extendedRootConfig
? `${offsetFromRoot(projectConfig.root)}${extendedRootConfig}`
: undefined;
const addDependencyChecks = isBuildableLibraryProject(projectConfig);
const overrides: Linter.ConfigOverride<Linter.RulesRecord>[] = [
{
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 (isBuildableLibraryProject(projectConfig)) {
overrides.push({
files: ['*.json'],
parser: 'jsonc-eslint-parser',
rules: {
'@nx/dependency-checks': 'error',
},
});
}
if (useFlatConfig(tree)) {
const isCompatNeeded = addDependencyChecks;
const nodes = [];
const importMap = new Map();
if (extendedRootConfig) {
importMap.set(pathToRootConfig, 'baseConfig');
nodes.push(generateSpreadElement('baseConfig'));
}
overrides.forEach((override) => {
nodes.push(generateFlatOverride(override));
});
const nodeList = createNodeList(importMap, nodes, isCompatNeeded);
const content = stringifyNodeList(nodeList);
tree.write(join(projectConfig.root, 'eslint.config.js'), content);
} else {
writeJson(tree, 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: Tree): boolean {
const nxJson = readJson<NxJsonConfiguration>(tree, 'nx.json');
const jsPluginConfig = nxJson.pluginsConfig?.['@nx/js'] as {
analyzeSourceFiles?: boolean;
};
return (
jsPluginConfig?.analyzeSourceFiles ??
nxJson.extends !== 'nx/presets/npm.json'
);
}
function isBuildableLibraryProject(
projectConfig: ProjectConfiguration
): boolean {
return (
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(
projects: Record<string, ProjectConfiguration>,
tree: Tree
): boolean {
// the base config is already created, migration has been done
if (
tree.exists(baseEsLintConfigFile) ||
tree.exists(baseEsLintFlatConfigFile)
) {
return false;
}
const configs = Object.values(projects);
if (configs.length === 1) {
return false;
}
// get root project
const rootProject = configs.find((p) => p.root === '.');
if (!rootProject || !rootProject.targets) {
return false;
}
// find if root project has lint target
const lintTarget = findLintTarget(rootProject);
if (!lintTarget) {
return false;
}
return true;
}