UNPKG

@nx/eslint-plugin

Version:

The eslint-plugin package is an ESLint plugin that contains a collection of recommended ESLint rule configurations which you can extend from in your own ESLint configs, as well as an Nx-specific lint rule called enforce-module-boundaries.

379 lines (378 loc) • 15.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.stringifyTags = stringifyTags; exports.hasNoneOfTheseTags = hasNoneOfTheseTags; exports.isComboDepConstraint = isComboDepConstraint; exports.findDependenciesWithTags = findDependenciesWithTags; exports.matchImportWithWildcard = matchImportWithWildcard; exports.isRelative = isRelative; exports.getTargetProjectBasedOnRelativeImport = getTargetProjectBasedOnRelativeImport; exports.findProject = findProject; exports.isAbsoluteImportIntoAnotherProject = isAbsoluteImportIntoAnotherProject; exports.findProjectUsingImport = findProjectUsingImport; exports.findConstraintsFor = findConstraintsFor; exports.hasStaticImportOfDynamicResource = hasStaticImportOfDynamicResource; exports.getSourceFilePath = getSourceFilePath; exports.hasBannedImport = hasBannedImport; exports.findTransitiveExternalDependencies = findTransitiveExternalDependencies; exports.hasBannedDependencies = hasBannedDependencies; exports.isDirectDependency = isDirectDependency; exports.hasBuildExecutor = hasBuildExecutor; exports.isTerminalRun = isTerminalRun; exports.groupImports = groupImports; exports.belongsToDifferentNgEntryPoint = belongsToDifferentNgEntryPoint; exports.appIsMFERemote = appIsMFERemote; exports.getParserServices = getParserServices; const tslib_1 = require("tslib"); const devkit_1 = require("@nx/devkit"); const js_1 = require("@nx/js"); const internal_1 = require("@nx/js/src/internal"); const utils_1 = require("@typescript-eslint/utils"); const path = tslib_1.__importStar(require("node:path")); const find_project_for_path_1 = require("nx/src/project-graph/utils/find-project-for-path"); const fileutils_1 = require("nx/src/utils/fileutils"); const graph_utils_1 = require("./graph-utils"); function stringifyTags(tags) { return tags.map((t) => `"${t}"`).join(', '); } function hasNoneOfTheseTags(proj, tags) { return tags.filter((tag) => hasTag(proj, tag)).length === 0; } function isComboDepConstraint(depConstraint) { return !!depConstraint.allSourceTags; } /** * Check if any of the given tags is included in the project * @param proj ProjectGraphProjectNode * @param tags * @returns */ function findDependenciesWithTags(targetProject, tags, graph) { // find all reachable projects that have one of the tags and // are reacheable from the targetProject (including self) const allReachableProjects = Object.keys(graph.nodes).filter((projectName) => (0, graph_utils_1.pathExists)(graph, targetProject.name, projectName) && tags.some((tag) => hasTag(graph.nodes[projectName], tag))); // return path from targetProject to reachable project return allReachableProjects.map((project) => targetProject.name === project ? [targetProject] : (0, graph_utils_1.getPath)(graph, targetProject.name, project)); } const regexMap = new Map(); function hasTag(proj, tag) { if (tag === '*') return true; // if the tag is a regex, check if the project matches the regex if (tag.startsWith('/') && tag.endsWith('/')) { let regex; if (regexMap.has(tag)) { regex = regexMap.get(tag); } else { regex = new RegExp(tag.substring(1, tag.length - 1)); regexMap.set(tag, regex); } return (proj.data.tags || []).some((t) => regex.test(t)); } // if the tag is a glob, check if the project matches the glob prefix if (tag.includes('*')) { const regex = mapGlobToRegExp(tag); return (proj.data.tags || []).some((t) => regex.test(t)); } return (proj.data.tags || []).indexOf(tag) > -1; } function matchImportWithWildcard( // This may or may not contain wildcards ("*") allowableImport, extractedImport) { if (allowableImport.endsWith('/**')) { const prefix = allowableImport.substring(0, allowableImport.length - 2); return extractedImport.startsWith(prefix); } else if (allowableImport.endsWith('/*')) { const prefix = allowableImport.substring(0, allowableImport.length - 1); if (!extractedImport.startsWith(prefix)) return false; return extractedImport.substring(prefix.length).indexOf('/') === -1; } else if (allowableImport.indexOf('/**/') > -1) { const [prefix, suffix] = allowableImport.split('/**/'); return (extractedImport.startsWith(prefix) && extractedImport.endsWith(suffix)); } else { return new RegExp(allowableImport).test(extractedImport); } } function isRelative(s) { return s.startsWith('./') || s.startsWith('../'); } function getTargetProjectBasedOnRelativeImport(imp, projectPath, projectGraph, projectRootMappings, sourceFilePath) { if (!isRelative(imp)) { return undefined; } const sourceDir = path.join(projectPath, path.dirname(sourceFilePath)); const targetFile = (0, devkit_1.normalizePath)(path.resolve(sourceDir, imp)).substring(projectPath.length + 1); return findProject(projectGraph, projectRootMappings, targetFile); } function findProject(projectGraph, projectRootMappings, sourceFilePath) { return projectGraph.nodes[(0, find_project_for_path_1.findProjectForPath)(sourceFilePath, projectRootMappings)]; } function isAbsoluteImportIntoAnotherProject(imp, workspaceLayout = { libsDir: 'libs', appsDir: 'apps' }) { return (imp.startsWith(`${workspaceLayout.libsDir}/`) || imp.startsWith(`/${workspaceLayout.libsDir}/`) || imp.startsWith(`${workspaceLayout.appsDir}/`) || imp.startsWith(`/${workspaceLayout.appsDir}/`)); } function findProjectUsingImport(projectGraph, targetProjectLocator, filePath, imp) { const target = targetProjectLocator.findProjectFromImport(imp, filePath); return projectGraph.nodes[target] || projectGraph.externalNodes?.[target]; } function findConstraintsFor(depConstraints, sourceProject) { return depConstraints.filter((f) => { if (isComboDepConstraint(f)) { return f.allSourceTags.every((tag) => hasTag(sourceProject, tag)); } else { return hasTag(sourceProject, f.sourceTag); } }); } function hasStaticImportOfDynamicResource(node, graph, sourceProjectName, targetProjectName) { if (node.type !== utils_1.AST_NODE_TYPES.ImportDeclaration || node.importKind === 'type') { return false; } return onlyLoadChildren(graph, sourceProjectName, targetProjectName, []); } function onlyLoadChildren(graph, sourceProjectName, targetProjectName, visited) { if (visited.indexOf(sourceProjectName) > -1) { return false; } return ((graph.dependencies[sourceProjectName] || []).filter((d) => { if (d.type !== devkit_1.DependencyType.dynamic) { return false; } if (d.target === targetProjectName) { return true; } return onlyLoadChildren(graph, d.target, targetProjectName, [ ...visited, sourceProjectName, ]); }).length > 0); } function getSourceFilePath(sourceFileName, projectPath) { const normalizedProjectPath = (0, devkit_1.normalizePath)(projectPath); const normalizedSourceFileName = (0, devkit_1.normalizePath)(sourceFileName); return normalizedSourceFileName.slice(normalizedProjectPath.length + 1); } /** * Find constraint (if any) that explicitly banns the given target npm project * @param externalProject * @param depConstraints * @returns */ function isConstraintBanningProject(externalProject, constraint, imp) { const { allowedExternalImports, bannedExternalImports } = constraint; const { packageName } = externalProject.data; if (imp !== packageName && !imp.startsWith(`${packageName}/`)) { return false; } /* Check if import is banned... */ if (bannedExternalImports?.some((importDefinition) => mapGlobToRegExp(importDefinition).test(imp))) { return true; } /* ... then check if there is a whitelist and if there is a match in the whitelist. */ return allowedExternalImports?.every((importDefinition) => !imp.startsWith(packageName) || !mapGlobToRegExp(importDefinition).test(imp)); } function hasBannedImport(source, target, depConstraints, imp) { // return those constraints that match source project depConstraints = depConstraints.filter((c) => { let tags = []; if (isComboDepConstraint(c)) { tags = c.allSourceTags; } else { tags = [c.sourceTag]; } return tags.every((t) => hasTag(source, t)); }); return depConstraints.find((constraint) => isConstraintBanningProject(target, constraint, imp)); } /** * Find all unique (transitive) external dependencies of given project * @param graph * @param source * @returns */ function findTransitiveExternalDependencies(graph, source) { if (!graph.externalNodes) { return []; } const allReachableProjects = []; const allProjects = Object.keys(graph.nodes); for (let i = 0; i < allProjects.length; i++) { if ((0, graph_utils_1.pathExists)(graph, source.name, allProjects[i])) { allReachableProjects.push(allProjects[i]); } } const externalDependencies = []; for (let i = 0; i < allReachableProjects.length; i++) { const dependencies = graph.dependencies[allReachableProjects[i]]; if (dependencies) { for (let d = 0; d < dependencies.length; d++) { const dependency = dependencies[d]; if (graph.externalNodes[dependency.target]) { externalDependencies.push(dependency); } } } } return externalDependencies; } /** * Check if * @param externalDependencies * @param graph * @param depConstraint * @returns */ function hasBannedDependencies(externalDependencies, graph, depConstraint, imp) { return externalDependencies .filter((dependency) => isConstraintBanningProject(graph.externalNodes[dependency.target], depConstraint, imp)) .map((dep) => [ graph.externalNodes[dep.target], graph.nodes[dep.source], depConstraint, ]); } function isDirectDependency(source, target) { return (packageExistsInPackageJson(target.data.packageName, '.') || packageExistsInPackageJson(target.data.packageName, source.data.root)); } function packageExistsInPackageJson(packageName, projectRoot) { const content = (0, fileutils_1.readFileIfExisting)(path.join(devkit_1.workspaceRoot, projectRoot, 'package.json')); if (content) { const { dependencies, devDependencies, peerDependencies } = (0, devkit_1.parseJson)(content); if (dependencies && dependencies[packageName]) { return true; } if (peerDependencies && peerDependencies[packageName]) { return true; } if (devDependencies && devDependencies[packageName]) { return true; } } return false; } /** * Maps import with wildcards to regex pattern * @param importDefinition * @returns */ function mapGlobToRegExp(importDefinition) { // we replace all instances of `*`, `**..*` and `.*` with `.*` const mappedWildcards = importDefinition.split(/(?:\.\*)|\*+/).join('.*'); return new RegExp(`^${new RegExp(mappedWildcards).source}$`); } /** * Verifies whether the given node has a builder target * @param projectGraph the node to verify * @param buildTargets the list of targets to check for */ function hasBuildExecutor(projectGraph, buildTargets = ['build']) { return (projectGraph.data.targets && buildTargets.some((target) => projectGraph.data.targets[target] && projectGraph.data.targets[target].executor !== '')); } const ESLINT_REGEX = /node_modules.*[\/\\]eslint(?:\.js)?$/; const JEST_REGEX = /node_modules\/.bin\/jest$/; // when we run unit tests in jest const NRWL_CLI_REGEX = /nx[\/\\]bin[\/\\]run-executor\.js$/; function isTerminalRun() { return (process.argv.length > 1 && (!!process.argv[1].match(NRWL_CLI_REGEX) || !!process.argv[1].match(JEST_REGEX) || !!process.argv[1].match(ESLINT_REGEX) || !!process.argv[1].endsWith('/bin/jest.js'))); } /** * Takes an array of imports and tries to group them, so rather than having * `import { A } from './some-location'` and `import { B } from './some-location'` you get * `import { A, B } from './some-location'` * @param importsToRemap * @returns */ function groupImports(importsToRemap) { const importsToRemapGrouped = importsToRemap.reduce((acc, curr) => { const existing = acc.find((i) => i.importPath === curr.importPath && i.member !== curr.member); if (existing) { if (existing.member) { existing.member += `, ${curr.member}`; } } else { acc.push({ importPath: curr.importPath, member: curr.member, }); } return acc; }, []); return importsToRemapGrouped .map((entry) => `import { ${entry.member} } from '${entry.importPath}';`) .join('\n'); } /** * Checks if source file belongs to a secondary entry point different than the import one */ function belongsToDifferentNgEntryPoint(importExpr, filePath, projectRoot) { const resolvedImportFile = (0, internal_1.resolveModuleByImport)(importExpr, filePath, // not strictly necessary, but speeds up resolution path.join(devkit_1.workspaceRoot, (0, js_1.getRootTsConfigFileName)())); if (!resolvedImportFile) { return false; } const importEntryPoint = getAngularEntryPoint(resolvedImportFile, projectRoot); const srcEntryPoint = getAngularEntryPoint(filePath, projectRoot); // check if the entry point of import expression is different than the source file's entry point return importEntryPoint !== srcEntryPoint; } function getAngularEntryPoint(file, projectRoot) { let parent = (0, devkit_1.joinPathFragments)(file, '../'); while (parent !== `${projectRoot}/`) { // we need to find closest existing ng-package.json // in order to determine if the file matches the secondary entry point const ngPackageContent = (0, fileutils_1.readFileIfExisting)(path.join(devkit_1.workspaceRoot, parent, 'ng-package.json')); if (ngPackageContent) { // https://github.com/ng-packagr/ng-packagr/blob/23c718d04eea85e015b4c261310b7bd0c39e5311/src/ng-package.schema.json#L54 const entryFile = (0, devkit_1.parseJson)(ngPackageContent)?.lib?.entryFile; return (0, devkit_1.joinPathFragments)(parent, entryFile); } parent = (0, devkit_1.joinPathFragments)(parent, '../'); } return undefined; } /** * Returns true if the given project contains MFE config with "exposes:" section */ function appIsMFERemote(project) { const mfeConfig = (0, fileutils_1.readFileIfExisting)(path.join(devkit_1.workspaceRoot, project.data.root, 'module-federation.config.js')) || (0, fileutils_1.readFileIfExisting)(path.join(devkit_1.workspaceRoot, project.data.root, 'module-federation.config.ts')); if (mfeConfig) { return !!mfeConfig.match(/('|")?exposes('|")?:/); } return false; } /** * parserServices moved from the context object to the nested sourceCode object in v8, * and was removed from its original location in v9. */ function getParserServices(context) { if (context.sourceCode && context.sourceCode.parserServices) { return context.sourceCode.parserServices; } const parserServices = context.parserServices; if (!parserServices) { throw new Error('Parser Services are not available, please check your ESLint configuration'); } return parserServices; }