Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion eslint-plugin/docs/rules/avoid-autoplay.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,15 @@ return (
);
```

This rule is build for [React](https://react.dev/) and JSX.
This rule supports [React](https://react.dev/) (JSX) and Vue SFC templates when using `vue-eslint-parser`.

<template>
Comment thread
neptia marked this conversation as resolved.
<video autoplay></video> <!-- Non-compliant -->
<video preload="none"></video> <!-- Compliant -->
</template>

Vue support requires `vue-eslint-parser` so the rule can access the template AST.
Comment thread
neptia marked this conversation as resolved.
Outdated
Dynamic bindings like `:preload="x"` are not validated by this rule.

## Resources

Expand Down
7 changes: 7 additions & 0 deletions eslint-plugin/docs/rules/avoid-css-animations.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ Limiting the usage of CSS animations helps in creating a more energy-efficient a
<div style={{ border: "1px solid black" }} /> // Compliant
```

<template>
<div style="border: 1px solid black; transition: border 2s ease;"></div> <!-- Non-compliant -->
<div style="border: 1px solid black;"></div> <!-- Compliant -->
</template>

Vue support only checks static style attributes; :style bindings are not validated.

It's important to note that while limiting animations is generally advisable for certain scenarios, there are cases
where animations contribute positively to the user experience and overall design.
In this case they should be limited to the CSS properties `opacity` and `transform` with it's associated
Expand Down
11 changes: 10 additions & 1 deletion eslint-plugin/docs/rules/no-empty-image-src-attribute.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,16 @@ return (
);
```

This rule is build for [React](https://react.dev/) and JSX.
This rule supports React (JSX) and Vue SFC templates when using vue-eslint-parser.

<template>
<img src="" /> <!-- Non-compliant -->
<img /> <!-- Non-compliant -->
<img src="./logo.svg" /> <!-- Compliant -->
</template>

Vue support requires vue-eslint-parser; dynamic bindings like :src are not validated by this rule.


## Resources

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ is supported, the image.webp image will be downloaded; otherwise, image.jpg imag
</picture>
```

<template>
<img src="./assets/cat.jpg" alt="Unoptimized image of a cat" /> <!-- Non-compliant -->
<img src="./assets/cat.webp" alt="Optimized image of a cat" /> <!-- Compliant -->
</template>

Vue support requires vue-eslint-parser; dynamic bindings like :src are not validated by this rule.

Also remember to consider browser compatibility.
Older browsers may not recognize .webp/.avif images and fail to display them.
To address this issue, you can supply multiple formats for the same image.
Expand Down
7 changes: 7 additions & 0 deletions eslint-plugin/docs/rules/prefer-shorthand-css-notations.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ For example, if you only want to set the left margin, you must continue to use `
</div>
```

<template>
<div style="margin-top: 1em; margin-right: 0; margin-bottom: 2em; margin-left: 0.5em;"></div> <!-- Non-compliant -->
<div style="margin: 1em 0 2em 0.5em;"></div> <!-- Compliant -->
</template>

Vue support only checks static style attributes; :style bindings are not validated.

This optimization works for a number of
properties [listed here](https://developer.mozilla.org/en-US/docs/Web/CSS/Shorthand_properties#see_also).

Expand Down
78 changes: 56 additions & 22 deletions eslint-plugin/lib/rules/avoid-autoplay.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,59 @@ module.exports = {
schema: [],
},
create(context) {
const reportAutoplay = (autoplayAttr, preloadAttr, preloadValue, fallback) => {
if (autoplayAttr && preloadValue !== "none") {
context.report({
node: autoplayAttr || preloadAttr,
messageId: "NoAutoplayAndEnforcePreloadNone",
});
return;
}

if (autoplayAttr) {
context.report({
node: autoplayAttr,
messageId: "NoAutoplay",
});
}

if (!preloadAttr || preloadValue !== "none") {
context.report({
node: preloadAttr || fallback,
messageId: "EnforcePreloadNone",
});
}
};

const parserServices =
context.parserServices || context.sourceCode?.parserServices;

const vueTemplateVisitor = parserServices?.defineTemplateBodyVisitor
? parserServices.defineTemplateBodyVisitor({
VElement(node) {
const rawName =
typeof node.name === "string"
? node.name
: node.name?.name || node.rawName;
const name = rawName?.toLowerCase();
if (name !== "video" && name !== "audio") return;

const getAttr = (attrName) =>
node.startTag.attributes.find(
(attr) =>
attr.type === "VAttribute" &&
attr.key?.name?.toLowerCase() === attrName,
);

const autoplayAttr = getAttr("autoplay");
const preloadAttr = getAttr("preload");
const preloadValue = preloadAttr?.value?.value;

reportAutoplay(autoplayAttr, preloadAttr, preloadValue, node);
},
})
: {};

return {
JSXOpeningElement(node) {
if (node.name.name === "video" || node.name.name === "audio") {
Expand All @@ -45,31 +98,12 @@ module.exports = {
const preloadAttr = node.attributes.find(
(attr) => attr.name?.name.toLowerCase() === "preload",
);
if (
autoplayAttr &&
(!preloadAttr || preloadAttr.value.value !== "none")
) {
context.report({
node: autoplayAttr || preloadAttr,
messageId: "NoAutoplayAndEnforcePreloadNone",
});
} else {
if (autoplayAttr) {
context.report({
node: autoplayAttr,
messageId: "NoAutoplay",
});
}
const preloadValue = preloadAttr?.value?.value;

if (!preloadAttr || preloadAttr.value.value !== "none") {
context.report({
node: preloadAttr || node,
messageId: "EnforcePreloadNone",
});
}
}
reportAutoplay(autoplayAttr, preloadAttr, preloadValue, node);
}
},
...vueTemplateVisitor,
};
},
};
31 changes: 29 additions & 2 deletions eslint-plugin/lib/rules/avoid-css-animations.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,42 @@ module.exports = {
},
schema: [],
},
create(context) {
create(context) {
const forbiddenProperties = ["transition", "animation"];

const parserServices =
context.parserServices || context.sourceCode?.parserServices;

const vueTemplateVisitor = parserServices?.defineTemplateBodyVisitor
? parserServices.defineTemplateBodyVisitor({
VElement(node) {
const styleAttr = node.startTag.attributes.find(
(attr) => attr.type === "VAttribute" && attr.key?.name === "style",
);
const styleValue = styleAttr?.value?.value;
if (!styleValue) return;

const matched = forbiddenProperties.find((prop) =>
new RegExp(`(^|;)\\s*${prop}\\s*:`, "i").test(styleValue),
);
if (!matched) return;

context.report({
node: styleAttr,
messageId: "AvoidCSSAnimations",
data: { attribute: matched },
});
},
})
: {};

return {
JSXOpeningElement(node) {
const styleAttribute = node.attributes.find(
(attribute) => attribute.name?.name === "style",
);

if (styleAttribute?.value.expression?.properties) {
// To prevent (for example) <div style={{ animate: 'width 2s' }}>
Comment thread
neptia marked this conversation as resolved.
const property = styleAttribute.value.expression.properties.find(
(prop) =>
prop.key != null &&
Expand All @@ -60,6 +86,7 @@ module.exports = {
}
}
},
...vueTemplateVisitor,
};
},
};
31 changes: 29 additions & 2 deletions eslint-plugin/lib/rules/no-empty-image-src-attribute.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,27 +34,54 @@ module.exports = {
schema: [],
},
create(context) {
const parserServices =
context.parserServices || context.sourceCode?.parserServices;

const vueTemplateVisitor = parserServices?.defineTemplateBodyVisitor
? parserServices.defineTemplateBodyVisitor({
VElement(node) {
const rawName =
typeof node.name === "string"
? node.name
: node.name?.name || node.rawName;
const name = rawName?.toLowerCase();
if (name !== "img") return;

const srcAttr = node.startTag.attributes.find(
(attr) => attr.type === "VAttribute" && attr.key?.name === "src",
);
const srcValue = srcAttr?.value?.value;

if (srcValue === "" || !srcAttr) {
context.report({
node: srcAttr || node,
messageId: "SpecifySrcAttribute",
});
}
},
})
: {};

return {
JSXOpeningElement(node) {
if (node.name.name === "img") {
const srcValue = node.attributes.find(
(attr) => attr.name.name === "src",
);
if (srcValue?.value?.value === "") {
//to prevent <img src='' alt='Empty image'/>
Comment thread
neptia marked this conversation as resolved.
context.report({
node: srcValue,
messageId: "SpecifySrcAttribute",
});
} else if (!srcValue) {
//to prevent <img />
Comment thread
neptia marked this conversation as resolved.
context.report({
node,
messageId: "SpecifySrcAttribute",
});
}
}
},
...vueTemplateVisitor,
};
},
};
41 changes: 38 additions & 3 deletions eslint-plugin/lib/rules/prefer-lighter-formats-for-image-files.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,42 @@ module.exports = {
create(context) {
const eligibleExtensions = ["webp", "avif", "svg", "jxl"];

return {
const parserServices = context.parserServices || context.sourceCode?.parserServices;

const vueTemplateVisitor = parserServices?.defineTemplateBodyVisitor
? parserServices.defineTemplateBodyVisitor({
VElement(node) {
const name =
typeof node.name === "string" ? node.name : node.name?.name;
if (name?.toLowerCase() !== "img") return;

const parent = node.parent?.type === "VElement" ? node.parent : null;
const parentName =
typeof parent?.name === "string" ? parent.name : parent?.name?.name;
if (parentName?.toLowerCase() === "picture") return;

const srcAttr = node.startTag.attributes.find(
(attr) => attr.type === "VAttribute" && attr.key?.name === "src",
);
const srcValue = srcAttr?.value?.value;
if (!srcValue) return;

const fileName = srcValue.substring(srcValue.lastIndexOf("/") + 1);
const dotIndex = fileName.lastIndexOf(".");
if (dotIndex === -1) return;

const imgExtension = fileName.substring(dotIndex + 1);
if (eligibleExtensions.includes(imgExtension.toLowerCase())) return;

context.report({
node,
messageId: "PreferLighterFormatsForImageFiles",
data: { eligibleExtensions: eligibleExtensions.join(", ") },
});
},
})
: {};
return {
JSXOpeningElement(node) {
const tagName = node.name.name;
if (tagName?.toLowerCase() !== "img") return;
Expand Down Expand Up @@ -66,7 +101,7 @@ module.exports = {
messageId: "PreferLighterFormatsForImageFiles",
data: { eligibleExtensions: eligibleExtensions.join(", ") },
});
},
};
}
, ...vueTemplateVisitor };
},
};
Loading