Browse Source
* add eslint rule to prevent hardcoded colors in svgs * add tests * warn instead of error for nowpull/16233/merge
4 changed files with 129 additions and 1 deletions
@ -1,3 +1,9 @@
@@ -1,3 +1,9 @@
|
||||
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, |
||||
}, |
||||
}; |
||||
|
||||
@ -0,0 +1,65 @@
@@ -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] }, |
||||
}); |
||||
} |
||||
}, |
||||
}; |
||||
}, |
||||
}; |
||||
@ -0,0 +1,53 @@
@@ -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" } }], |
||||
}, |
||||
], |
||||
}); |
||||
Loading…
Reference in new issue