eslint-plugin-file-export-name
Version:
ESLint plugin to enforce matching file names with default export names.
133 lines (119 loc) • 4.48 kB
JavaScript
const path = require("path");
const { convertToCase, matchPattern } = require("./utils.cjs");
module.exports = {
rules: {
"match-file-export-name": {
meta: {
type: "suggestion",
docs: {
description: "Ensure default export name matches the file name",
category: "Best Practices",
recommended: false,
},
schema: [{
oneOf: [
{
type: "object",
properties: {
cases: {
type: "array",
items: { type: "string", enum: ["pascal", "camel", "kebab", "snake"] },
uniqueItems: true,
},
pattern: { type: "string" },
ignore: { type: "array", items: { type: "string" } },
extensions: {
type: "array",
items: { type: "string" },
uniqueItems: true,
},
},
additionalProperties: false,
},
{
type: "array",
items: {
type: "object",
properties: {
cases: {
type: "array",
items: { type: "string", enum: ["pascal", "camel", "kebab", "snake"] },
uniqueItems: true,
},
pattern: { type: "string" },
ignore: { type: "array", items: { type: "string" } },
extensions: {
type: "array",
items: { type: "string" },
uniqueItems: true,
},
},
additionalProperties: false,
},
}
]
}],
messages: {
mismatch:
"Default export '{{exportName}}' does not match the file name '{{fileName}}' in any of the allowed cases or patterns.",
},
},
create(context) {
const defaultConfig = {
cases: ["pascal", "camel"],
extensions: [".jsx", ".tsx", ".js", ".ts"],
ignore: ["<text>", "<input>"],
pattern: null
};
const options = context.options[0] ?? {};
const configurations = Array.isArray(options) ? options : [options];
return {
Program(node) {
const filePath = context.getFilename();
const fileExt = path.extname(filePath);
const fileNameWithExt = path.basename(filePath);
const fileName = fileNameWithExt.replace(fileExt, "");
const matchingConfig = configurations.length === 0 ? defaultConfig :
configurations.find(config =>
[...defaultConfig.extensions, ...(config.extensions || [])].includes(fileExt)
) || defaultConfig;
const allowedCases = matchingConfig.cases?.length ? matchingConfig.cases : defaultConfig.cases;
const pattern = matchingConfig.pattern ? new RegExp(matchingConfig.pattern) : defaultConfig.pattern;
const ignoreFiles = [...defaultConfig.ignore, ...(matchingConfig.ignore || [])];
const extensions = [...defaultConfig.extensions, ...(matchingConfig.extensions || [])];
if (
fileName === "index" ||
ignoreFiles.some((pattern) => new RegExp(pattern).test(filePath))
) {
return;
}
let defaultExportName = null;
node.body.forEach((statement) => {
if (
statement.type === "ExportDefaultDeclaration" &&
statement.declaration.type === "Identifier"
) {
defaultExportName = statement.declaration.name;
}
});
if (!defaultExportName) return;
const isCaseMatch =
allowedCases.length > 0 &&
allowedCases.some((caseType) => convertToCase(fileName, caseType) === defaultExportName);
const isPatternMatch = pattern && matchPattern(pattern, defaultExportName);
if (!isCaseMatch && !isPatternMatch) {
context.report({
node,
messageId: "mismatch",
data: {
exportName: defaultExportName,
fileName,
},
});
}
},
};
},
},
},
};