mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[CL-846] forbid hardcoded colors in svg (#16167)
* add eslint rule to prevent hardcoded colors in svgs * add tests * warn instead of error for now
This commit is contained in:
@@ -35,6 +35,7 @@ export default tseslint.config(
|
|||||||
rxjs: fixupPluginRules(rxjs),
|
rxjs: fixupPluginRules(rxjs),
|
||||||
"rxjs-angular": fixupPluginRules(angularRxjs),
|
"rxjs-angular": fixupPluginRules(angularRxjs),
|
||||||
"@bitwarden/platform": platformPlugins,
|
"@bitwarden/platform": platformPlugins,
|
||||||
|
"@bitwarden/components": componentPlugins,
|
||||||
},
|
},
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
@@ -74,8 +75,11 @@ export default tseslint.config(
|
|||||||
"@angular-eslint/prefer-standalone": 0,
|
"@angular-eslint/prefer-standalone": 0,
|
||||||
"@angular-eslint/use-lifecycle-interface": "error",
|
"@angular-eslint/use-lifecycle-interface": "error",
|
||||||
"@angular-eslint/use-pipe-transform-interface": 0,
|
"@angular-eslint/use-pipe-transform-interface": 0,
|
||||||
|
|
||||||
"@bitwarden/platform/required-using": "error",
|
"@bitwarden/platform/required-using": "error",
|
||||||
"@bitwarden/platform/no-enums": "error",
|
"@bitwarden/platform/no-enums": "error",
|
||||||
|
"@bitwarden/components/require-theme-colors-in-svg": "warn",
|
||||||
|
|
||||||
"@typescript-eslint/explicit-member-accessibility": ["error", { accessibility: "no-public" }],
|
"@typescript-eslint/explicit-member-accessibility": ["error", { accessibility: "no-public" }],
|
||||||
"@typescript-eslint/no-explicit-any": "off", // TODO: This should be re-enabled
|
"@typescript-eslint/no-explicit-any": "off", // TODO: This should be re-enabled
|
||||||
"@typescript-eslint/no-floating-promises": "error",
|
"@typescript-eslint/no-floating-promises": "error",
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
import requireLabelOnBiticonbutton from "./require-label-on-biticonbutton.mjs";
|
import requireLabelOnBiticonbutton from "./require-label-on-biticonbutton.mjs";
|
||||||
|
import requireThemeColorsInSvg from "./require-theme-colors-in-svg.mjs";
|
||||||
|
|
||||||
export default { rules: { "require-label-on-biticonbutton": requireLabelOnBiticonbutton } };
|
export default {
|
||||||
|
rules: {
|
||||||
|
"require-label-on-biticonbutton": requireLabelOnBiticonbutton,
|
||||||
|
"require-theme-colors-in-svg": requireThemeColorsInSvg,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
65
libs/eslint/components/require-theme-colors-in-svg.mjs
Normal file
65
libs/eslint/components/require-theme-colors-in-svg.mjs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Forbid hardcoded colors in SVGs; enforce CSS variables instead.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const COLOR_REGEX =
|
||||||
|
/(?:fill|stroke|stop-color|flood-color|lighting-color)\s*=\s*["'](?!none["'])(?!var\(--)(#(?:[0-9a-f]{3,8})|rgba?\([^)]+\)|hsla?\([^)]+\)|[a-zA-Z]+)["']/gi;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
meta: {
|
||||||
|
type: "problem",
|
||||||
|
docs: {
|
||||||
|
description: "Forbid hardcoded colors in SVGs; enforce theme variables instead.",
|
||||||
|
category: "Best Practices",
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
hardcodedColor:
|
||||||
|
"Hardcoded color '{{color}}' found in SVG. Use Tailwind or CSS variables instead.",
|
||||||
|
},
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
tagNames: {
|
||||||
|
type: "array",
|
||||||
|
items: { type: "string" },
|
||||||
|
default: ["svgIcon"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
create(context) {
|
||||||
|
const options = context.options[0] || {};
|
||||||
|
const tagNames = options.tagNames || ["svgIcon"];
|
||||||
|
|
||||||
|
function isSvgTaggedTemplate(node) {
|
||||||
|
return (
|
||||||
|
node.tag &&
|
||||||
|
((node.tag.type === "Identifier" && tagNames.includes(node.tag.name)) ||
|
||||||
|
(node.tag.type === "MemberExpression" && tagNames.includes(node.tag.property.name)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
TaggedTemplateExpression(node) {
|
||||||
|
if (!isSvgTaggedTemplate(node)) return;
|
||||||
|
|
||||||
|
const svgString = node.quasi.quasis.map((q) => q.value.raw).join("");
|
||||||
|
let match;
|
||||||
|
while ((match = COLOR_REGEX.exec(svgString)) !== null) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
loc: context.getSourceCode().getLocFromIndex(node.range[0] + match.index),
|
||||||
|
messageId: "hardcodedColor",
|
||||||
|
data: { color: match[1] },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
53
libs/eslint/components/require-theme-colors-in-svg.spec.mjs
Normal file
53
libs/eslint/components/require-theme-colors-in-svg.spec.mjs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { RuleTester } from "@typescript-eslint/rule-tester";
|
||||||
|
import rule from "./require-theme-colors-in-svg.mjs";
|
||||||
|
|
||||||
|
const ruleTester = new RuleTester({
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: [__dirname + "/../tsconfig.spec.json"],
|
||||||
|
projectService: {
|
||||||
|
allowDefaultProject: ["*.ts*"],
|
||||||
|
},
|
||||||
|
tsconfigRootDir: __dirname + "/..",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
ruleTester.run("require-theme-colors-in-svg", rule.default, {
|
||||||
|
valid: [
|
||||||
|
{
|
||||||
|
name: "Allows fill=none",
|
||||||
|
code: 'const icon = svgIcon`<svg><path fill="none"/></svg>`;',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Allows CSS variable",
|
||||||
|
code: 'const icon = svgIcon`<svg><path fill="var(--my-color)"/></svg>`;',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Allows class-based coloring",
|
||||||
|
code: 'const icon = svgIcon`<svg><path class="tw-fill-art-primary"/></svg>`;',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
name: "Errors on fill with hex color",
|
||||||
|
code: 'const icon = svgIcon`<svg><path fill="#000000"/></svg>`;',
|
||||||
|
errors: [{ messageId: "hardcodedColor", data: { color: "#000000" } }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Errors on stroke with named color",
|
||||||
|
code: 'const icon = svgIcon`<svg><path stroke="red"/></svg>`;',
|
||||||
|
errors: [{ messageId: "hardcodedColor", data: { color: "red" } }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Errors on fill with rgb()",
|
||||||
|
code: 'const icon = svgIcon`<svg><path fill="rgb(255,0,0)"/></svg>`;',
|
||||||
|
errors: [{ messageId: "hardcodedColor", data: { color: "rgb(255,0,0)" } }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Errors on fill with named color",
|
||||||
|
code: 'const icon = svgIcon`<svg><path fill="blue"/></svg>`;',
|
||||||
|
errors: [{ messageId: "hardcodedColor", data: { color: "blue" } }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user