197 lines
8.7 KiB
JavaScript
197 lines
8.7 KiB
JavaScript
import { TSESTree } from '@typescript-eslint/utils';
|
|
import { dirname } from 'node:path';
|
|
import { isNodeTypeClass, isCredentialTypeClass, findClassProperty, findObjectProperty, getStringLiteralValue, validateIconPath, findSimilarSvgFiles, isFileType, createRule, } from '../utils/index.js';
|
|
const messages = {
|
|
iconFileNotFound: 'Icon file "{{ iconPath }}" does not exist',
|
|
iconNotSvg: 'Icon file "{{ iconPath }}" must be an SVG file (end with .svg)',
|
|
lightDarkSame: 'Light and dark icons cannot be the same file. Both point to "{{ iconPath }}"',
|
|
invalidIconPath: 'Icon path "{{ iconPath }}" must use file: protocol and be a string',
|
|
missingIcon: 'Node/Credential class must have an icon property defined',
|
|
addPlaceholder: 'Add icon property with placeholder',
|
|
addFileProtocol: "Add 'file:' protocol to icon path",
|
|
changeExtension: "Change icon extension to '.svg'",
|
|
similarIcon: "Use existing icon '{{ suggestedName }}'",
|
|
};
|
|
export const IconValidationRule = createRule({
|
|
name: 'icon-validation',
|
|
meta: {
|
|
type: 'problem',
|
|
docs: {
|
|
description: 'Validate node and credential icon files exist, are SVG format, and light/dark icons are different',
|
|
},
|
|
messages,
|
|
schema: [],
|
|
hasSuggestions: true,
|
|
},
|
|
defaultOptions: [],
|
|
create(context) {
|
|
if (!isFileType(context.filename, '.node.ts') &&
|
|
!isFileType(context.filename, '.credentials.ts')) {
|
|
return {};
|
|
}
|
|
const validateIcon = (iconPath, node) => {
|
|
if (!iconPath) {
|
|
context.report({
|
|
node,
|
|
messageId: 'invalidIconPath',
|
|
data: { iconPath: iconPath ?? '' },
|
|
});
|
|
return false;
|
|
}
|
|
const currentDir = dirname(context.filename);
|
|
const validation = validateIconPath(iconPath, currentDir);
|
|
if (!validation.isFile) {
|
|
const suggestions = [];
|
|
if (!iconPath.startsWith('file:')) {
|
|
suggestions.push({
|
|
messageId: 'addFileProtocol',
|
|
fix(fixer) {
|
|
return fixer.replaceText(node, `"file:${iconPath}"`);
|
|
},
|
|
});
|
|
}
|
|
context.report({
|
|
node,
|
|
messageId: 'invalidIconPath',
|
|
data: { iconPath },
|
|
suggest: suggestions,
|
|
});
|
|
return false;
|
|
}
|
|
if (!validation.isSvg) {
|
|
const relativePath = iconPath.replace(/^file:/, '');
|
|
const suggestions = [];
|
|
const pathWithoutExt = relativePath.replace(/\.[^/.]+$/, '');
|
|
const svgPath = `${pathWithoutExt}.svg`;
|
|
suggestions.push({
|
|
messageId: 'changeExtension',
|
|
fix(fixer) {
|
|
return fixer.replaceText(node, `"file:${svgPath}"`);
|
|
},
|
|
});
|
|
context.report({
|
|
node,
|
|
messageId: 'iconNotSvg',
|
|
data: { iconPath: relativePath },
|
|
suggest: suggestions,
|
|
});
|
|
return false;
|
|
}
|
|
if (!validation.exists) {
|
|
const relativePath = iconPath.replace(/^file:/, '');
|
|
const suggestions = [];
|
|
// Find similar SVG files in the same directory
|
|
const similarFiles = findSimilarSvgFiles(relativePath, currentDir);
|
|
for (const similarFile of similarFiles) {
|
|
suggestions.push({
|
|
messageId: 'similarIcon',
|
|
data: { suggestedName: similarFile },
|
|
fix(fixer) {
|
|
return fixer.replaceText(node, `"file:${similarFile}"`);
|
|
},
|
|
});
|
|
}
|
|
context.report({
|
|
node,
|
|
messageId: 'iconFileNotFound',
|
|
data: { iconPath: relativePath },
|
|
suggest: suggestions,
|
|
});
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
const validateIconValue = (iconValue) => {
|
|
if (iconValue.type === TSESTree.AST_NODE_TYPES.Literal) {
|
|
const iconPath = getStringLiteralValue(iconValue);
|
|
validateIcon(iconPath, iconValue);
|
|
}
|
|
else if (iconValue.type === TSESTree.AST_NODE_TYPES.ObjectExpression) {
|
|
const lightProperty = findObjectProperty(iconValue, 'light');
|
|
const darkProperty = findObjectProperty(iconValue, 'dark');
|
|
const lightPath = lightProperty ? getStringLiteralValue(lightProperty.value) : null;
|
|
const darkPath = darkProperty ? getStringLiteralValue(darkProperty.value) : null;
|
|
if (lightProperty) {
|
|
validateIcon(lightPath, lightProperty.value);
|
|
}
|
|
if (darkProperty) {
|
|
validateIcon(darkPath, darkProperty.value);
|
|
}
|
|
if (lightPath && darkPath && lightPath === darkPath && lightProperty) {
|
|
context.report({
|
|
node: lightProperty.value,
|
|
messageId: 'lightDarkSame',
|
|
data: { iconPath: lightPath.replace(/^file:/, '') },
|
|
});
|
|
}
|
|
}
|
|
};
|
|
return {
|
|
ClassDeclaration(node) {
|
|
const isNodeClass = isNodeTypeClass(node);
|
|
const isCredentialClass = isCredentialTypeClass(node);
|
|
if (!isNodeClass && !isCredentialClass) {
|
|
return;
|
|
}
|
|
if (isNodeClass) {
|
|
const descriptionProperty = findClassProperty(node, 'description');
|
|
if (!descriptionProperty?.value ||
|
|
descriptionProperty.value.type !== TSESTree.AST_NODE_TYPES.ObjectExpression) {
|
|
context.report({
|
|
node,
|
|
messageId: 'missingIcon',
|
|
});
|
|
return;
|
|
}
|
|
const descriptionValue = descriptionProperty.value;
|
|
const iconProperty = findObjectProperty(descriptionValue, 'icon');
|
|
if (!iconProperty) {
|
|
const suggestions = [];
|
|
suggestions.push({
|
|
messageId: 'addPlaceholder',
|
|
fix(fixer) {
|
|
const lastProperty = descriptionValue.properties[descriptionValue.properties.length - 1];
|
|
if (lastProperty) {
|
|
return fixer.insertTextAfter(lastProperty, ',\n\t\ticon: "file:./icon.svg"');
|
|
}
|
|
return null;
|
|
},
|
|
});
|
|
context.report({
|
|
node,
|
|
messageId: 'missingIcon',
|
|
suggest: suggestions,
|
|
});
|
|
return;
|
|
}
|
|
validateIconValue(iconProperty.value);
|
|
}
|
|
else if (isCredentialClass) {
|
|
const iconProperty = findClassProperty(node, 'icon');
|
|
if (!iconProperty?.value) {
|
|
const suggestions = [];
|
|
suggestions.push({
|
|
messageId: 'addPlaceholder',
|
|
fix(fixer) {
|
|
const classBody = node.body.body;
|
|
const lastProperty = classBody[classBody.length - 1];
|
|
if (lastProperty) {
|
|
return fixer.insertTextAfter(lastProperty, '\n\n\ticon = "file:./icon.svg";');
|
|
}
|
|
return null;
|
|
},
|
|
});
|
|
context.report({
|
|
node,
|
|
messageId: 'missingIcon',
|
|
suggest: suggestions,
|
|
});
|
|
return;
|
|
}
|
|
validateIconValue(iconProperty.value);
|
|
}
|
|
},
|
|
};
|
|
},
|
|
});
|
|
//# sourceMappingURL=icon-validation.js.map
|