Skip to content

Commit b969199

Browse files
authored
Merge pull request #5425 from remotion-dev/copilot/fix-5424
2 parents 97513fe + c631da5 commit b969199

File tree

3 files changed

+349
-0
lines changed

3 files changed

+349
-0
lines changed

packages/eslint-plugin/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import noBackgroundImage from './rules/no-background-image';
44
import durationInFrames from './rules/no-duration-frames-infinity';
55
import noFrom0 from './rules/no-from-0';
66
import noStringAssets from './rules/no-string-assets';
7+
import slowCssProperty from './rules/slow-css-property';
78
import staticFileNoRelative from './rules/staticfile-no-relative';
89
import staticFileNoRemote from './rules/staticfile-no-remote';
910
import useGifComponent from './rules/use-gif-component';
@@ -23,6 +24,7 @@ const rules = {
2324
'staticfile-no-relative': staticFileNoRelative,
2425
'staticfile-no-remote': staticFileNoRemote,
2526
'no-background-image': noBackgroundImage,
27+
'slow-css-property': slowCssProperty,
2628
'v4-config-import': v4Import,
2729
};
2830

@@ -38,6 +40,7 @@ const recommendedRuleConfig = {
3840
'@remotion/staticfile-no-relative': 'error',
3941
'@remotion/staticfile-no-remote': 'error',
4042
'@remotion/no-background-image': 'error',
43+
'@remotion/slow-css-property': 'warn',
4144
'@remotion/v4-config-import': 'error',
4245
} as const;
4346

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import {ESLintUtils} from '@typescript-eslint/utils';
2+
3+
const createRule = ESLintUtils.RuleCreator(() => {
4+
return 'https://remotion.dev/docs/gpu';
5+
});
6+
7+
type Options = [];
8+
type MessageIds = 'SlowCssProperty';
9+
10+
const SlowCssProperty = [
11+
"This GPU effect may slow down the render on machines which don't have a GPU.",
12+
'See: https://remotion.dev/docs/gpu',
13+
].join('\n');
14+
15+
const slowCssProperties = new Set(['boxShadow', 'textShadow', 'filter']);
16+
const slowCssPropertiesKebab = new Set(['box-shadow', 'text-shadow', 'filter']);
17+
18+
// Tailwind classes that correspond to slow CSS properties
19+
const slowTailwindClasses = [
20+
// Shadow classes (box-shadow)
21+
/\bshadow-(?:sm|md|lg|xl|2xl|inner|none|\w+)\b/,
22+
/\bshadow-\w+(?:\/\d+)?\b/, // Custom shadow colors
23+
// Filter classes
24+
/\bblur-(?:none|sm|md|lg|xl|2xl|3xl|\w+)\b/,
25+
/\bbrightness-\d+\b/,
26+
/\bcontrast-\d+\b/,
27+
/\bdrop-shadow-(?:sm|md|lg|xl|2xl|none|\w+)\b/,
28+
/\bgrayscale(?:-\d+)?\b/,
29+
/\bhue-rotate-\d+\b/,
30+
/\binvert(?:-\d+)?\b/,
31+
/\bsaturate-\d+\b/,
32+
/\bsepia(?:-\d+)?\b/,
33+
// Text shadow (custom utilities)
34+
/\btext-shadow-\w+\b/,
35+
];
36+
37+
function containsSlowTailwindClass(classString: string): boolean {
38+
return slowTailwindClasses.some((pattern) => pattern.test(classString));
39+
}
40+
41+
export default createRule<Options, MessageIds>({
42+
name: 'slow-css-property',
43+
meta: {
44+
type: 'problem',
45+
docs: {
46+
description: SlowCssProperty,
47+
recommended: 'warn',
48+
},
49+
fixable: undefined,
50+
schema: [],
51+
messages: {
52+
SlowCssProperty: SlowCssProperty,
53+
},
54+
},
55+
defaultOptions: [],
56+
create: (context) => {
57+
return {
58+
Property: (node) => {
59+
let propertyName: string | undefined;
60+
61+
if (node.key.type === 'Identifier') {
62+
propertyName = node.key.name;
63+
} else if (
64+
node.key.type === 'Literal' &&
65+
typeof node.key.value === 'string'
66+
) {
67+
propertyName = node.key.value;
68+
}
69+
70+
if (!propertyName) {
71+
return;
72+
}
73+
74+
const isSlowProperty =
75+
slowCssProperties.has(propertyName) ||
76+
slowCssPropertiesKebab.has(propertyName);
77+
78+
if (isSlowProperty) {
79+
context.report({
80+
messageId: 'SlowCssProperty',
81+
node,
82+
});
83+
}
84+
},
85+
JSXAttribute: (node) => {
86+
if (
87+
node.name.type === 'JSXIdentifier' &&
88+
node.name.name === 'className' &&
89+
node.value
90+
) {
91+
let classString: string | undefined;
92+
93+
if (
94+
node.value.type === 'Literal' &&
95+
typeof node.value.value === 'string'
96+
) {
97+
classString = node.value.value;
98+
} else if (
99+
node.value.type === 'JSXExpressionContainer' &&
100+
node.value.expression.type === 'Literal' &&
101+
typeof node.value.expression.value === 'string'
102+
) {
103+
classString = node.value.expression.value;
104+
} else if (
105+
node.value.type === 'JSXExpressionContainer' &&
106+
node.value.expression.type === 'TemplateLiteral'
107+
) {
108+
// For template literals, check all string parts
109+
const templateLiteral = node.value.expression;
110+
classString = templateLiteral.quasis
111+
.map((q) => q.value.cooked || q.value.raw)
112+
.join(' ');
113+
}
114+
115+
if (classString && containsSlowTailwindClass(classString)) {
116+
context.report({
117+
messageId: 'SlowCssProperty',
118+
node,
119+
});
120+
}
121+
}
122+
},
123+
};
124+
},
125+
});
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import {ESLintUtils} from '@typescript-eslint/utils';
2+
import rule from '../rules/slow-css-property';
3+
4+
const ruleTester = new ESLintUtils.RuleTester({
5+
parser: '@typescript-eslint/parser',
6+
parserOptions: {
7+
ecmaFeatures: {
8+
jsx: true,
9+
},
10+
},
11+
});
12+
13+
ruleTester.run('slow-css-property', rule, {
14+
valid: [
15+
`const style = {color: "red"}`,
16+
`const style = {backgroundColor: "blue"}`,
17+
`const style = {margin: 10}`,
18+
`const style = {border: "1px solid black"}`,
19+
// Valid Tailwind classes (no slow properties)
20+
`<div className="text-red-500 bg-blue-500 p-4" />`,
21+
`<div className="font-bold text-lg" />`,
22+
`<div className="border border-gray-300" />`,
23+
`<div className={"text-blue-500"} />`,
24+
],
25+
invalid: [
26+
{
27+
code: `const style = {boxShadow: "0 0 5px red"}`,
28+
errors: [
29+
{
30+
messageId: 'SlowCssProperty',
31+
},
32+
],
33+
},
34+
{
35+
code: `const style = {textShadow: "1px 1px 1px black"}`,
36+
errors: [
37+
{
38+
messageId: 'SlowCssProperty',
39+
},
40+
],
41+
},
42+
{
43+
code: `const style = {filter: "blur(5px)"}`,
44+
errors: [
45+
{
46+
messageId: 'SlowCssProperty',
47+
},
48+
],
49+
},
50+
{
51+
code: `const style = {
52+
color: "red",
53+
boxShadow: "0 0 5px red",
54+
textShadow: "1px 1px 1px black"
55+
}`,
56+
errors: [
57+
{
58+
messageId: 'SlowCssProperty',
59+
},
60+
{
61+
messageId: 'SlowCssProperty',
62+
},
63+
],
64+
},
65+
{
66+
code: `const style = {"box-shadow": "0 0 5px red"}`,
67+
errors: [
68+
{
69+
messageId: 'SlowCssProperty',
70+
},
71+
],
72+
},
73+
{
74+
code: `const style = {"text-shadow": "1px 1px 1px black"}`,
75+
errors: [
76+
{
77+
messageId: 'SlowCssProperty',
78+
},
79+
],
80+
},
81+
{
82+
code: `const style = {"filter": "blur(5px)"}`,
83+
errors: [
84+
{
85+
messageId: 'SlowCssProperty',
86+
},
87+
],
88+
},
89+
{
90+
code: `const style = {
91+
color: "red",
92+
"box-shadow": "0 0 5px red",
93+
"text-shadow": "1px 1px 1px black"
94+
}`,
95+
errors: [
96+
{
97+
messageId: 'SlowCssProperty',
98+
},
99+
{
100+
messageId: 'SlowCssProperty',
101+
},
102+
],
103+
},
104+
// Tailwind shadow classes
105+
{
106+
code: `<div className="shadow-lg" />`,
107+
errors: [
108+
{
109+
messageId: 'SlowCssProperty',
110+
},
111+
],
112+
},
113+
{
114+
code: `<div className="shadow-xl text-red-500" />`,
115+
errors: [
116+
{
117+
messageId: 'SlowCssProperty',
118+
},
119+
],
120+
},
121+
{
122+
code: `<div className="text-blue-500 shadow-md p-4" />`,
123+
errors: [
124+
{
125+
messageId: 'SlowCssProperty',
126+
},
127+
],
128+
},
129+
// Tailwind filter classes
130+
{
131+
code: `<div className="blur-sm" />`,
132+
errors: [
133+
{
134+
messageId: 'SlowCssProperty',
135+
},
136+
],
137+
},
138+
{
139+
code: `<div className="brightness-75" />`,
140+
errors: [
141+
{
142+
messageId: 'SlowCssProperty',
143+
},
144+
],
145+
},
146+
{
147+
code: `<div className="contrast-125" />`,
148+
errors: [
149+
{
150+
messageId: 'SlowCssProperty',
151+
},
152+
],
153+
},
154+
{
155+
code: `<div className="drop-shadow-lg" />`,
156+
errors: [
157+
{
158+
messageId: 'SlowCssProperty',
159+
},
160+
],
161+
},
162+
{
163+
code: `<div className="grayscale" />`,
164+
errors: [
165+
{
166+
messageId: 'SlowCssProperty',
167+
},
168+
],
169+
},
170+
{
171+
code: `<div className="hue-rotate-90" />`,
172+
errors: [
173+
{
174+
messageId: 'SlowCssProperty',
175+
},
176+
],
177+
},
178+
{
179+
code: `<div className="invert" />`,
180+
errors: [
181+
{
182+
messageId: 'SlowCssProperty',
183+
},
184+
],
185+
},
186+
{
187+
code: `<div className="saturate-150" />`,
188+
errors: [
189+
{
190+
messageId: 'SlowCssProperty',
191+
},
192+
],
193+
},
194+
{
195+
code: `<div className="sepia" />`,
196+
errors: [
197+
{
198+
messageId: 'SlowCssProperty',
199+
},
200+
],
201+
},
202+
// JSX Expression Container
203+
{
204+
code: `<div className={"shadow-lg"} />`,
205+
errors: [
206+
{
207+
messageId: 'SlowCssProperty',
208+
},
209+
],
210+
},
211+
// Template literal
212+
{
213+
code: `<div className={\`shadow-lg \${variant}\`} />`,
214+
errors: [
215+
{
216+
messageId: 'SlowCssProperty',
217+
},
218+
],
219+
},
220+
],
221+
});

0 commit comments

Comments
 (0)