UNPKG

eslint-plugin-canonical

Version:
220 lines (219 loc) 8.68 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const node_fs_1 = require("node:fs"); const node_path_1 = require("node:path"); const resolve_1 = __importDefault(require("eslint-module-utils/resolve")); const utilities_1 = require("../utilities"); const findDirectory_1 = require("../utilities/findDirectory"); const readPackageJson_1 = require("../utilities/readPackageJson"); const findRootPath_1 = require("../utilities/findRootPath"); const extensions = ['.js', '.ts', '.tsx']; const defaultOptions = { // You may want to disable ignorePackages because there can be too many false-positives // when attempting to identify if a package import requires .js extension or not. // // * We need to consider that the resolved path can be @types/. // * We need to consider that the package.json might have package.json#exports rules. ignorePackages: false, }; const isExistingFile = (fileName) => { return (0, node_fs_1.existsSync)(fileName) && (0, node_fs_1.lstatSync)(fileName).isFile(); }; const fixRelativeImport = (fixer, node, fileName, overrideExtension = true) => { if (!node.source) { throw new Error('Node has no source'); } const importPath = (0, node_path_1.resolve)((0, node_path_1.dirname)(fileName), node.source.value); for (const extension of extensions) { if (isExistingFile(importPath + extension)) { return fixer.replaceTextRange(node.source.range, `'${node.source.value + (overrideExtension ? '.js' : extension)}'`); } } for (const extension of extensions) { if (isExistingFile((0, node_path_1.resolve)(importPath, 'index') + extension)) { return fixer.replaceTextRange(node.source.range, `'${node.source.value + `${node_path_1.sep}index` + (overrideExtension ? '.js' : extension)}'`); } } return null; }; const fixPathImport = (fixer, node, fileName, resolvedImportPath, overrideExtension = true) => { if (!node.source) { throw new Error('Node has no source'); } const importPath = node.source.value; const lastSegment = importPath.split('/').pop(); for (const extension of extensions) { if (resolvedImportPath.endsWith(lastSegment + extension)) { return fixer.replaceTextRange(node.source.range, `'${node.source.value + (overrideExtension ? '.js' : extension)}'`); } } for (const extension of extensions) { if (resolvedImportPath.endsWith(lastSegment + '/index' + extension)) { return fixer.replaceTextRange(node.source.range, `'${node.source.value + '/index' + (overrideExtension ? '.js' : extension)}'`); } } return null; }; const endsWith = (subject, needles) => { return needles.some((needle) => { return subject.endsWith(needle); }); }; const createTSConfigFinder = () => { const cache = {}; return (fileName) => { if (cache[fileName] !== undefined) { return cache[fileName]; } let tsconfig; try { tsconfig = JSON.parse((0, node_fs_1.readFileSync)(fileName, 'utf8')); } catch (_a) { throw new Error(`Failed to parse TSConfig ${fileName}`); } cache[fileName] = tsconfig; return tsconfig; }; }; const findTSConfig = createTSConfigFinder(); const handleRelativePath = (context, node, importPath) => { var _a; if (!importPath.startsWith('.')) { return false; } const filename = (_a = context.filename) !== null && _a !== void 0 ? _a : context.getFilename(); // This would mean that the import path resolves to a non-JavaScript file, e.g. CSS import. if (isExistingFile((0, node_path_1.resolve)((0, node_path_1.dirname)(filename), importPath))) { return true; } context.report({ fix(fixer) { return fixRelativeImport(fixer, node, filename); }, messageId: 'extensionMissing', node, }); return true; }; const normalizePackageName = (packageName) => { if (packageName.startsWith('@types/')) { // @types/testing-library__jest-dom -> @testing-library/jest-dom return '@' + packageName.replace('@types/', '').replace('__', '/'); } return packageName; }; const handleAliasPath = (context, node, importPath, ignorePackages) => { var _a, _b, _c, _d; // @ts-expect-error we know this setting exists const project = ((_c = (_b = (_a = context.settings['import/resolver']) === null || _a === void 0 ? void 0 : _a.typescript) === null || _b === void 0 ? void 0 : _b.project) !== null && _c !== void 0 ? _c : null); if (typeof project !== 'string') { return false; } const tsconfig = findTSConfig(project); if (!tsconfig) { return false; } let resolvedImportPath; const filename = (_d = context.filename) !== null && _d !== void 0 ? _d : context.getFilename(); try { // There are odd cases where using `resolveImport` resolves to a unexpected file, e.g. // `import turbowatch from 'turbowatch';` inside of `turbowatch.ts` resolves to `turbowatch.js`. // Using `require.resolve` with the `paths` option resolves to the correct file in those instances. resolvedImportPath = require.resolve(importPath, { paths: [filename], }); } catch (_e) { // no-op } if (!resolvedImportPath) { // @ts-expect-error TODO check what's going on here resolvedImportPath = (0, resolve_1.default)(importPath, context); } if (!resolvedImportPath) { return false; } // This would mean that the import path resolves to a non-JavaScript file, e.g. CSS import. if (!endsWith(resolvedImportPath, extensions)) { return true; } const targetPackageJsonPath = (0, findDirectory_1.findDirectory)(resolvedImportPath, 'package.json', (0, findRootPath_1.findRootPath)(resolvedImportPath)); if (targetPackageJsonPath) { if (ignorePackages) { const currentPackageJsonPath = (0, findDirectory_1.findDirectory)(filename, 'package.json', (0, findRootPath_1.findRootPath)(resolvedImportPath)); if (currentPackageJsonPath && currentPackageJsonPath !== targetPackageJsonPath) { return false; } } const packageJson = (0, readPackageJson_1.readPackageJson)((0, node_path_1.resolve)(targetPackageJsonPath, 'package.json')); if (packageJson.name && normalizePackageName(packageJson.name) === importPath) { return false; } } context.report({ fix(fixer) { return fixPathImport(fixer, node, filename, resolvedImportPath); }, messageId: 'extensionMissing', node, }); return true; }; exports.default = (0, utilities_1.createRule)({ create: (context, [options]) => { var _a; const ignorePackages = (_a = options.ignorePackages) !== null && _a !== void 0 ? _a : defaultOptions.ignorePackages; const rule = (node) => { if (!node.source) { // export { foo }; // export const foo = () => {}; return; } const importPath = node.source.value; if (importPath.includes('?')) { // import { foo } from './foo.svg?url'; return; } const importPathHasExtension = endsWith(importPath, extensions); if (importPathHasExtension) { return; } void (handleRelativePath(context, node, importPath) || handleAliasPath(context, node, importPath, ignorePackages)); }; return { ExportAllDeclaration: rule, ExportNamedDeclaration: rule, ImportDeclaration: rule, }; }, defaultOptions: [defaultOptions], meta: { docs: { description: 'Require file extension in import and export statements', }, fixable: 'code', messages: { extensionMissing: 'Must include file extension', }, schema: [ { additionalProperties: false, properties: { ignorePackages: { type: 'boolean', }, }, type: 'object', }, ], type: 'layout', }, name: 'require-extension', });