281 lines
8.2 KiB
TypeScript
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;
|
|
}
|