-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Breaking changes in plugins potentially caused by parser changes #13473
Comments
If there is any way to get a more granular version selection menu on Tailwind Play, I'd be happy to investigate further and find which version(s) triggered these regressions. |
Perhaps related to #13102 — different issue, but both issues occur when |
I've investigated this a bit further (Play CDN examples) and found another case where this breaks, demonstrated below. I'm going to backversion the Play CDN to see if I can pinpoint which version this bug first presented in.
|
Both groups of examples ( The last version the first group of examples (
I've traversed back pretty far, and I'm struggling to find a version where If nested variants are not currently supported due to some issue parsing the |
That investigation above concerns my MultiTool plugin, though some of those fixes, such as the quotes issue, might also fix some of the issues with the JS Tool plugin. It seems likely that some other symbols are being flagged as well, as JS Tool is breaking in some places without quotes at symbols like parentheses (e.g. Interestingly, in the Tailwind Play example, the intellisense on Tailwind Play appears to be able to process this, though the actual values never make it to the CSSOM. JS Tool also appears to have broken during the v3.3.6 release:
|
@RobinMalfait Could we have the fix for this pushed to both v3 and v4 once it's ready, as it introduced breaking changes to existing v3 users? Thanks |
Hey! You are sharing a few bug reports at once, so I'll try to address them but sorry if I missed one. In v3, we added arbitrary value support, the goal of arbitrary values is to provide an escape hatch for users that need one-off values that don't really fit in your design system. The arbitrary value is a value that should be valid CSS because it will be used as-is in the generated CSS. This is a very important part and we will get back to this in a minute. This means that we have to validate the value a bit to ensure that we don't generate completely broken CSS. One example is making sure that brackets are balanced. This is useful when scanning minified files (where you don't have control over the contents), and also in cases where you missed a bracket e.g.: Back to your issues. One example you shared is this one: <div class="multi-[underline]">Should be underlined</div>
<div class="multi-[underline;font-bold]">Should be underlined and bold</div>
<div class="multi-[underline;font-bold;hover:font-bold]">Should be underlined, bold, and red</div> The last example here doesn't work. This example only ever worked in an alpha version ( The reason is because we introduced arbitrary properties (e.g.: This means that we have to ensure that we don't have a This is why the The other bug you mentioned is this one: <div class="multi-[[font-family:'verdana']]">abc</div> This behavior changed in The same idea applies here where we want to make sure that the arbitrary values are validated (especially around minified files) to prevent creating invalid CSS. That brings me to the clue of the story. We want to ensure that arbitrary values are basically valid CSS, this means that some validation needs to happen. There is an exception to the rule where you can almost do anything you want, the only requirement is that you wrap it in quotes. Later in your plugin, you can parse the value yourself. The origin of this feature is for the An analogy of what you are trying to do: you are trying to invent GraphQL as a new language inside JS syntax and that fails in various ways. This is because we expect valid CSS values in the arbitrary values position. Don't get me wrong, it's awesome to see that people are pushing the boundaries of what Tailwind CSS can do. Your I'm sure that we can fix the current bugs and figure out a way to not regress on previous bugs that happened before, but I'm pretty sure it will result in a whack-a-mole situation where you will discover more and more bugs that this feature in Tailwind CSS is not designed to solve in the first place. So my proposal is this, if you want to write plugins with arbitrary values and those arbitrary values don't map to valid CSS values but rather a custom DSL, wrap the arbitrary value in quotes and adjust the plugin to parse it. This way it is just a plain string and you should be able to put anything in there. To account for this in your plugin: matchUtilities({
multi: (value) => {
const escape = (str) => {
return str.replace(/_/g, '\\_').replace(/ /g, '_')
}
- const utilities = value.split(';').map(escape).join(' ')
+ const utilities = value.slice(1, -1).split(';').map(escape).join(' ')
console.log({ value, utilities })
return {
[`@apply ${utilities}`]: {},
}
},
}) Usage once implemented: - multi-[[font-family:'comic_sans_ms']]
+ multi-["[font-family:'comic_sans_ms']"]
- multi-[!bg-green-500/30;text-green-800;hover:bg-purple-500/30;hover:text-purple-300]
+ multi-['!bg-green-500/30;text-green-800;hover:bg-purple-500/30;hover:text-purple-300'] So while this doesn't solve the issue you are facing, I hope the reasoning behind the change makes sense and allows you to adjust your plugin. Hope this helps! |
@RobinMalfait Thanks for the thorough feedback! For that last example, am I correct in assuming that even in quotes, A few ideas for ways we may be able to combat this:
All three of these blended together might look like this: /* e.g. for arbitrary custom property values */
plugin(({ matchUtilities, theme }) => {
matchUtilities(
{
custom: value => ({ '--custom': value })
},
{
type: ['arbitrary'],
supportsInvalidValues: true,
}
)
})
/* e.g. for arbitrary values that compute to length values */
plugin(({ matchUtilities, theme }) => {
matchUtilities(
{
custom: value => ({ 'width': customFunction(value) })
},
{
type: ['length'],
supportsInvalidValues: true,
}
)
}) I recognize this |
I'm not sure if the same limitation mentioned above (support for |
Hey! Just tried to answer some of your questions below:
No that's not correct, the validation only happens for validating the data inside the square brackets
Imo, that's a bit backwards, you typically always want to validate user input up front instead of validating the response you are going to generate. While we could do this, this would make everything way slower even if 99.999% of the generated CSS is going to be correct because some code has to run over the generated rules to validate them. If we don't validate upfront and only at the end, then each plugin has to do its own validation. We can do that for core plugins, but we can't guarantee that for 3rd party plugins. This means that eventually each plugins has to do validation, and after the calling the plugin, we would have to do another pass to validate if it's actually correct.
The For example:
* { --value: (!bg-green-500/30;text-green-800;hover:bg-purple-500/30;hover:text-purple-300) } This unfortunately is a bit of a bad example because it's true that CSS custom properties can basically retrieve any value. However, this can only ever be used in CSS custom properties position. Apart from that, again this would make the system way to complex for what it's worth. Not only for parsing the values but also picking out the values from your content files. If we allow anything, then there is no real grammar for what Tailwind CSS classes mean. This means that we have to handle way more potential classes that we have to throw away.
Yep, so this is not an issue as stated earlier. So to summarise, wrapping the arbitrary value in quotes only requires you to add the Allowing any value, and validating the "output" of a plugin will make everything more complex and way slower without much benefit. The only difference for your plugin is the need for quotes, once they are there everything should work as expected. |
@RobinMalfait Thanks again I'm not proposing that each plugin does its own validation, but rather that plugins be given the ability to opt into self-validation, with the accepted trade-off of being a bit slower. This would be especially useful for plugins like these two, where users are already prioritizing DX over performance, as Multi can potentially bloat stylesheets except when used with truly unique utilities (e.g. long variant chains under keyed group, etc.) We agree that quotations are a small trade-off and would be easy to account for in my plugin. My reason for surfacing this concern now is not any additional effort on my part but rather ensuring a better DX (and fewer gotchas) for my users. The idea behind Multi is that any utilities that work on their own should be able to be added into a
|
My preference here would still be to allow plugins to opt into arbitrary content, which would open up what is possible with plugins even outside of the usual Tailwind grammars. However, it would appear that nested quotes do work in most cases, though I am trying to figure out why these both work: <div class="multi-['font-bold;text-green-700;before:content-['$']']">5.42</div>
<div class="multi-['multi-['multi-['font-bold']'];text-green-700;before:content-['$']']">5.42</div> but this does not: <div class="multi-['multi-['multi-['font-bold']'];text-green-700;multi-['before:content-['$']']']">5.42</div> Inspect these examples on Tailwind Play: |
What version of Tailwind CSS are you using?
v3.4.3
What browser are you using?
Chrome v123.0.6312.107 (Official Build) (arm64)
What operating system are you using?
macOS Sonoma v14.4.1 (23E224)
Reproduction URL
Describe your issue
I was auditing some of the plugins in my growing collection of plugins I've developed for the Tailwind CSS community this past week, and a couple of them appear to now break due to what I perceive to be changes in the way the parser handles arbitrary values in plugins.
Affected plugins:
1. MultiTool for Tailwind CSS (github | npm)
My MultiTool for Tailwind CSS plugin breaks when it contains a string including the
:
symbol, likely due to the parser terminating/splitting variants when it encounters the:
symbol.I've done some testing, and this doesn't appear to be something my plugin can account for internally. If you look at the reproduction URL linked above (also here), the first two lines receive the expected styles, but the last breaks entirely if it contains that character.
The problematic instances (those that contain
:
) don't appear to trigger the plugin and are likely seen as erroneous since, assuming my guesses here are correct, a usage likemulti-[underline;hover:font-bold]
would be split into:multi-[underline;hover
andfont-bold]
.With some slight tweaking to the parser, values could whitelist any instances of
:
within brackets.'a:b:[c:d]:e'
➞['a', 'b', '[c', 'd]', 'e']
'a:b:[c:d]:e'
➞['a', 'b', '[c:d]', 'e']
It's also important to note here that this was not always the case, or at least I am convinced it is not, as I include this example in the plugin's README, which worked when I tested it before including it in the README:
Even a more
:
-riddled nested variant should be perfectly fine:2. JS Tool for Tailwind CSS (github | npm)
Without rehashing many of the same points I did above, if you take a look at this Tailwind Play example linked above (also here), you'll see many examples I use to showcase how JS can expose specific values to Tailwind CSS using plugins. Admittedly, I seldom use this one, but it is nice sometimes when I need to break glass and expose JSON values to CSS while testing. I could expose them as CSS variables alternatively, but it's helpful to have some means to directly interface with raw data like this, even if only used on occasion.
Here are those same examples from the linked Tailwind Play example, all of which previously worked:
In both of these examples, I'm primarily addressing how these regressions limit plugin authors from achieving some of the powerful tasks plugins were formerly able to perform.
Both of these plugins may seem to operate unconventionally, but that's why they're plugins. This is part of what excites a community like this one—testing the limits of a technology like Tailwind CSS, finding various ways it can be extended to achieve different tasks, and ultimately building within those limits to make someone else's work easier.
I've heard from real users of my plugins how my tools have improved their experience building with Tailwind CSS, and I would hate to discontinue some of them due to breaking changes.
Even this past week, I've been prepping content for a blog post or video—perhaps both—on the powerful ways Tailwind CSS plugins supercharge the developer experience, demonstrating examples and providing a crash course on how to build effective plugins.
"The power of JavaScript in the simple and familiar form factor of Tailwind CSS utilities." (still bikeshedding the name)
Because many people primarily associate Tailwind CSS with CSS (for obvious reasons), they miss that with plugins, we can interject all sorts of sophisticated and useful functionality into our utilities. Plugins are essentially middleware that allow us to bridge the parser/compiler to our frontend in dynamic and performant ways, leveraging build-time processing.
Many things that are not yet possible in pure CSS are possible with Tailwind CSS because of this JS processing, so it acts more like mixins and functions than just variable interpolation.
That said, it would be helpful to use examples like these to demonstrate the flexibility of Tailwind CSS plugins. Thanks!
The text was updated successfully, but these errors were encountered: