w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.5k stars 661 forks source link

[css-nesting] Require `div&`, disallow `&div`, for Sass compat #8662

Open tabatkins opened 1 year ago

tabatkins commented 1 year ago

Given the very heartening news that infinite-lookahead is viable in Chrome, I'd like to try and revisit one of our syntax changes caused by Nesting.

In the original version of the spec, you had to start a selector with &, so to handle the case where you want to add a type selector, we relaxed the restriction that a type selector had to be the first thing in a compound selector. This allowed &div to work, tho div& was theoretically okay (in cases like @nest div&).

In the current version of the spec, you still can't start a selector with an ident, so &div is still the right way to spell things most of the time (but again, still okay in theory to do the other way, like .foo div&).

Neither of these restrictions will apply anymore if #7961 goes thru. Which is good, because &div is terrible for Sass.

Sass essentially uses text concatenation for its nesting feature. If you write .foo { &div {...}} in Sass, it generates a .foodiv {...} rule - a single larger class selector. In CSS Nesting, this is instead equivalent to div.foo {...} - a type selector plus a class selector.

This mismatch in syntax was always going to be an enormous pain for Sass to migrate to CSS Nesting (possibly a straight-up blocker, requiring Sass users to explicitly opt into CSS Nesting instead), but when other factors made &div the preferred form, I accepted that it was just one of those painful situations that'll be worth it in the long term.

But given #7961, there's no longer any reason to prefer &div over div&. So we can simultaneously (a) remove a syntax change, preserving the original syntax of Selectors that has been stable for a long time, and (b) make Sass's task of migrating to CSS Nesting natively massively easier (along with any other preprocessor that has a similar feature, but I'm familiar with Sass's syntax here).

I've talked with @andruud about this and he's dropping a Use Counter into Chrome preemptively, to make sure we'll actually be able to make this change. (Since we're shipping the current Nesting spec in Chrome 112 which is just now releasing to Stable.)

/cc @nex3

romainmenke commented 1 year ago

This would be breaking for anyone using the current specification together with something like PostCSS plugins, lightningcss, ...

Given that we have been teaching users to write &div this is definitely breaking for a lot of people.

Having a use counter in Chrome won't say much. The actual usage is hidden because everyone ships "transpiled" CSS.


Edit :

I think this change will shift the migration pains from users using Sass to users using PostCSS/LightningCSS/...

It will also make the feature harder to teach because it again adds a weird exception.

tabatkins commented 1 year ago

This would be breaking for anyone using the current specification together with something like PostCSS plugins, lightningcss, ...

This is the cost of transpiling ahead of shipping implementations, yes.

Given that we have been teaching users to write &div this is definitely breaking for a lot of people.

Depends on how common that actually is.

It will also make the feature harder to teach because it again adds a weird exception.

What do you mean by this? This returns us to the prior rules for compound selectors, where a type selector, if present, must occur first.

romainmenke commented 1 year ago

This is the cost of transpiling ahead of shipping implementations, yes.

The same is true for sass. Any argument goes both ways here :)

If we are arguing that the spec should be changed because sass tanspiles in a specific way we can also argue that it shouldn't be changed because PostCSS transpiles in a specific way.

In principle I do agree that any polyfill that is shipped before implementations is likely to see breaking changes. But this specific change is being made to accommodate sass, not to help authors or implementations.

What do you mean by this? This returns us to the prior rules for compound selectors, where a type selector, if present, must occur first.

Maybe it is not yet too late to take this back and it won't be seen as an exception. I am not the right person to judge this given how used I am to all the iterations this specification has seen :) To me it now seems like a weird exception, but it might not be.

tabatkins commented 1 year ago

The same is true for sass. Any argument goes both ways here :)

Somewhat. Sass far predates CSS Nesting, and we're invading the same syntax space. This is different from projects that intentionally followed our spec, intending to match what browsers eventually ship with - there was always the chance that the spec changes before browsers ship. (It's already happened once, with the removal of @nest and loosening of restrictions on selectors. Anyone using @nest in those processors was "broken" to the exact same degree that anyone using &div today would be.) Following the specs ahead of browsers is inherently risky, and we intentionally do not account for this type of thing when deciding whether to change features in specs.

The type of breakage is also significant. Any project following the current Nesting spec which allows &div can just switch to outputting div& - &div being a syntax error in raw CSS means that there's no harm in recognizing it and correcting the output.

This is distinct from the Sass problem, where &div is already a recognized syntax with a distinct behavior, which is not reproducible in pure CSS.

(In either case, these tools all output non-nested CSS, which means their output is going to continue to be correct. The only compat problem would be if they started outputting nested CSS for newer browsers, and spat out &div, as that would be invalid after the change.)

One must also realistically consider the scale of the userbases between the projects. We're talking compat here, so actual number-of-devs-affected is more than a theoretical concern.

romainmenke commented 1 year ago

One must also realistically consider the scale of the userbases between the projects. We're talking compat here, so actual number-of-devs-affected is more than a theoretical concern.

PostCSS Nesting for example has ±7 million weekly installs. Whereas Sass has about ±11million. There are other projects like LightningCSS which adhere to the same principles and there are other distributions of Sass.

Neither group is small enough to simply ignore :) There will be real compat pains here.

This is different from projects that intentionally followed our spec, intending to match what browsers eventually ship with - there was always the chance that the spec changes before browsers ship. (It's already happened once, with the removal of @nest and loosening of restrictions on selectors.

Correct, we have and always will follow the specification. We might challenge proposed changes, but we will always follow the specification.

This is the only thing that makes sense long term. Any migration pains are short term.

nex3 commented 1 year ago

PostCSS Nesting for example has ±7 million weekly installs. Whereas Sass has about ±11million.

Note that Sass's npm distribution is not the whole story—Sass is also commonly installed via Homebrew, Chocolatey, GitHub downloads, the occasional Linux distro, and so on. Sass's support for &foo is also much older than postcss-nesting's (Sass added support in 2014). On top of all of that, using &foo as a suffix is likely to be much more popular among Sass users than using it to add a type selector is among postcss-nesting users: &-suffix is a critical part of BEM-style design methodologies that use modifiers of base class names to communicate semantic relationships, while needing to add a type selector to a compound selector in a nested context is so rare that we only had a small handful of support requests about it in the entire lifetime of Sass.

It's also worth noting that Less (~5 million weekly installs on npm) and Stylus (~3 million) both also use Sass's semantics for &foo here, and have also done so for around a decade. The postcss-nested plugin (~6 million) also implements these semantics. So all told, the difference is more like ~8 million to ~25 million, even before considering the longevity and relative usefulness of the two forms.

romainmenke commented 1 year ago

So all told, the difference is more like ~8 million to ~25 million, even before considering the longevity and relative usefulness of the two forms.

Sure, but these are still two sizable user groups. I didn't mean to imply that both were equivalent in size, only that this isn't a less than 1% kind of thing.


I had two concerns, one of which tabatkins addressed.

1 : That the feature would be harder to teach.

But I agree with tabatkins on this. Restoring that restriction makes sense to me on it's own, while technically it is a "breaking change".

Authors who absolutely want to start every selector with & and want to write &div can also write &:is(div).

2 : That the adoption/usage of &div will only be determined by looking at a use counter in Chrome, ignoring that PostCSS and others exist. And ignoring the reality that people will continue to transpile their nested source to non-nested CSS for years to come.

They wrote CSS that works in the current Chrome version. That they transpile their source to support other browsers and older versions of Chrome should be taken into account.

Just an acknowledgment that some/any weight will given to the migration pains of these CSS authors would be sufficient.


The type of breakage is also significant. Any project following the current Nesting spec which allows &div can just switch to outputting div& - &div being a syntax error in raw CSS means that there's no harm in recognizing it and correcting the output.

Indeed, we will handle this similarly as we will handle the removal of @nest.

This combination is the least disruptive.

tabatkins commented 1 year ago

That the adoption/usage of &div will only be determined by looking at a use counter in Chrome, ignoring that PostCSS and others exist. And ignoring the reality that people will continue to transpile their nested source to non-nested CSS for years to come.

Yes, we intentionally do not pay attention to preprocessors that attempt to lead the spec before browser support is solidified, because "ships in a major browser and sees measurable use" is the point at which we consider a feature stable. Going ahead of browsers means you're working with Explicitly Unstable And Possibly Bad Ideas.

I'm being very firm here for a reason - it's incredibly hostile to all other parties to attempt to unilaterally thrust an early, unstable version of our work into "frozen in practice" stability. The browsers all have explicit steps in their launch processes for seeking and ensuring consensus and stability, and advance without those guarantees very carefully and deliberately. PostCSS and related tools do not meet that bar.

It is also the case that, as a general rule, such tools do not impose nearly the "frozen in practice" weight that a browser does. They generate valid old-style code, which will continue to work as intended regardless of how we change the feature the preprocessor is implementing. The tool itself will continue to accept CSS written for it, regardless of how we change the spec, so long as authors don't update the version. The only issue arises when authors are updating their tool version but not maintaining their code; then their sites will break. But that's the case for every tool they use, for any purpose whatsoever. A syntax change is a major-version bump in semver; you must be ready to fix breakage if you accept the bump.


Separate from the above, as Natalie said, if we make this change and PostCSS/etc follow, then the &div syntax will just become invalid in them. This means those tools can offer migration tools: auto fixup, warnings, source rewriting, etc.

This is not the case for Sass/etc here - if we leave the spec as-is, then it conflicts with valid Sass/etc code that has specific, unreproducable-in-CSS behavior. And we know that it's pretty common behavior in Sass/etc.

Luckily it's not the end of the world for these tools - div& is legal to write today in CSS (so long as it's not at the start of the selector, but that restriction'll drop), so they could just continue to say that &div has its current meaning in those tools (concatenation) while div& means the same as CSS. This would be a behavior divergence from CSS, but as Natalie noted, it's a pretty rare situation in practice anyway.

But, since the only reason for relaxing the "type selectors must go in front" restriction was to deal with the parsing restrictions that earlier versions of Nesting imposed, and those restrictions no longer apply, and reverting to the old type-selector behavior would avoid a behavior difference with a decade-old Sass syntax that we're intentionally invading the syntax space of, it'll be nice to make the change if we can.

romainmenke commented 1 year ago

I'm being very firm here for a reason - it's incredibly hostile to all other parties to attempt to unilaterally thrust an early, unstable version of our work into "frozen in practice" stability. The browsers all have explicit steps in their launch processes for seeking and ensuring consensus and stability, and advance without those guarantees very carefully and deliberately. PostCSS and related tools do not meet that bar.

I strongly agree with this and have added some guidelines to prevent this going forward. It is ok for anyone to create a tool to play around with a proposed feature before implementation, it is not ok for a tool with a large user base to act as if these are "ready for use" before they ship in actual browsers.

It think it is even worse than what you are describing. By going ahead of browsers we also shape the perception of a feature and the mental model around a feature before it is ready. Every poll to gather CSS Author feedback around nesting was biased because of PostCSS Nesting and similar tools.


My argument wasn't that some random PostCSS plugin should be given equal or similar weight as a browser implementation. Only that we "obfuscate" adoption of a feature in source code by transpiling it. But I agree that this is actually a good thing in this scenario.

Real usage in browsers will be extremely low because anyone not writing a demo on nesting itself will use a transpiler. And these tools, as you said, have semver, ...


hehe, this escalated nicely, sorry about that :)

c-smile commented 1 year ago

Alternative solution to nesting that does not change existing syntax grammar.

1 To introduce style blocks :

@set  MyComponent {
   :root { border: 1px solid; } /* root element of the set, or :scope */
   :root > div { ... } /* immediate child of the root */
   div { ... } /* any element inside the root */
}

and one property named style-set: ... nameOfTheSet ...:

widget.myComponent {
   style-set: MyComponent;
}

style-set is an inheritable by default property.

Element that has explicit declaration of style-set is the root.

This variant solves problem of CSS declarations modularity 1) without breaking changes and 2) that is more valuable IMO, reduces complexity of style resolution - selectors inside the block are checked only for elements that have style-set defined. Therefore @set declaration will not increase time needed for style resolution.

As a bonus we can add @styleset attribute to HTML

<widget styleset="cssfile#MyComponent"> ... </widget>

to make declarations local to components.

tabatkins commented 1 year ago

Fun new information: it turns out that Chrome's impl never actually supported &div in the first place - our impl never relaxed the restriction that type selectors have to be the first component of a compound selector. (There aren't any tests for this specific behavior right now...)

So there's zero compat issues from Chrome, at least, in returning to the previous restriction. (Thanks for tracking this info down, @andruud !)

css-meeting-bot commented 1 year ago

The CSS Working Group just discussed [css-nesting] Require `div&`, disallow `&div`, for Sass compat, and agreed to the following:

The full IRC log of that discussion <fantasai> TabAtkins: In previous version of Nesting, we relaxed restriction to starting with a tag selector in order to allow & at the front
<fantasai> ... which was required for previous parsing solutions
<fantasai> TabAtkins: SASS uses & as a textual substitution, so if you write &div, you're asking SASS to append the letters "div", so if your parent selector was ".foo" you get ".foodiv"
<fantasai> ... having this mismatch would be an annoying upgrade story for them, because this sort of concatenation is very heavily used
<fantasai> ... due to object-oriented class naming patterns
<fantasai> ... On the other hand, putting additional type selectors on the compound selector is exceedingly rare
<fantasai> ... she's heard the request only a few times
<fantasai> ... So this is very low priority for them
<fantasai> ... upshot of all this, is I suggest we remove the relaxed restriction that allows type selectors to not be at the front
<fantasai> ... restoring us to previous restriction, which requires the tag selector in front
<fantasai> ... then you can write div& but not &div
<fantasai> ... which protects that syntax space for SASS and related languages
<fantasai> TabAtkins: This also helps with some degree of migration
<fantasai> ... if they know it's an error, they can autocorrect to the right form
<fantasai> TabAtkins: I was initially uncertain of specifying this, if there is already usage of &div in Chrome or Safari
<fantasai> ... but apparently Chrome's implementation never relaxed that restriction so &div has been invalid this whole time
<fantasai> TabAtkins: so at least for Chrome, this isn't an issue, so making this invalid again would be fine
<fantasai> ... unclear about Safari I couldn't test
<fantasai> TabAtkins: So I propose to revert the syntax restrictions
<fantasai> ??: [missed]
<fantasai> ??: We don't have the same behavior as Chrome, so it would be a breaking change for us
<fantasai> astearns: Given that there is likely not that much content targetting Safari's current implementation, would you be ok with this change?
<fantasai> ??: Pretty sure there are zero websites targetting it, so won't break the Web
<oriol> q+
<fantasai> TabAtkins: Then I ask for a resolution
<dbaron> s/??/matthieu/
<fantasai> s/??/mattieu/
<fantasai> s/??/mattieu/
<astearns> ack oriol
<emilio> +1 to keep that restriction, fwiw on Firefox's impl I never implemented it either
<oriol> Lety me reconnect
<fantasai> jensimmons: Curious to know what miriam thinks about this issue
<fantasai> miriam: I think this is a good idea, for the reasons Tab listed
<fantasai> ... I am not on the internals of everything that Natalie is conerned about here, but was part of the conversation with Tab and agree this is the direction to go to minorly limit a problem
<astearns> ack oriol
<fantasai> oriol: I'm opposed to the restriction, but not clear to me how exactly it's helping. In SASS the behavior is something else, and ppl are using that behavior, if they switch to Nesting they will have to adapt somehow anyway
<dbaron> s/mattieu/matthieu/
<fantasai> ... I'm not sure whether it's invalid or it means something different, if it is that relevant to people
<dbaron> s/mattieu/matthieu/
<fantasai> TabAtkins: As much as possible, SASS tries not to interpret valid CSS differently as how browsers would interpret it
<fantasai> ... It is helpful if we put it as invalid syntax, so it is definitely not something that would mean something in the browser
<emilio> q+
<fantasai> ... It's not strictly necessary, because they'll have compat pain anyway, but a long-term goal here, is that as long as author is not using SASS-specific features, they want to emit native CSS in the future
<astearns> ack emilio
<fantasai> ... being certain about using CSS-compatible syntax or invalid syntax that is SASS-interpreted is a goal
<fantasai> emilio: If we ever expose the final selector somehow, it would be weird if this couldn't be serialized in anyway
<fantasai> ... so I support not allowing &div
<fantasai> emilio: In particular, if devtools wanted to show the final selector that this element matched, you want to see something useful
<fantasai> ... if you write &div, you can't just expand it
<fantasai> emilio: so I would prefer to avoid this special case
<fantasai> astearns: Other comments or conerns?
<fantasai> s/conerns/concerns/
<fantasai> TabAtkins: Proposed to remove relaxation of type selector rules, keep current rule that type selector must be first in a compound selector
<fantasai> astearns: objections?
<fantasai> RESOLVED: type selector remains required first; &div is invalid
tabatkins commented 1 year ago

All right, spec should be updated to this condition now. We'll need to update the tests (and probably add some additional ones).