eslint-plugin-html-compat
Version:
ESLint plugin to check HTML element and attribute compatibility using browserslist and @mdn/browser-compat-data
184 lines • 8.48 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const browserslist_1 = require("../utils/browserslist");
const compatibility_1 = require("../utils/compatibility");
const rule = {
meta: {
type: "problem",
docs: {
description: "Check HTML element and attribute compatibility with target browsers",
category: "Possible Errors",
recommended: true,
},
fixable: undefined,
schema: [
{
type: "object",
properties: {
browserslistConfig: {
oneOf: [
{ type: "string" },
{ type: "array", items: { type: "string" } },
],
},
ignoreBrowsers: {
type: "array",
items: { type: "string" },
description: "List of browsers to ignore in compatibility checks (e.g., ['ie 11', 'opera_mini'])",
},
},
additionalProperties: false,
},
],
messages: {
incompatibleElement: 'HTML element "{{element}}" is not supported in: {{browsers}}.\n See {{mdnUrl}} for details.',
incompatibleAttribute: 'HTML attribute "{{attribute}}" on element "{{element}}" is not supported in: {{browsers}}.\n See {{mdnUrl}} for details.',
},
},
create(context) {
const options = context.options[0] || {};
const targetBrowsers = options.browserslistConfig
? (0, browserslist_1.getSupportedBrowsers)(options.browserslistConfig)
: (0, browserslist_1.parseBrowserslistConfig)();
function checkJSXElement(node) {
const elementName = node.openingElement?.name?.name || node.name?.name;
if (!elementName)
return;
const elementResult = (0, compatibility_1.checkHtmlElementCompatibility)(elementName, targetBrowsers, options.ignoreBrowsers);
if (!elementResult.isSupported) {
context.report({
node,
messageId: "incompatibleElement",
data: {
element: elementName,
browsers: elementResult.unsupportedBrowsers.join(", "),
mdnUrl: elementResult.mdnUrl || "",
},
});
}
const attributes = node.openingElement?.attributes || node.attributes;
if (attributes) {
for (const attr of attributes) {
if (attr.type === "JSXAttribute" && attr.name?.name) {
const attrName = attr.name.name.toLowerCase();
const jsxOnlyAttributes = [
"classname",
"htmlfor",
"defaultvalue",
"defaultchecked",
];
if (jsxOnlyAttributes.includes(attrName)) {
continue;
}
const attrResult = (0, compatibility_1.checkHtmlAttributeCompatibility)(elementName, attrName, targetBrowsers, options.ignoreBrowsers);
if (!attrResult.isSupported) {
context.report({
node: attr,
messageId: "incompatibleAttribute",
data: {
attribute: attrName,
element: elementName,
browsers: attrResult.unsupportedBrowsers.join(", "),
mdnUrl: attrResult.mdnUrl || "",
},
});
}
}
}
}
}
function checkHTMLElement(node) {
const elementName = node.tagName?.toLowerCase() || node.name?.toLowerCase();
if (!elementName)
return;
const elementResult = (0, compatibility_1.checkHtmlElementCompatibility)(elementName, targetBrowsers, options.ignoreBrowsers);
if (!elementResult.isSupported) {
context.report({
node,
messageId: "incompatibleElement",
data: {
element: elementName,
browsers: elementResult.unsupportedBrowsers.join(", "),
mdnUrl: elementResult.mdnUrl || "",
},
});
}
// Regular HTML nodes use attributes
const attributes = node.attributes;
if (attributes) {
for (const attr of attributes) {
const attrName = attr.name?.toLowerCase();
if (attrName) {
const attrResult = (0, compatibility_1.checkHtmlAttributeCompatibility)(elementName, attrName, targetBrowsers, options.ignoreBrowsers);
if (!attrResult.isSupported) {
context.report({
node: attr,
messageId: "incompatibleAttribute",
data: {
attribute: attrName,
element: elementName,
browsers: attrResult.unsupportedBrowsers.join(", "),
mdnUrl: attrResult.mdnUrl || "",
},
});
}
}
}
}
}
function checkVueElement(node) {
// VElement has different structure: node.name for element name
const elementName = node.name?.toLowerCase();
if (!elementName)
return;
const elementResult = (0, compatibility_1.checkHtmlElementCompatibility)(elementName, targetBrowsers, options.ignoreBrowsers);
if (!elementResult.isSupported) {
context.report({
node,
messageId: "incompatibleElement",
data: {
element: elementName,
browsers: elementResult.unsupportedBrowsers.join(", "),
mdnUrl: elementResult.mdnUrl || "",
},
});
}
// VElement attributes are in startTag.attributes
const attributes = node.startTag?.attributes;
if (attributes) {
for (const attr of attributes) {
// VAttribute has key.name for attribute name
const attrName = attr.key?.name?.toLowerCase();
if (attrName) {
// Skip Vue-specific directives (v-, @, :)
if (attrName.startsWith('v-') || attrName.startsWith('@') || attrName.startsWith(':')) {
continue;
}
const attrResult = (0, compatibility_1.checkHtmlAttributeCompatibility)(elementName, attrName, targetBrowsers, options.ignoreBrowsers);
if (!attrResult.isSupported) {
context.report({
node: attr,
messageId: "incompatibleAttribute",
data: {
attribute: attrName,
element: elementName,
browsers: attrResult.unsupportedBrowsers.join(", "),
mdnUrl: attrResult.mdnUrl || "",
},
});
}
}
}
}
}
return {
JSXElement: checkJSXElement,
HTMLElement: checkHTMLElement,
Element: checkHTMLElement,
VElement: checkVueElement,
SvelteElement: checkHTMLElement,
};
},
};
exports.default = rule;
//# sourceMappingURL=html-compat.js.map