Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- [#112](https://github.com/green-code-initiative/creedengo-javascript/pull/112) Clarify rule GCI9 "no-import-all-from-library"
- [#113](https://github.com/green-code-initiative/creedengo-javascript/pull/113) Extend rule GCI530 "no-torch" to detect HTML5 Web API usage
- [#115](https://github.com/green-code-initiative/creedengo-javascript/pull/115) Add Vue SFC template support to JSX‑based rules

## [3.1.0] - 2026-05-10

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ This project proposes rules for the following technologies:
- NestJS
- React (JSX)
- React Native / Expo
- Vue

## 🔧 ESLint plugin

Expand Down
13 changes: 12 additions & 1 deletion eslint-plugin/docs/rules/avoid-autoplay.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ Nevertheless, some parts of the video or audio files may be downloaded even if a
will be unnecessarily downloaded even if users do not start the video playback. It is therefore necessary to force
browsers not to preload anything by setting the `preload` attribute to `none`.

This rule supports both [React](https://react.dev/) (JSX) and [Vue](https://vuejs.org/) (template) syntax.

### React (JSX)

```jsx
return (
<>
Expand All @@ -28,7 +32,14 @@ return (
);
```

This rule is build for [React](https://react.dev/) and JSX.
### Vue

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

## Resources

Expand Down
15 changes: 15 additions & 0 deletions eslint-plugin/docs/rules/avoid-css-animations.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ On mobile devices, constant animations can contribute to increased power consump
life.
Limiting the usage of CSS animations helps in creating a more energy-efficient and mobile-friendly user experience.

This rule supports both [React](https://react.dev/) (JSX) and [Vue](https://vuejs.org/) (template) syntax.

### React (JSX)

```jsx
<div style={{ border: "1px solid black", transition: "border 2s ease" }} /> // Non-compliant
```
Expand All @@ -24,6 +28,17 @@ Limiting the usage of CSS animations helps in creating a more energy-efficient a
<div style={{ border: "1px solid black" }} /> // Compliant
```

### Vue

```html
<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
14 changes: 13 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 @@ -17,6 +17,10 @@ Screen readers and other assistive technologies rely on valid image sources to p
with disabilities.
A missing src attribute can result in confusion and hinder accessibility.

This rule supports both [React](https://react.dev/) (JSX) and [Vue](https://vuejs.org/) (template) syntax.

### React (JSX)

```jsx
return (
<>
Expand All @@ -40,7 +44,15 @@ return (
);
```

This rule is build for [React](https://react.dev/) and JSX.
### Vue

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

## Resources

Expand Down
11 changes: 11 additions & 0 deletions eslint-plugin/docs/rules/prefer-lighter-formats-for-image-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ We recommend using the following formats:
- **SVG** (Scalable Vector Graphics) is a vector image format that is based on XML.
Files are lightweight and can be scaled without loss of quality.

This rule supports plain HTML/JS, [React](https://react.dev/) (JSX), and [Vue](https://vuejs.org/) (template) syntax.

```html
<img src="./assets/images/cat.jpg" alt="Unoptimized image of a cat" /> //
Non-compliant
Expand Down Expand Up @@ -72,6 +74,15 @@ is supported, the image.webp image will be downloaded; otherwise, image.jpg imag
</picture>
```

### Vue

```html
<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>
```

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
15 changes: 15 additions & 0 deletions eslint-plugin/docs/rules/prefer-shorthand-css-notations.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ module.exports = {
For example, the `font` shorthand consolidates various font-related properties, and the `margin` shorthand streamlines
the definition of margins around a box.

This rule supports both [React](https://react.dev/) (JSX) and [Vue](https://vuejs.org/) (template) syntax.

### React (JSX)

```jsx
<div
style={{
Expand Down Expand Up @@ -64,6 +68,17 @@ For example, if you only want to set the left margin, you must continue to use `
</div>
```

### Vue

```html
<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
70 changes: 51 additions & 19 deletions eslint-plugin/lib/rules/avoid-autoplay.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@

"use strict";

const {
getVueElementName,
getVueAttribute,
defineVueTemplateVisitor,
} = require("../utils/vue-template");

/** @type {import("eslint").Rule.RuleModule} */
module.exports = {
meta: {
Expand All @@ -36,6 +42,48 @@ 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 vueTemplateVisitor = defineVueTemplateVisitor(context, {
VElement(node) {
const name = getVueElementName(node);
if (name !== "video" && name !== "audio") return;

const autoplayAttr = getVueAttribute(node, "autoplay");
const preloadAttr = getVueAttribute(node, "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,28 +93,12 @@ module.exports = {
const preloadAttr = node.attributes.find(
(attr) => attr.name?.name.toLowerCase() === "preload",
);
if (autoplayAttr && preloadAttr?.value.value !== "none") {
context.report({
node: autoplayAttr,
messageId: "NoAutoplayAndEnforcePreloadNone",
});
} else {
if (autoplayAttr) {
context.report({
node: autoplayAttr,
messageId: "NoAutoplay",
});
}
const preloadValue = preloadAttr?.value?.value;

if (preloadAttr?.value.value !== "none") {
context.report({
node: preloadAttr || node,
messageId: "EnforcePreloadNone",
});
}
}
reportAutoplay(autoplayAttr, preloadAttr, preloadValue, node);
}
},
...vueTemplateVisitor,
};
},
};
26 changes: 26 additions & 0 deletions eslint-plugin/lib/rules/avoid-css-animations.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@

"use strict";

const {
getVueAttribute,
defineVueTemplateVisitor,
} = require("../utils/vue-template");

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
Expand All @@ -34,6 +39,26 @@ module.exports = {
},
create(context) {
const forbiddenProperties = ["transition", "animation"];

const vueTemplateVisitor = defineVueTemplateVisitor(context, {
VElement(node) {
const styleAttr = getVueAttribute(node, "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(
Expand All @@ -60,6 +85,7 @@ module.exports = {
}
}
},
...vueTemplateVisitor,
};
},
};
23 changes: 23 additions & 0 deletions eslint-plugin/lib/rules/no-empty-image-src-attribute.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@

"use strict";

const {
getVueElementName,
getVueAttribute,
defineVueTemplateVisitor,
} = require("../utils/vue-template");

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
Expand All @@ -34,6 +40,22 @@ module.exports = {
schema: [],
},
create(context) {
const vueTemplateVisitor = defineVueTemplateVisitor(context, {
VElement(node) {
if (getVueElementName(node) !== "img") return;

const srcAttr = getVueAttribute(node, "src");
const srcValue = srcAttr?.value?.value;

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

return {
JSXOpeningElement(node) {
if (node.name.name === "img") {
Expand All @@ -55,6 +77,7 @@ module.exports = {
}
}
},
...vueTemplateVisitor,
};
},
};
32 changes: 32 additions & 0 deletions eslint-plugin/lib/rules/prefer-lighter-formats-for-image-files.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@

"use strict";

const {
getVueElementName,
getVueAttribute,
defineVueTemplateVisitor,
} = require("../utils/vue-template");

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
Expand All @@ -36,6 +42,31 @@ module.exports = {
create(context) {
const eligibleExtensions = ["webp", "avif", "svg", "jxl"];

const vueTemplateVisitor = defineVueTemplateVisitor(context, {
VElement(node) {
if (getVueElementName(node) !== "img") return;

const parent = node.parent?.type === "VElement" ? node.parent : null;
if (getVueElementName(parent) === "picture") return;

const srcAttr = getVueAttribute(node, "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;
Expand Down Expand Up @@ -67,6 +98,7 @@ module.exports = {
data: { eligibleExtensions: eligibleExtensions.join(", ") },
});
},
...vueTemplateVisitor,
};
},
};
Loading