@soleil-se/eslint-config
Version:
ESLint configuration for Sitevision apps and projects.
129 lines (111 loc) • 3.57 kB
JavaScript
/** @import { Linter, Rule } from 'eslint' */
/**
* @typedef ImportNamingOptions
* @property {string[]} packages
*/
/** @param {string} source */
function isPackageImport(source) {
return !source.startsWith('.') && !source.startsWith('/');
}
/** @param {string} source @param {string} packageName */
function matchesPackagePrefix(source, packageName) {
return source === packageName || source.startsWith(`${packageName}/`);
}
/** @param {string} source @param {ImportNamingOptions} settings */
function isIncludedImport(source, settings) {
return settings.packages.some(
(packageName) => matchesPackagePrefix(source, packageName),
);
}
/** @param {string} source @param {ImportNamingOptions} settings */
function getExpectedImportName(source, settings) {
if (!isPackageImport(source) || !isIncludedImport(source, settings)) return undefined;
const moduleName = source.split('/').at(-1);
return moduleName && /^[$A-Z_a-z][$\w]*$/.test(moduleName)
? moduleName
: undefined;
}
/** @type {Rule.RuleModule} */
const matchPackageDefaultImportNameRule = {
meta: {
type: 'suggestion',
fixable: 'code',
schema: [
{
type: 'object',
properties: {
packages: {
type: 'array',
items: {
type: 'string',
},
},
},
additionalProperties: false,
},
],
messages: {
mismatch:
'Default import from {{source}} should be named {{expected}}.',
conflict: [
'Default import from {{source}} should be named {{expected}},',
'but that name is already used in this scope.',
].join(' '),
},
},
create(context) {
const { sourceCode } = context;
/** @type {ImportNamingOptions} */
const settings = {
packages: context.options[0]?.packages ?? [],
};
return {
ImportDeclaration(node) {
if (typeof node.source.value !== 'string') return;
const defaultSpecifier = node.specifiers.find(
(specifier) => specifier.type === 'ImportDefaultSpecifier',
);
if (!defaultSpecifier) return;
const expectedName = getExpectedImportName(node.source.value, settings);
if (!expectedName) return;
if (defaultSpecifier.local.name === expectedName) return;
const [variable] = sourceCode.getDeclaredVariables(defaultSpecifier);
if (!variable) return;
const conflictingVariable = variable.scope.set.get(expectedName);
if (conflictingVariable && conflictingVariable !== variable) {
context.report({
node: defaultSpecifier.local,
messageId: 'conflict',
data: {
expected: expectedName,
source: node.source.value,
},
});
return;
}
context.report({
node: defaultSpecifier.local,
messageId: 'mismatch',
data: {
expected: expectedName,
source: node.source.value,
},
fix(fixer) {
const identifiers = [
defaultSpecifier.local,
...variable.references.map((reference) => reference.identifier),
];
return identifiers.map((identifier) => fixer.replaceText(identifier, expectedName));
},
});
},
};
},
};
/** @type {Linter.Plugin} */
const importNamingPlugin = {
rules: {
'match-package-default-import-name': matchPackageDefaultImportNameRule,
},
};
export default importNamingPlugin;