eslint-plugin-canonical
Version:
Canonical linting rules for ESLint.
220 lines (219 loc) • 8.68 kB
JavaScript
;
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',
});