Skip to content

Commit ca408d0

Browse files
authored
Do not extract candidates containing JS string interpolation pattern ${ (#17142)
This PR fixes an issue where often people run into issues where they try to use string interpolation and it doesn't work. Even worse, it could result in crashes because we will actually generate CSS. This fix only filters out candidates with a pattern like `${`. If this occurs in a string position it is fine. Another solution would be to add a pre processor for JS/TS (and all thousand file extension combinations) but the problem is that you can also write JS in HTML files so we would have to pre process HTML as well which would not be ideal. # Test plan 1. Added tests to prove this works in arbitrary values, arbitrary variables in both utilities and variants. 2. Existing tests pass. 3. Some screenshots with before / after situations: Given this input: ```ts let color = '#0088cc'; let opacity = 0.8; let name = 'variable-name'; let classes = [ // Arbitrary Properties `[color:${color}]` `[${property}:value]`, `[--img:url('https://example.com?q=${name}')]`, // WONT WORK BUT VALID CSS // Arbitrary Values `bg-[${color}]`, // Arbitrary Variables `bg-(--my-${color})`, `bg-(--my-color,${color})`, // Arbitrary Modifier `bg-red-500/[${opacity}]`, `bg-red-500/(--my-${name})`, `bg-red-500/(--my-opacity,${opacity})`, // Arbitrary Variant `data-[state=${name}]:flex`, `supports-(--my-${name}):flex`, `[@media(width>=${value})]:flex`, ]; ``` This is the result: | Before | After | | --- | --- | | <img width="908" alt="image" src="https://github.com/user-attachments/assets/c64d1b16-d39d-48a6-a098-bc4477cb4b0a" /> | <img width="908" alt="image" src="https://github.com/user-attachments/assets/d71aaf62-5e13-4174-82bb-690eb81aaeaf" /> | Fixes: #17054 Fixes: #15853
1 parent 4455048 commit ca408d0

File tree

5 files changed

+114
-0
lines changed

5 files changed

+114
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
- _Experimental_: Add `user-valid` and `user-invalid` variants ([#12370](https://github.com/tailwindlabs/tailwindcss/pull/12370))
1818
- _Experimental_: Add `wrap-anywhere`, `wrap-break-word`, and `wrap-normal` utilities ([#12128](https://github.com/tailwindlabs/tailwindcss/pull/12128))
1919

20+
### Fixed
21+
22+
- Do not extract candidates with JS string interpolation `${` ([#17142](https://github.com/tailwindlabs/tailwindcss/pull/17142))
23+
2024
## [4.0.13] - 2025-03-11
2125

2226
### Fixed

crates/oxide/src/extractor/arbitrary_property_machine.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,11 @@ impl Machine for ArbitraryPropertyMachine<ParsingValueState> {
226226
// URLs are not allowed
227227
Class::Slash if start_of_value_pos == cursor.pos => return self.restart(),
228228

229+
// String interpolation-like syntax is not allowed. E.g.: `[${x}]`
230+
Class::Dollar if matches!(cursor.next.into(), Class::OpenCurly) => {
231+
return self.restart()
232+
}
233+
229234
// Everything else is valid
230235
_ => cursor.advance(),
231236
};
@@ -276,6 +281,9 @@ enum Class {
276281
#[bytes(b'-')]
277282
Dash,
278283

284+
#[bytes(b'$')]
285+
Dollar,
286+
279287
#[bytes_range(b'a'..=b'z')]
280288
AlphaLower,
281289

@@ -411,4 +419,26 @@ mod tests {
411419
}
412420
}
413421
}
422+
423+
#[test]
424+
fn test_exceptions() {
425+
for (input, expected) in [
426+
// JS string interpolation
427+
// In key
428+
("[${x}:value]", vec![]),
429+
// As part of the key
430+
("[background-${property}:value]", vec![]),
431+
// In value
432+
("[key:${x}]", vec![]),
433+
// As part of the value
434+
("[key:value-${x}]", vec![]),
435+
// Allowed in strings
436+
("[--img:url('${x}')]", vec!["[--img:url('${x}')]"]),
437+
] {
438+
assert_eq!(
439+
ArbitraryPropertyMachine::<IdleState>::test_extract_all(input),
440+
expected
441+
);
442+
}
443+
}
414444
}

crates/oxide/src/extractor/arbitrary_value_machine.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ impl Machine for ArbitraryValueMachine {
9595
// Any kind of whitespace is not allowed
9696
Class::Whitespace => return self.restart(),
9797

98+
// String interpolation-like syntax is not allowed. E.g.: `[${x}]`
99+
Class::Dollar if matches!(cursor.next.into(), Class::OpenCurly) => {
100+
return self.restart()
101+
}
102+
98103
// Everything else is valid
99104
_ => cursor.advance(),
100105
};
@@ -133,6 +138,9 @@ enum Class {
133138
#[bytes(b' ', b'\t', b'\n', b'\r', b'\x0C')]
134139
Whitespace,
135140

141+
#[bytes(b'$')]
142+
Dollar,
143+
136144
#[fallback]
137145
Other,
138146
}
@@ -188,4 +196,17 @@ mod tests {
188196
assert_eq!(ArbitraryValueMachine::test_extract_all(input), expected);
189197
}
190198
}
199+
200+
#[test]
201+
fn test_exceptions() {
202+
for (input, expected) in [
203+
// JS string interpolation
204+
("[${x}]", vec![]),
205+
("[url(${x})]", vec![]),
206+
// Allowed in strings
207+
("[url('${x}')]", vec!["[url('${x}')]"]),
208+
] {
209+
assert_eq!(ArbitraryValueMachine::test_extract_all(input), expected);
210+
}
211+
}
191212
}

crates/oxide/src/extractor/arbitrary_variable_machine.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,11 @@ impl Machine for ArbitraryVariableMachine<ParsingFallbackState> {
252252
// Any kind of whitespace is not allowed
253253
Class::Whitespace => return self.restart(),
254254

255+
// String interpolation-like syntax is not allowed. E.g.: `[${x}]`
256+
Class::Dollar if matches!(cursor.next.into(), Class::OpenCurly) => {
257+
return self.restart()
258+
}
259+
255260
// Everything else is valid
256261
_ => cursor.advance(),
257262
};
@@ -284,6 +289,9 @@ enum Class {
284289
#[bytes(b'.')]
285290
Dot,
286291

292+
#[bytes(b'$')]
293+
Dollar,
294+
287295
#[bytes(b'\\')]
288296
Escape,
289297

@@ -380,4 +388,25 @@ mod tests {
380388
);
381389
}
382390
}
391+
392+
#[test]
393+
fn test_exceptions() {
394+
for (input, expected) in [
395+
// JS string interpolation
396+
// As part of the variable
397+
("(--my-${var})", vec![]),
398+
// As the fallback
399+
("(--my-variable,${var})", vec![]),
400+
// As the fallback in strings
401+
(
402+
"(--my-variable,url('${var}'))",
403+
vec!["(--my-variable,url('${var}'))"],
404+
),
405+
] {
406+
assert_eq!(
407+
ArbitraryVariableMachine::<IdleState>::test_extract_all(input),
408+
expected
409+
);
410+
}
411+
}
383412
}

crates/oxide/src/extractor/candidate_machine.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,4 +327,34 @@ mod tests {
327327
);
328328
}
329329
}
330+
331+
#[test]
332+
fn test_js_interpolation() {
333+
for (input, expected) in [
334+
// Utilities
335+
// Arbitrary value
336+
("bg-[${color}]", vec![]),
337+
// Arbitrary property
338+
("[color:${value}]", vec![]),
339+
("[${key}:value]", vec![]),
340+
("[${key}:${value}]", vec![]),
341+
// Arbitrary property for CSS variables
342+
("[--color:${value}]", vec![]),
343+
("[--color-${name}:value]", vec![]),
344+
// Arbitrary variable
345+
("bg-(--my-${name})", vec![]),
346+
("bg-(--my-variable,${fallback})", vec![]),
347+
(
348+
"bg-(--my-image,url('https://example.com?q=${value}'))",
349+
vec!["bg-(--my-image,url('https://example.com?q=${value}'))"],
350+
),
351+
// Variants
352+
("data-[state=${state}]:flex", vec![]),
353+
("support-(--my-${value}):flex", vec![]),
354+
("support-(--my-variable,${fallback}):flex", vec![]),
355+
("[@media(width>=${value})]:flex", vec![]),
356+
] {
357+
assert_eq!(CandidateMachine::test_extract_all(input), expected);
358+
}
359+
}
330360
}

0 commit comments

Comments
 (0)