-
Notifications
You must be signed in to change notification settings - Fork 14
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
Add compatibility for Stylis v4 #14
base: main
Are you sure you want to change the base?
Conversation
It would be nice if you could rebase that on top of @tpict changes or include him as co-author of this. Thank you very much for this - I like that you have included additional Emotion-based tests here. I won't have time to review this right away (definitely not this week) - could you ping me about this in the next week? |
Co-authored-by: Tom Picton <tom@tompicton.com>
Of course, I added @tpict as a co-author on the commit. I'll ping again sometime next week. |
Thanks very much for picking this up. Funnily enough I just found a bug in my implementation that's present in this version too. The extra scope is added to the right hand side of the child combinator rather than the left, e.g. .parent > .child {
color: red;
} becomes .parent > #my-extra-scope .child {
color: red;
} rather than #my-extra-scope .parent > .child {
color: red;
} |
This looks like a critical problem that should be addressed before merging |
Thanks for finding this. I'll add unit tests and start looking for a fix. |
@tpict do you have an example test case that breaks this implementation? This test passes:
|
Ah right, this is specifically when using the |
Hmm this is also passing:
I haven't tried adding tests in the context of emotion yet, so I can check that next. |
I added unit tests covering different child selector cases and it's still working as expected. |
I'll try to get you a working (broken) example tomorrow–in the mean time I can tell you that the component I'm seeing the issue with looks something like const MyComponent: React.FC = function() {
return <span css={{
[`.${someVariable}:focus ~ &, .${someOtherVariable}:focus > &`]: {
outlineColor: "red",
},
}}
/>;
} |
Alright, I've had some time to dig into this and the problem seems to be with an interaction with the The broken selector example I'm using is I added logging for the current element before and after the plugin logic to see how it's being affected, and you can see how in the input changes with/without Input CSS: .some-class {
.class-one:focus ~ &, .class-two:focus > & {
outlineColor: red;
}
} Before, first element: {
value: '.some-class',
root: null,
parent: null,
type: 'rule',
props: [ '.some-class' ],
children: [],
line: 1,
column: 14,
length: 0,
return: ''
} After, first element: {
value: '.some-class',
root: null,
parent: null,
type: 'rule',
props: [ '#my-scope .some-class' ],
children: [],
line: 1,
column: 14,
length: 0,
return: ''
} Before, second element w/o compat: {
value: '.class-one:focus~&,.class-two:focus>&',
root: null,
parent: {
value: '#my-scope .some-class',
root: null,
parent: null,
type: 'rule',
props: [ '#my-scope .some-class' ],
children: [],
line: 1,
column: 14,
length: 0,
return: ''
},
type: 'rule',
props: [ '.class-one:focus~.some-class', '.class-two:focus>.some-class' ],
children: [
{
value: 'outlineColor:red;',
root: [Circular],
parent: [Circular],
type: 'decl',
props: 'outlineColor',
children: 'red',
line: 3,
column: 29,
length: 12,
return: ''
}
],
line: 2,
column: 53,
length: 31,
return: ''
} Before, second element w/ compat: {
value: '.class-one:focus~&,.class-two:focus>&',
root: null,
parent: {
value: '#my-scope .some-class',
root: null,
parent: null,
type: 'rule',
props: [ '#my-scope .some-class' ],
children: [],
line: 1,
column: 14,
length: 0,
return: ''
},
type: 'rule',
props: [ '.class-one:focus~#my-scope .some-class', '.class-two:focus>#my-scope .some-class' ],
children: [
{
value: 'outlineColor:red;',
root: [Circular],
parent: [Circular],
type: 'decl',
props: 'outlineColor',
children: 'red',
line: 3,
column: 29,
length: 12,
return: ''
}
],
line: 2,
column: 53,
length: 31,
return: ''
} You can see that with I think I have a solution if it ran after It would also be helpful to understand better what |
It's also worth mentioning
is broken, but both
and
work. So it's specifically the combination of the psuedo-class, parent reference, and multiple selectors that's broken here. |
Stylis v3 behavior was to interpret this: .scope {
:hover {
color: hotpink;
}
} as .scope:hover {
color: hotpink;
} but this was not "correct" (according to the semantics coined by SCSS & co) because it should be: .scope :hover {
color: hotpink;
} So this got fixed in Stylis v4 but in Emotion we've wanted to avoid this being a breaking change and thus we've introduced this "compat" plugin that tries to "fixup" the new behavior to the old one. Could you add a failing test case to this PR? I will try to look into this to understand better why this breaks. |
Thanks, that context is helpful. I just pushed the changes with a broken unit test for each suite. The current version of the plugin is broken in both, due to the logic that tries to work around
to
Passes all tests for the non-Emotion suite. |
I wonder if a reasonable work-around might be to add a feature to Emotion that allows turning the |
src/index.js
Outdated
if ( | ||
!element.parent || | ||
(element.props.length === 1 && element.value.charCodeAt(0) !== 58) || | ||
!element.length | ||
) { | ||
element.props = element.props | ||
.map((prop) => scopes.map((scope) => scope + prop)) | ||
.reduce((scopesArray, scope) => scopesArray.concat(scope), []) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that this if statement here doesn't make sense, at least for the most part. Especially parts focusing on anything else than element.parent
seems dubious - they are trying to battle the logic from the compat plugin, right?
If I just apply this change:
if ( | |
!element.parent || | |
(element.props.length === 1 && element.value.charCodeAt(0) !== 58) || | |
!element.length | |
) { | |
element.props = element.props | |
.map((prop) => scopes.map((scope) => scope + prop)) | |
.reduce((scopesArray, scope) => scopesArray.concat(scope), []) | |
} | |
element.props = element.props | |
.map((prop) => scopes.map((scope) => scope + prop)) | |
.reduce((scopesArray, scope) => scopesArray.concat(scope), []) |
then "vanilla" tests still pass and that is good. We, of course, still need to figure out how to fix the compatibility with compat plugin though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
q: what if we'd serialize the current rule node and set its .return
property with the scope prefixed to the output? maybe we don't have to manipulate element.props
at all?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this change makes sense to me. It matches up with @tpict's original implementation, before the changes to account for compat
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, the serializing advice could work out if the received callback
would be pure but in the case of Emotion it contains the rulesheet
plugin so we can't just call into it freely
Maybe worth opening a discussion in the Stylis problem about this? I'm trying to think through how those 2 plugins can coexist but so far I didn't get an epiphany with the solution 😬
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, commented before seeing that second message. Can you explain more how the .return
prop is used? Are there any good docs for writing plugins outside of the examples in Middleware.js?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, this is a little bit fuzzy to me but IIRC .return
holds the "final" stringified value of a given node. I think though that this is a little bit over my head - it would be best to create a discussion in the Stylis repo so maybe we could get some guidance from Sultan for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, just started a discussion about it in Stylis: thysultan/stylis#281
Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
Thanks both of you for investigating! |
Hi @Andarist, anything new thoughts on how this could potentially work with the compat plugin? |
I wonder if one viable alternative might be to change Emotion to get rid of the compat plugin and instead preprocess the style object before passing it to stylis. It seems like that would just require traversing the object structure and changing any keys that start with For that matter, if Emotion generally supported style object proprocessers and we implemented |
Here is my basic implementation, export default function createExtraScopePlugin(scope) {
return (element) => {
if (element.type !== 'rule') {
return
}
if (element.root?.type === '@keyframes') {
return
}
element.props = element.props
.map((prop) => (prop.startsWith(':root') || prop.startsWith(scope)) ? prop : `${scope} ${prop}`)
}
} I adapted it for my usecase, where i needed to have two components inside a cms page so I scoped the css. |
This is based on the changes from #11, just updated with master from the base repo and with additional unit tests.
Thanks @tpict for the initial implementation.