@nx/eslint
Version:
344 lines (343 loc) • 15.9 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.createNodes = exports.createNodesV2 = void 0;
const devkit_1 = require("@nx/devkit");
const calculate_hash_for_create_nodes_1 = require("@nx/devkit/src/utils/calculate-hash-for-create-nodes");
const node_fs_1 = require("node:fs");
const posix_1 = require("node:path/posix");
const file_hasher_1 = require("nx/src/hasher/file-hasher");
const cache_directory_1 = require("nx/src/utils/cache-directory");
const globs_1 = require("nx/src/utils/globs");
const workspace_context_1 = require("nx/src/utils/workspace-context");
const semver_1 = require("semver");
const config_file_1 = require("../utils/config-file");
const resolve_eslint_class_1 = require("../utils/resolve-eslint-class");
const pmc = (0, devkit_1.getPackageManagerCommand)();
const DEFAULT_EXTENSIONS = [
'ts',
'cts',
'mts',
'tsx',
'js',
'cjs',
'mjs',
'jsx',
'html',
'vue',
];
const PROJECT_CONFIG_FILENAMES = ['project.json', 'package.json'];
const ESLINT_CONFIG_GLOB_V1 = (0, globs_1.combineGlobPatterns)(config_file_1.ESLINT_CONFIG_FILENAMES.map((f) => `**/${f}`));
const ESLINT_CONFIG_GLOB_V2 = (0, globs_1.combineGlobPatterns)([
...config_file_1.ESLINT_CONFIG_FILENAMES.map((f) => `**/${f}`),
...PROJECT_CONFIG_FILENAMES.map((f) => `**/${f}`),
]);
function readTargetsCache(cachePath) {
return process.env.NX_CACHE_PROJECT_GRAPH !== 'false' && (0, node_fs_1.existsSync)(cachePath)
? (0, devkit_1.readJsonFile)(cachePath)
: {};
}
function writeTargetsToCache(cachePath, results) {
(0, devkit_1.writeJsonFile)(cachePath, results);
}
const internalCreateNodes = async (configFilePath, options, context, projectsCache) => {
options = normalizeOptions(options);
const configDir = (0, posix_1.dirname)(configFilePath);
// Ensure that configFiles are set, e2e-run fails due to them being undefined in CI (does not occur locally)
// TODO(JamesHenry): Further troubleshoot this in CI
context.configFiles = context.configFiles ?? [];
// Create a Set of all the directories containing eslint configs, and a
// list of globs to exclude from child projects
const nestedEslintRootPatterns = [];
for (const configFile of context.configFiles) {
const eslintRootDir = (0, posix_1.dirname)(configFile);
if (eslintRootDir !== configDir && isSubDir(configDir, eslintRootDir)) {
nestedEslintRootPatterns.push(`${eslintRootDir}/**/*`);
}
}
const projectFiles = await (0, workspace_context_1.globWithWorkspaceContext)(context.workspaceRoot, ['project.json', 'package.json', '**/project.json', '**/package.json'].map((f) => (0, posix_1.join)(configDir, f)), nestedEslintRootPatterns.length ? nestedEslintRootPatterns : undefined);
// dedupe and sort project roots by depth for more efficient traversal
const dedupedProjectRoots = Array.from(new Set(projectFiles.map((f) => (0, posix_1.dirname)(f)))).sort((a, b) => (a !== b && isSubDir(a, b) ? -1 : 1));
const excludePatterns = dedupedProjectRoots.map((root) => `${root}/**/*`);
const ESLint = await (0, resolve_eslint_class_1.resolveESLintClass)({
useFlatConfigOverrideVal: (0, config_file_1.isFlatConfig)(configFilePath),
});
const eslintVersion = ESLint.version;
const projects = {};
await Promise.all(dedupedProjectRoots.map(async (childProjectRoot, index) => {
// anything after is either a nested project or a sibling project, can be excluded
const nestedProjectRootPatterns = excludePatterns.slice(index + 1);
// Ignore project roots where the project does not contain any lintable files
const lintableFiles = await (0, workspace_context_1.globWithWorkspaceContext)(context.workspaceRoot, [(0, posix_1.join)(childProjectRoot, `**/*.{${options.extensions.join(',')}}`)],
// exclude nested eslint roots and nested project roots
[...nestedEslintRootPatterns, ...nestedProjectRootPatterns]);
const parentConfigs = context.configFiles.filter((eslintConfig) => isSubDir(childProjectRoot, (0, posix_1.dirname)(eslintConfig)));
const hash = await (0, calculate_hash_for_create_nodes_1.calculateHashForCreateNodes)(childProjectRoot, options, context, [...parentConfigs, (0, posix_1.join)(childProjectRoot, '.eslintignore')]);
if (projectsCache[hash]) {
// We can reuse the projects in the cache.
Object.assign(projects, projectsCache[hash]);
return;
}
const eslint = new ESLint({
cwd: (0, posix_1.join)(context.workspaceRoot, childProjectRoot),
});
let hasNonIgnoredLintableFiles = false;
for (const file of lintableFiles) {
if (!(await eslint.isPathIgnored((0, posix_1.join)(context.workspaceRoot, file)))) {
hasNonIgnoredLintableFiles = true;
break;
}
}
if (!hasNonIgnoredLintableFiles) {
// No lintable files in the project, store in the cache and skip further processing
projectsCache[hash] = {};
return;
}
const project = getProjectUsingESLintConfig(configFilePath, childProjectRoot, eslintVersion, options, context);
if (project) {
projects[childProjectRoot] = project;
// Store project into the cache
projectsCache[hash] = { [childProjectRoot]: project };
}
else {
// No project found, store in the cache
projectsCache[hash] = {};
}
}));
return {
projects,
};
};
const internalCreateNodesV2 = async (configFilePath, options, context, projectRootsByEslintRoots, lintableFilesPerProjectRoot, projectsCache, hashByRoot) => {
const configDir = (0, posix_1.dirname)(configFilePath);
const ESLint = await (0, resolve_eslint_class_1.resolveESLintClass)({
useFlatConfigOverrideVal: (0, config_file_1.isFlatConfig)(configFilePath),
});
const eslintVersion = ESLint.version;
const projects = {};
await Promise.all(projectRootsByEslintRoots.get(configDir).map(async (projectRoot) => {
const hash = hashByRoot.get(projectRoot);
if (projectsCache[hash]) {
// We can reuse the projects in the cache.
Object.assign(projects, projectsCache[hash]);
return;
}
const eslint = new ESLint({
cwd: (0, posix_1.join)(context.workspaceRoot, projectRoot),
});
let hasNonIgnoredLintableFiles = false;
for (const file of lintableFilesPerProjectRoot.get(projectRoot) ?? []) {
if (!(await eslint.isPathIgnored((0, posix_1.join)(context.workspaceRoot, file)))) {
hasNonIgnoredLintableFiles = true;
break;
}
}
if (!hasNonIgnoredLintableFiles) {
// No lintable files in the project, store in the cache and skip further processing
projectsCache[hash] = {};
return;
}
const project = getProjectUsingESLintConfig(configFilePath, projectRoot, eslintVersion, options, context);
if (project) {
projects[projectRoot] = project;
// Store project into the cache
projectsCache[hash] = { [projectRoot]: project };
}
else {
// No project found, store in the cache
projectsCache[hash] = {};
}
}));
return {
projects,
};
};
exports.createNodesV2 = [
ESLINT_CONFIG_GLOB_V2,
async (configFiles, options, context) => {
options = normalizeOptions(options);
const optionsHash = (0, file_hasher_1.hashObject)(options);
const cachePath = (0, posix_1.join)(cache_directory_1.workspaceDataDirectory, `eslint-${optionsHash}.hash`);
const targetsCache = readTargetsCache(cachePath);
const { eslintConfigFiles, projectRoots, projectRootsByEslintRoots } = splitConfigFiles(configFiles);
const lintableFilesPerProjectRoot = await collectLintableFilesByProjectRoot(projectRoots, options, context);
const hashes = await (0, calculate_hash_for_create_nodes_1.calculateHashesForCreateNodes)(projectRoots, options, context, projectRoots.map((root) => {
const parentConfigs = eslintConfigFiles.filter((eslintConfig) => isSubDir(root, (0, posix_1.dirname)(eslintConfig)));
return [...parentConfigs, (0, posix_1.join)(root, '.eslintignore')];
}));
const hashByRoot = new Map(projectRoots.map((r, i) => [r, hashes[i]]));
try {
return await (0, devkit_1.createNodesFromFiles)((configFile, options, context) => internalCreateNodesV2(configFile, options, context, projectRootsByEslintRoots, lintableFilesPerProjectRoot, targetsCache, hashByRoot), eslintConfigFiles, options, context);
}
finally {
writeTargetsToCache(cachePath, targetsCache);
}
},
];
exports.createNodes = [
ESLINT_CONFIG_GLOB_V1,
(configFilePath, options, context) => {
devkit_1.logger.warn('`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.');
return internalCreateNodes(configFilePath, options, context, {});
},
];
function splitConfigFiles(configFiles) {
const eslintConfigFiles = [];
const projectRoots = new Set();
for (const configFile of configFiles) {
if (PROJECT_CONFIG_FILENAMES.includes((0, posix_1.basename)(configFile))) {
projectRoots.add((0, posix_1.dirname)(configFile));
}
else {
eslintConfigFiles.push(configFile);
}
}
const uniqueProjectRoots = Array.from(projectRoots);
const projectRootsByEslintRoots = groupProjectRootsByEslintRoots(eslintConfigFiles, uniqueProjectRoots);
return {
eslintConfigFiles,
projectRoots: uniqueProjectRoots,
projectRootsByEslintRoots,
};
}
function groupProjectRootsByEslintRoots(eslintConfigFiles, projectRoots) {
const projectRootsByEslintRoots = new Map();
for (const eslintConfig of eslintConfigFiles) {
projectRootsByEslintRoots.set((0, posix_1.dirname)(eslintConfig), []);
}
for (const projectRoot of projectRoots) {
const eslintRoot = getRootForDirectory(projectRoot, projectRootsByEslintRoots);
if (eslintRoot) {
projectRootsByEslintRoots.get(eslintRoot).push(projectRoot);
}
}
return projectRootsByEslintRoots;
}
async function collectLintableFilesByProjectRoot(projectRoots, options, context) {
const lintableFilesPerProjectRoot = new Map();
const lintableFiles = await (0, workspace_context_1.globWithWorkspaceContext)(context.workspaceRoot, [
`**/*.{${options.extensions.join(',')}}`,
]);
for (const projectRoot of projectRoots) {
lintableFilesPerProjectRoot.set(projectRoot, []);
}
for (const file of lintableFiles) {
const projectRoot = getRootForDirectory((0, posix_1.dirname)(file), lintableFilesPerProjectRoot);
if (projectRoot) {
lintableFilesPerProjectRoot.get(projectRoot).push(file);
}
}
return lintableFilesPerProjectRoot;
}
function getRootForDirectory(directory, roots) {
let currentPath = (0, posix_1.normalize)(directory);
while (currentPath !== (0, posix_1.dirname)(currentPath)) {
if (roots.has(currentPath)) {
return currentPath;
}
currentPath = (0, posix_1.dirname)(currentPath);
}
return roots.has(currentPath) ? currentPath : null;
}
function getProjectUsingESLintConfig(configFilePath, projectRoot, eslintVersion, options, context) {
const rootEslintConfig = [
config_file_1.baseEsLintConfigFile,
...config_file_1.BASE_ESLINT_CONFIG_FILENAMES,
...config_file_1.ESLINT_CONFIG_FILENAMES,
].find((f) => (0, node_fs_1.existsSync)((0, posix_1.join)(context.workspaceRoot, f)));
// Add a lint target for each child project without an eslint config, with the root level config as an input
let standaloneSrcPath;
if (projectRoot === '.' &&
(0, node_fs_1.existsSync)((0, posix_1.join)(context.workspaceRoot, projectRoot, 'package.json'))) {
if ((0, node_fs_1.existsSync)((0, posix_1.join)(context.workspaceRoot, projectRoot, 'src'))) {
standaloneSrcPath = 'src';
}
else if ((0, node_fs_1.existsSync)((0, posix_1.join)(context.workspaceRoot, projectRoot, 'lib'))) {
standaloneSrcPath = 'lib';
}
}
if (projectRoot === '.' && !standaloneSrcPath) {
return null;
}
const eslintConfigs = [configFilePath];
if (rootEslintConfig && !eslintConfigs.includes(rootEslintConfig)) {
eslintConfigs.unshift(rootEslintConfig);
}
return {
targets: buildEslintTargets(eslintConfigs, eslintVersion, projectRoot, context.workspaceRoot, options, standaloneSrcPath),
};
}
function buildEslintTargets(eslintConfigs, eslintVersion, projectRoot, workspaceRoot, options, standaloneSrcPath) {
const isRootProject = projectRoot === '.';
const targets = {};
const targetConfig = {
command: `eslint ${isRootProject && standaloneSrcPath ? `./${standaloneSrcPath}` : '.'}`,
cache: true,
options: {
cwd: projectRoot,
},
inputs: [
'default',
// Certain lint rules can be impacted by changes to dependencies
'^default',
...eslintConfigs.map((config) => `{workspaceRoot}/${config}`.replace(`{workspaceRoot}/${projectRoot}`, isRootProject ? '{projectRoot}/' : '{projectRoot}')),
...((0, node_fs_1.existsSync)((0, posix_1.join)(workspaceRoot, projectRoot, '.eslintignore'))
? ['{projectRoot}/.eslintignore']
: []),
'{workspaceRoot}/tools/eslint-rules/**/*',
{ externalDependencies: ['eslint'] },
],
outputs: ['{options.outputFile}'],
metadata: {
technologies: ['eslint'],
description: 'Runs ESLint on project',
help: {
command: `${pmc.exec} eslint --help`,
example: {
options: {
'max-warnings': 0,
},
},
},
},
};
// Always set the environment variable to ensure that the ESLint CLI can run on eslint v8 and v9
const useFlatConfig = eslintConfigs.some((config) => (0, config_file_1.isFlatConfig)(config));
// Flat config is default for 9.0.0+
const defaultSetting = (0, semver_1.gte)(eslintVersion, '9.0.0');
if (useFlatConfig !== defaultSetting) {
targetConfig.options.env = {
ESLINT_USE_FLAT_CONFIG: useFlatConfig ? 'true' : 'false',
};
}
targets[options.targetName] = targetConfig;
return targets;
}
function normalizeOptions(options) {
const normalizedOptions = {
targetName: options?.targetName ?? 'lint',
};
// Normalize user input for extensions (strip leading . characters)
if (Array.isArray(options?.extensions)) {
normalizedOptions.extensions = options.extensions.map((f) => f.replace(/^\.+/, ''));
}
else {
normalizedOptions.extensions = DEFAULT_EXTENSIONS;
}
return normalizedOptions;
}
/**
* Determines if `child` is a subdirectory of `parent`. This is a simplified
* version that takes into account that paths are always relative to the
* workspace root.
*/
function isSubDir(parent, child) {
if (parent === '.') {
return true;
}
parent = (0, posix_1.normalize)(parent);
child = (0, posix_1.normalize)(child);
if (!parent.endsWith(posix_1.sep)) {
parent += posix_1.sep;
}
return child.startsWith(parent);
}
;