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.

512 lines (511 loc) • 29.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RULE_NAME = void 0; const devkit_1 = require("@nx/devkit"); const utils_1 = require("@typescript-eslint/utils"); const internal_1 = require("@nx/js/src/internal"); const fileutils_1 = require("nx/src/utils/fileutils"); const path_1 = require("path"); const ast_utils_1 = require("../utils/ast-utils"); const graph_utils_1 = require("../utils/graph-utils"); const project_graph_utils_1 = require("../utils/project-graph-utils"); const runtime_lint_utils_1 = require("../utils/runtime-lint-utils"); const project_graph_1 = require("nx/src/config/project-graph"); exports.RULE_NAME = 'enforce-module-boundaries'; exports.default = utils_1.ESLintUtils.RuleCreator(() => `https://github.com/nrwl/nx/blob/${devkit_1.NX_VERSION}/docs/generated/packages/eslint-plugin/documents/enforce-module-boundaries.md`)({ name: exports.RULE_NAME, meta: { type: 'suggestion', docs: { description: `Ensure that module boundaries are respected within the monorepo`, }, fixable: 'code', schema: [ { type: 'object', properties: { enforceBuildableLibDependency: { type: 'boolean' }, allowCircularSelfDependency: { type: 'boolean' }, checkDynamicDependenciesExceptions: { type: 'array', items: { type: 'string' }, }, ignoredCircularDependencies: { type: 'array', items: { type: 'array', items: { type: 'string' }, minItems: 2, maxItems: 2, }, }, banTransitiveDependencies: { type: 'boolean' }, checkNestedExternalImports: { type: 'boolean' }, allow: { type: 'array', items: { type: 'string' } }, buildTargets: { type: 'array', items: { type: 'string' } }, depConstraints: { type: 'array', items: { oneOf: [ { type: 'object', properties: { sourceTag: { type: 'string' }, onlyDependOnLibsWithTags: { type: 'array', items: { type: 'string' }, }, allowedExternalImports: { type: 'array', items: { type: 'string' }, }, bannedExternalImports: { type: 'array', items: { type: 'string' }, }, notDependOnLibsWithTags: { type: 'array', items: { type: 'string' }, }, }, additionalProperties: false, }, { type: 'object', properties: { allSourceTags: { type: 'array', items: { type: 'string' }, minItems: 2, }, onlyDependOnLibsWithTags: { type: 'array', items: { type: 'string' }, }, allowedExternalImports: { type: 'array', items: { type: 'string' }, }, bannedExternalImports: { type: 'array', items: { type: 'string' }, }, notDependOnLibsWithTags: { type: 'array', items: { type: 'string' }, }, }, additionalProperties: false, }, ], }, }, }, additionalProperties: false, }, ], messages: { noRelativeOrAbsoluteImportsAcrossLibraries: `Projects cannot be imported by a relative or absolute path, and must begin with a npm scope`, noRelativeOrAbsoluteExternals: `External resources cannot be imported using a relative or absolute path`, noCircularDependencies: `Circular dependency between "{{sourceProjectName}}" and "{{targetProjectName}}" detected: {{path}}\n\nCircular file chain:\n{{filePaths}}`, noSelfCircularDependencies: `Projects should use relative imports to import from other files within the same project. Use "./path/to/file" instead of import from "{{imp}}"`, noImportsOfApps: 'Imports of apps are forbidden', noImportsOfE2e: 'Imports of e2e projects are forbidden', noImportOfNonBuildableLibraries: 'Buildable libraries cannot import or export from non-buildable libraries', noImportsOfLazyLoadedLibraries: `Static imports of lazy-loaded libraries are forbidden.\n\nLibrary "{{targetProjectName}}" is lazy-loaded in these files:\n{{filePaths}}`, projectWithoutTagsCannotHaveDependencies: `A project without tags matching at least one constraint cannot depend on any libraries`, bannedExternalImportsViolation: `A project tagged with "{{sourceTag}}" is not allowed to import "{{imp}}"`, nestedBannedExternalImportsViolation: `A project tagged with "{{sourceTag}}" is not allowed to import "{{imp}}". Nested import found at {{childProjectName}}`, noTransitiveDependencies: `Only packages defined in the "package.json" can be imported. Transitive or unresolvable dependencies are not allowed.`, onlyTagsConstraintViolation: `A project tagged with "{{sourceTag}}" can only depend on libs tagged with {{tags}}`, emptyOnlyTagsConstraintViolation: 'A project tagged with "{{sourceTag}}" cannot depend on any libs with tags', notTagsConstraintViolation: `A project tagged with "{{sourceTag}}" can not depend on libs tagged with {{tags}}\n\nViolation detected in:\n{{projects}}`, }, }, defaultOptions: [ { allow: [], buildTargets: ['build'], depConstraints: [], enforceBuildableLibDependency: false, allowCircularSelfDependency: false, checkDynamicDependenciesExceptions: [], ignoredCircularDependencies: [], banTransitiveDependencies: false, checkNestedExternalImports: false, }, ], create(context, [{ allow, buildTargets, depConstraints, enforceBuildableLibDependency, allowCircularSelfDependency, checkDynamicDependenciesExceptions, ignoredCircularDependencies, banTransitiveDependencies, checkNestedExternalImports, },]) { /** * Globally cached info about workspace */ const projectPath = (0, devkit_1.normalizePath)(global.projectPath || devkit_1.workspaceRoot); const fileName = (0, devkit_1.normalizePath)(context.filename ?? context.getFilename()); const { projectGraph, projectRootMappings, projectFileMap, targetProjectLocator, } = (0, project_graph_utils_1.readProjectGraph)(exports.RULE_NAME); if (!projectGraph) { return {}; } const workspaceLayout = global.workspaceLayout; const expandedIgnoreCircularDependencies = (0, graph_utils_1.expandIgnoredCircularDependencies)(ignoredCircularDependencies, projectGraph); function run(node) { // Ignoring ExportNamedDeclarations like: // export class Foo {} if (!node.source) { return; } // accept only literals because template literals have no value if (node.source.type !== utils_1.AST_NODE_TYPES.Literal) { return; } const imp = node.source.value; // whitelisted import if (allow.some((a) => (0, runtime_lint_utils_1.matchImportWithWildcard)(a, imp))) { return; } const sourceFilePath = (0, runtime_lint_utils_1.getSourceFilePath)(fileName, projectPath); const sourceProject = (0, runtime_lint_utils_1.findProject)(projectGraph, projectRootMappings, sourceFilePath); // If source is not part of an nx workspace, return. if (!sourceProject) { return; } // check for relative and absolute imports const isAbsoluteImportIntoAnotherProj = (0, runtime_lint_utils_1.isAbsoluteImportIntoAnotherProject)(imp, workspaceLayout); let targetProject; if (isAbsoluteImportIntoAnotherProj) { targetProject = (0, runtime_lint_utils_1.findProject)(projectGraph, projectRootMappings, imp); } else { targetProject = (0, runtime_lint_utils_1.getTargetProjectBasedOnRelativeImport)(imp, projectPath, projectGraph, projectRootMappings, sourceFilePath); } if ((targetProject && sourceProject !== targetProject) || isAbsoluteImportIntoAnotherProj) { context.report({ node, messageId: 'noRelativeOrAbsoluteImportsAcrossLibraries', fix(fixer) { if (targetProject) { const indexTsPaths = (0, ast_utils_1.getBarrelEntryPointProjectNode)(targetProject); if (indexTsPaths && indexTsPaths.length > 0) { const specifiers = node.specifiers; if (!specifiers || specifiers.length === 0) { return; } const imports = specifiers .filter((s) => s.type === 'ImportSpecifier') .map((s) => s.imported.name); // process each potential entry point and try to find the imports const importsToRemap = []; for (const entryPointPath of indexTsPaths) { for (const importMember of imports) { const importPath = (0, ast_utils_1.getRelativeImportPath)(importMember, (0, path_1.join)(devkit_1.workspaceRoot, entryPointPath.path)); // we cannot remap, so leave it as is if (importPath) { importsToRemap.push({ member: importMember, importPath: entryPointPath.importScope, }); } } } const adjustedRelativeImports = (0, runtime_lint_utils_1.groupImports)(importsToRemap); if (adjustedRelativeImports !== '') { return fixer.replaceTextRange(node.range, adjustedRelativeImports); } } } }, }); return; } targetProject = targetProject || (0, runtime_lint_utils_1.findProjectUsingImport)(projectGraph, targetProjectLocator, sourceFilePath, imp); if (!targetProject) { // non-project imports cannot use relative or absolute paths if ((0, fileutils_1.isRelativePath)(imp) || imp.startsWith('/')) { context.report({ node, messageId: 'noRelativeOrAbsoluteExternals', }); } else if (banTransitiveDependencies && !(0, internal_1.isBuiltinModuleImport)(imp)) { /** * At this point, the target project could not be found in the project graph, and it is not a relative or absolute import. * Therefore it could be an external/npm package dependency (but not a node built in module) which has not yet been installed * in the workspace. * * If the banTransitiveDependencies option is enabled, we should flag this case as an error because the user is trying to * depend on something which is not explicitly defined/resolvable on disk for their project. */ context.report({ node, messageId: 'noTransitiveDependencies', }); } // If target is not found (including node internals) we bail early return; } // we only allow relative paths within the same project // and if it's not a secondary entrypoint in an angular lib if (sourceProject === targetProject && !(0, graph_utils_1.circularPathHasPair)([sourceProject, targetProject], expandedIgnoreCircularDependencies)) { if (!allowCircularSelfDependency && !(0, fileutils_1.isRelativePath)(imp) && !(0, runtime_lint_utils_1.belongsToDifferentNgEntryPoint)(imp, sourceFilePath, sourceProject.data.root)) { context.report({ node, messageId: 'noSelfCircularDependencies', data: { imp, }, fix(fixer) { // imp has form of @myorg/someproject/some/path const indexTsPaths = (0, ast_utils_1.getBarrelEntryPointByImportScope)(imp); if (indexTsPaths.length > 0) { const specifiers = node.specifiers; if (!specifiers || specifiers.length === 0) { return; } // imported JS functions to remap const imports = specifiers .filter((s) => s.type === 'ImportSpecifier') .map((s) => s.imported.name); // process each potential entry point and try to find the imports const importsToRemap = []; for (const entryPointPath of indexTsPaths) { for (const importMember of imports) { const importPath = (0, ast_utils_1.getRelativeImportPath)(importMember, (0, path_1.join)(devkit_1.workspaceRoot, entryPointPath)); if (importPath) { // resolve the import path const relativePath = (0, path_1.relative)((0, path_1.dirname)(fileName), (0, path_1.dirname)(importPath)); // the string we receive from elsewhere might not have a leading './' here despite still being a relative path // we'd like to ensure it's a normalized relative form starting from ./ or ../ const ensureRelativeForm = (path) => path.startsWith('./') || path.startsWith('../') ? path : `./${path}`; // if the string is empty, it's the current file const importPathResolved = relativePath === '' ? `./${(0, path_1.basename)(importPath)}` : ensureRelativeForm((0, devkit_1.joinPathFragments)(relativePath, (0, path_1.basename)(importPath))); importsToRemap.push({ member: importMember, // remove .ts or .tsx from the end of the file path importPath: importPathResolved.replace(/.tsx?$/, ''), }); } } } const adjustedRelativeImports = (0, runtime_lint_utils_1.groupImports)(importsToRemap); if (adjustedRelativeImports !== '') { return fixer.replaceTextRange(node.range, adjustedRelativeImports); } } }, }); } return; } // project => npm package if (targetProject.type === 'npm') { if (banTransitiveDependencies && !(0, runtime_lint_utils_1.isDirectDependency)(sourceProject, targetProject)) { context.report({ node, messageId: 'noTransitiveDependencies', }); } const constraint = (0, runtime_lint_utils_1.hasBannedImport)(sourceProject, targetProject, depConstraints, imp); if (constraint) { context.report({ node, messageId: 'bannedExternalImportsViolation', data: { sourceTag: (0, runtime_lint_utils_1.isComboDepConstraint)(constraint) ? constraint.allSourceTags.join('" and "') : constraint.sourceTag, imp, }, }); } return; } if (!(0, project_graph_1.isProjectGraphProjectNode)(targetProject)) { return; } targetProject = targetProject; // check constraints between libs and apps // check for circular dependency const circularPath = (0, graph_utils_1.checkCircularPath)(projectGraph, sourceProject, targetProject); if (circularPath.length !== 0 && !(0, graph_utils_1.circularPathHasPair)(circularPath, expandedIgnoreCircularDependencies)) { const circularFilePath = (0, graph_utils_1.findFilesInCircularPath)(projectFileMap, circularPath); // spacer text used for indirect dependencies when printing one line per file. // without this, we can end up with a very long line that does not display well in the terminal. const spacer = ' '; context.report({ node, messageId: 'noCircularDependencies', data: { sourceProjectName: sourceProject.name, targetProjectName: targetProject.name, path: circularPath.reduce((acc, v) => `${acc} -> ${v.name}`, sourceProject.name), filePaths: circularFilePath .map((files) => files.length > 1 ? `[${files .map((f) => `\n${spacer}${spacer}${f}`) .join(',')}\n${spacer}]` : files[0]) .reduce((acc, files) => `${acc}\n- ${files}`, `- ${sourceFilePath}`), }, }); return; } // cannot import apps if (targetProject.type === 'app' && !(0, runtime_lint_utils_1.appIsMFERemote)(targetProject)) { context.report({ node, messageId: 'noImportsOfApps', }); return; } // cannot import e2e projects if (targetProject.type === 'e2e') { context.report({ node, messageId: 'noImportsOfE2e', }); return; } // buildable-lib is not allowed to import non-buildable-lib if (enforceBuildableLibDependency === true && sourceProject.type === 'lib' && targetProject.type === 'lib') { if ((0, runtime_lint_utils_1.hasBuildExecutor)(sourceProject, buildTargets) && !(0, runtime_lint_utils_1.hasBuildExecutor)(targetProject, buildTargets)) { context.report({ node, messageId: 'noImportOfNonBuildableLibraries', }); return; } } // if we import a library using loadChildren, we should not import it using es6imports if (!checkDynamicDependenciesExceptions.some((a) => (0, runtime_lint_utils_1.matchImportWithWildcard)(a, imp)) && (0, runtime_lint_utils_1.hasStaticImportOfDynamicResource)(node, projectGraph, sourceProject.name, targetProject.name)) { const filesWithLazyImports = (0, graph_utils_1.findFilesWithDynamicImports)(projectFileMap, sourceProject.name, targetProject.name); context.report({ data: { filePaths: filesWithLazyImports .map(({ file }) => `- ${file}`) .join('\n'), targetProjectName: targetProject.name, }, node, messageId: 'noImportsOfLazyLoadedLibraries', }); return; } // check that dependency constraints are satisfied if (depConstraints.length > 0) { const constraints = (0, runtime_lint_utils_1.findConstraintsFor)(depConstraints, sourceProject); // when no constrains found => error. Force the user to provision them. if (constraints.length === 0) { context.report({ node, messageId: 'projectWithoutTagsCannotHaveDependencies', }); return; } const transitiveExternalDeps = checkNestedExternalImports ? (0, runtime_lint_utils_1.findTransitiveExternalDependencies)(projectGraph, targetProject) : []; for (let constraint of constraints) { if (constraint.onlyDependOnLibsWithTags && constraint.onlyDependOnLibsWithTags.length && (0, runtime_lint_utils_1.hasNoneOfTheseTags)(targetProject, constraint.onlyDependOnLibsWithTags)) { context.report({ node, messageId: 'onlyTagsConstraintViolation', data: { sourceTag: (0, runtime_lint_utils_1.isComboDepConstraint)(constraint) ? constraint.allSourceTags.join('" and "') : constraint.sourceTag, tags: (0, runtime_lint_utils_1.stringifyTags)(constraint.onlyDependOnLibsWithTags), }, }); return; } if (constraint.onlyDependOnLibsWithTags && constraint.onlyDependOnLibsWithTags.length === 0 && targetProject.data.tags.length !== 0) { context.report({ node, messageId: 'emptyOnlyTagsConstraintViolation', data: { sourceTag: (0, runtime_lint_utils_1.isComboDepConstraint)(constraint) ? constraint.allSourceTags.join('" and "') : constraint.sourceTag, }, }); return; } if (constraint.notDependOnLibsWithTags && constraint.notDependOnLibsWithTags.length) { const projectPaths = (0, runtime_lint_utils_1.findDependenciesWithTags)(targetProject, constraint.notDependOnLibsWithTags, projectGraph); if (projectPaths.length > 0) { context.report({ node, messageId: 'notTagsConstraintViolation', data: { sourceTag: (0, runtime_lint_utils_1.isComboDepConstraint)(constraint) ? constraint.allSourceTags.join('" and "') : constraint.sourceTag, tags: (0, runtime_lint_utils_1.stringifyTags)(constraint.notDependOnLibsWithTags), projects: projectPaths .map((projectPath) => `- ${projectPath.map((p) => p.name).join(' -> ')}`) .join('\n'), }, }); return; } } if (checkNestedExternalImports && constraint.bannedExternalImports && constraint.bannedExternalImports.length) { const matches = (0, runtime_lint_utils_1.hasBannedDependencies)(transitiveExternalDeps, projectGraph, constraint, imp); if (matches.length > 0) { matches.forEach(([target, violatingSource, constraint]) => { context.report({ node, messageId: 'nestedBannedExternalImportsViolation', data: { sourceTag: (0, runtime_lint_utils_1.isComboDepConstraint)(constraint) ? constraint.allSourceTags.join('" and "') : constraint.sourceTag, childProjectName: violatingSource.name, imp, }, }); }); return; } } } } } return { ImportDeclaration(node) { run(node); }, ImportExpression(node) { run(node); }, ExportAllDeclaration(node) { run(node); }, ExportNamedDeclaration(node) { run(node); }, }; }, });