Conversation
| // Check that not selectors were added | ||
| const notSelectorMatches = result.css.match(/:where\(body:not\(\[data-breakpoint-preview-mode\]\)\)/g); | ||
| assert.ok(notSelectorMatches && notSelectorMatches.length >= 2, | ||
| 'Should have not selectors for both media queries'); |
There was a problem hiding this comment.
I think it's more robust / readable to test the entire css output. Also there is a run method that takes the input and output and verify, you should use it like in other tests.
| [ containerBodySelector ] | ||
| ) | ||
| // For nested media queries | ||
| // For nested media queries |
There was a problem hiding this comment.
duplicated comment, + I think you don't need any, the variable name isNested is clear enough.
| // Convert viewport units if needed | ||
| let value = containerDecl.value; | ||
| if (Object.keys(unitConverter.units) | ||
| .some(unit => value.includes(unit))) { |
There was a problem hiding this comment.
A variable could be more readable here.
There was a problem hiding this comment.
I think I made the change you wanted.
| }); | ||
|
|
||
| // Convert viewport units if needed | ||
| let value = containerDecl.value; |
There was a problem hiding this comment.
Why defining value outside of the scope of the if statement if you use it only inside it. Should be defined inside the if.
| .addTargetToSelectors( | ||
| originalSelector, | ||
| conditionalNotSelector | ||
| ), |
There was a problem hiding this comment.
This won't work, unfortunately this where mechanism, which is more the mechanism allowing to run media queries only when not in mobile preview mode (we check the body attribute) using conditionalNotSelector. So this mechanism doesn't work with nested css, I think I definitely missed that possibility..
Since it checks by default some body attribute it cannot be nested in other selectors or it will just not work..
I allowed myself to write a test case to show you ideally how it should look like, I intentionally used a deeply (2 levels) nested selector. The check on the body attribute should be done at the first level, so I think we should somehow copy the full container (all levels), add the check on the body selector to the first level and copy the rest as it is, example:
.foo {
.bar {
.inside {
@media screen and (max-width: 500px) {
margin: 2rem;
}
}
}`;would become
:where(body:not([data-breakpoint-preview-mode])) .foo,
:where(body:not([data-breakpoint-preview-mode])).foo {
.bar {
.inside {
@media screen and (max-width: 500px) {
margin: 2rem;
}
}
}`;instead of
.foo {
.bar {
:where(body:not([data-breakpoint-preview-mode])).inside,
:where(body:not([data-breakpoint-preview-mode])) inside {
@media screen and (max-width: 500px) {
margin: 2rem;
}
}
}`;Because this last case won't work. Of course normal rules should stay as they are in the original css without the where target. and of course container query should still be added..
ValJed
left a comment
There was a problem hiding this comment.
Looks doing what is expected globally 👍🏼
Few concerns, seeing a duplicated media query node in the tests.
Main function starts to be huge my be splitted in the future.
I don't get all the logic for the nested part. Given the complexity I think we should cover all cases we have in mind in the tests, can be improved later too.
| value = unitConverter.convertUnitsInExpression(value); | ||
| const convertibleUnits = Object.keys(unitConverter.units); | ||
|
|
||
| if (convertibleUnits.some(unit => containerDecl.value.includes(unit))) { |
There was a problem hiding this comment.
I meant extracting all the logic in a nicely names variable for readability like:
let value = containerDecl.value;
const containsValuesToConvert = Object.keys(value)
.some(unit => value.includes(unit));
if (containsValuesToConvert) { ... }But I'm good with this version, looks readable enough.
| let rootRule = atRule.parent; | ||
| while (rootRule && rootRule.parent && rootRule.parent.type === 'rule') { | ||
| rootRule = rootRule.parent; | ||
| } |
|
|
||
| // Find the root nesting level | ||
| let rootParent = parentRule; | ||
| let nestingDepth = 0; |
There was a problem hiding this comment.
[Optional] Detail but could be a boolean since you only check if it's equal to 0.
Something like:
let oneLevelNesting = true;Set to false in the while loop.
| } | ||
| @media (width > 800px) { | ||
| top: 5rem; | ||
| } |
There was a problem hiding this comment.
I don't think this media query node should be duplicated?
There was a problem hiding this comment.
Well crap. I missed that doing a cut-and-paste from the input. The test passes because of a side effect of cssnano. Looking at my nesting logic, I'm cloning at the wrong point. Test fixed and will fix the nesting logic.
| "test": "npm run eslint && npm run mocha", | ||
| "mocha": "mocha", | ||
| "eslint": "eslint --ext .js,.vue ." | ||
| "eslint": "eslint --fix --ext .js,.vue ." |
There was a problem hiding this comment.
I don't think we want this command to actually fix code, since it runs in the CI env it might fix code in a container and not inform about linting error. You can configure your IDE to fix linting errors on save.
There was a problem hiding this comment.
I tried several times to set up VSCode to fix linting errors, but it does this odd thing where it gets rid of spaces inside []. I put that --fix there as a temporary work-around and forgot to remove it. Removed now.
There was a problem hiding this comment.
Maybe does not read properly your eslint config..
| clonedNode.walkAtRules('media', (mediaRule) => { | ||
| mediaRule[processed] = true; | ||
| }); | ||
| conditionalRule.append(clonedNode); |
There was a problem hiding this comment.
I have doubts about this block, we mark rules as processed but shouldn't we go through each node individually to handle everything?
I don't get all this part of the code nicely so prefer to ask.
There was a problem hiding this comment.
No problem! Let me walk through what's happening here for the multi-level nesting case:
Context: We have deeply nested CSS like:
.foo {
.bar {
.inside {
@media (max-width: 500px) { margin: 2rem; }
}
}
}Goal: Create a conditional wrapper at the root level (.foo) that contains the media query, so it only applies when the breakpoint preview is OFF.
What this code does:
We clone the entire nested structure from .foo down
During cloning, we remove any media queries that aren't the one currently being processed (to avoid duplicates)
We mark the current media query as [processed] = true so when the plugin encounters it again during traversal, it skips it
We append this cloned structure to the conditional wrapper
We mark as processed because after we clone and append the media query to the wrapper, PostCSS will walk through that new wrapper structure. Without the [processed] flag, the plugin would try to process those media queries again, creating infinite loops or duplicate container queries.
The walkAtRules('media', ...) ensures that not just the top-level media query but any nested ones are also marked as processed.
Does that clarify the logic? Happy to explain any specific part in more detail!
The updates above this code prevent duplicate media queries by only cloning the current media query being processed, not all media queries in the nested structure.
There was a problem hiding this comment.
I understand, and thanks!
So the [processed] flag is the key to preventing infinite recursion - when you clone the nested structure and append it to the conditional wrapper, PostCSS will naturally walk through that new cloned tree during its traversal. Without marking those cloned media queries as processed, the plugin would encounter them again and try to process them a second time, which would create duplicates or even infinite loops.
And by using walkAtRules('media', ...) on the cloned structure, you're ensuring that not just the immediate media query but any deeply nested ones within the clone are also flagged, so none of them get processed again when PostCSS continues its walk.
The combination of selective cloning (only the current media query) plus the processed flag is what keeps everything clean. Makes total sense - thanks for the detailed explanation!
| break; | ||
| } | ||
| prevNode = prevNode.prev(); | ||
| } |
There was a problem hiding this comment.
What is for the prevNode, its the root one that gets the :where(body:not?
From what I understand if this wrapper is already here we don't add it again, shouldn't be checked at the rootSelector level directly?
There was a problem hiding this comment.
The loop searches for an existing conditional wrapper created by a previously processed media query. Since wrappers are inserted with parentRule.before(conditionalRule) (or rootParent.before()), they become previous siblings. We need to check all previous siblings, not just the immediate one, because there could be multiple wrappers or other rules between them.
| rootParent.before(conditionalRule); | ||
| debugUtils.log('Created new conditional wrapper at root level', atRule); | ||
| } else { | ||
| // Wrapper exists, add media query to matching nested location |
There was a problem hiding this comment.
I suppose this is the part where we duplicate media queries maybe.
There was a problem hiding this comment.
The duplication issue was in the cloning logic above this line (inside rootParent.each), where all media queries were being cloned. The fix uses isSameMediaQuery to remove media queries that aren't the current one. The rootParent.before(conditionalRule) line just inserts the wrapper - it doesn't cause duplication. The duplication bug has been fixed and all tests are passing.
| } | ||
|
|
||
| // Remove from original | ||
| atRule.remove(); |
There was a problem hiding this comment.
Remove what from original, the atRule node?
There was a problem hiding this comment.
Yeah, that is correct. Otherwise, we would have the original and the clone, which could cause specificity conflicts.
| } else { | ||
| mediaRule[processed] = true; | ||
| } | ||
| }); |
There was a problem hiding this comment.
Shouldn't we check if the media has already been processed.
| clonedNode.walkAtRules('media', (mediaRule) => { | ||
| mediaRule[processed] = true; | ||
| }); | ||
| conditionalRule.append(clonedNode); |
There was a problem hiding this comment.
I understand, and thanks!
So the [processed] flag is the key to preventing infinite recursion - when you clone the nested structure and append it to the conditional wrapper, PostCSS will naturally walk through that new cloned tree during its traversal. Without marking those cloned media queries as processed, the plugin would encounter them again and try to process them a second time, which would create duplicates or even infinite loops.
And by using walkAtRules('media', ...) on the cloned structure, you're ensuring that not just the immediate media query but any deeply nested ones within the clone are also flagged, so none of them get processed again when PostCSS continues its walk.
The combination of selective cloning (only the current media query) plus the processed flag is what keeps everything clean. Makes total sense - thanks for the detailed explanation!
Summary
Summarize the changes briefly, including which issue/ticket this resolves. If it closes an existing Github issue, include "Closes #[issue number]"
This PR fixes two issues. One, with the Apollo Astro starter kits, switching on breakpoint preview would cause all the fonts to use the base system font. This was due to how the
:where()pseudoclass was wrapping elements and reducing their specificity to0. Two, a user noted that responsive Tailwind classes were not working in breakpoint preview. This was due to how Tailwind 4.x nests media queries. Closes PRO-8513What are the specific steps to test this change?
For example:
npx astro add tailwindin astarter-kit-astro-apolloproject.import "../styles/global.css"to thefrontend/src/[...slug].astrofile.frontend/src/templates/HomePage.astroadd the following above theAposArea:frontendproject.What kind of change does this PR introduce?
(Check at least one)
Make sure the PR fulfills these requirements:
If adding a new feature without an already open issue, it's best to open a feature request issue first and wait for approval before working on it.
Other information: