eslint-rules/no-relative-cross-package-imports.js (107 lines of code) (raw):

/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Disallows relative imports between specified monorepo packages. */ 'use strict'; import path from 'node:path'; import fs from 'node:fs'; /** * Finds the package name by searching for the nearest `package.json` file * in the directory hierarchy, starting from the given file's directory * and moving upwards until the specified root directory is reached. * It reads the `package.json` and extracts the `name` property. * * @requires module:path Node.js path module * @requires module:fs Node.js fs module * * @param {string} filePath - The path (absolute or relative) to a file within the potential package structure. * The search starts from the directory containing this file. * @param {string} root - The absolute path to the root directory of the project/monorepo. * The upward search stops when this directory is reached. * @returns {string | undefined | null} The value of the `name` field from the first `package.json` found. * Returns `undefined` if the `name` field doesn't exist in the found `package.json`. * Returns `null` if no `package.json` is found before reaching the `root` directory. * @throws {Error} Can throw an error if `fs.readFileSync` fails (e.g., permissions) or if `JSON.parse` fails on invalid JSON content. */ function findPackageName(filePath, root) { let currentDir = path.dirname(path.resolve(filePath)); while (currentDir !== root) { const parentDir = path.dirname(currentDir); const packageJsonPath = path.join(currentDir, 'package.json'); if (fs.existsSync(packageJsonPath)) { const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); return pkg.name; } // Move up one level currentDir = parentDir; // Safety break if we somehow reached the root directly in the loop condition (less likely with path.resolve) if (path.dirname(currentDir) === currentDir) break; } return null; // Not found within the expected structure } export default { meta: { type: 'problem', docs: { description: 'Disallow relative imports between packages.', category: 'Best Practices', recommended: 'error', }, fixable: 'code', schema: [ { type: 'object', properties: { root: { type: 'string', description: 'Absolute path to the root of all relevant packages to consider.', }, }, required: ['root'], additionalProperties: false, }, ], messages: { noRelativePathsForCrossPackageImport: "Relative import '{{importedPath}}' crosses package boundary from '{{importingPackage}}' to '{{importedPackage}}'. Use a direct package import ('{{importedPackage}}') instead.", relativeImportIsInvalidPackage: "Relative import '{{importedPath}}' does not reference a valid package. All source must be in a package directory.", }, }, create(context) { const options = context.options[0] || {}; const allPackagesRoot = options.root; const currentFilePath = context.filename; if ( !currentFilePath || currentFilePath === '<input>' || currentFilePath === '<text>' ) { // Skip if filename is not available (e.g., linting raw text) return {}; } const currentPackage = findPackageName(currentFilePath, allPackagesRoot); // If the current file isn't inside a package structure, don't apply the rule if (!currentPackage) { return {}; } return { ImportDeclaration(node) { const importingPackage = currentPackage; const importedPath = node.source.value; // Only interested in relative paths if ( !importedPath || typeof importedPath !== 'string' || !importedPath.startsWith('.') ) { return; } // Resolve the absolute path of the imported module const absoluteImportPath = path.resolve( path.dirname(currentFilePath), importedPath, ); // Find the package information for the imported file const importedPackage = findPackageName( absoluteImportPath, allPackagesRoot, ); // If the imported file isn't in a recognized package, report issue if (!importedPackage) { context.report({ node: node.source, messageId: 'relativeImportIsInvalidPackage', data: { importedPath: importedPath }, }); return; } // The core check: Are the source and target packages different? if (currentPackage !== importedPackage) { // We found a relative import crossing package boundaries context.report({ node: node.source, // Report the error on the source string literal messageId: 'noRelativePathsForCrossPackageImport', data: { importedPath, importedPackage, importingPackage, }, fix(fixer) { return fixer.replaceText(node.source, `'${importedPackage}'`); }, }); } }, }; }, };