-
Notifications
You must be signed in to change notification settings - Fork 0
Pro 8513 where issue #11
Changes from all commits
aa8dac3
a96475d
a72fc32
30aa0cb
c0e26e5
1d9e0c4
636ae2a
227d6cd
daa75fe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,8 +46,8 @@ const plugin = (opts = {}) => { | |
| ...options | ||
| }); | ||
| const ruleProcessor = createRuleProcessor({ | ||
| unitConverter, | ||
| ...options | ||
| ...options, | ||
| unitConverter | ||
| }); | ||
| const selectorHelper = createSelectorHelper({ modifierAttr }); | ||
|
|
||
|
|
@@ -57,6 +57,11 @@ const plugin = (opts = {}) => { | |
| // Flag to track if container context like sticky has been added | ||
| let hasAddedContainerContext = false; | ||
|
|
||
| const isSameMediaQuery = (mq1, mq2) => { | ||
| return mq1.params === mq2.params && | ||
| mq1.source?.start?.line === mq2.source?.start?.line; | ||
| }; | ||
|
|
||
| /** | ||
| * Adds a container context with `position: relative` and `contain: layout` if required. | ||
| * | ||
|
|
@@ -171,10 +176,17 @@ const plugin = (opts = {}) => { | |
|
|
||
| AtRule: { | ||
| media(atRule, helpers) { | ||
| debugUtils.logMediaQuery(atRule, 'START'); | ||
|
|
||
| if (atRule[processed]) { | ||
| debugUtils.log('Skipping already processed media query', atRule); | ||
| return; | ||
| } | ||
|
|
||
| // Check if this media query is nested inside a rule | ||
| const isNested = atRule.parent?.type === 'rule'; | ||
| debugUtils.log(`Media query is ${isNested ? 'NESTED' : 'TOP-LEVEL'}`, atRule); | ||
|
|
||
| let hasNotSelector = false; | ||
| atRule.walkRules(rule => { | ||
| if (rule.selector.includes(conditionalNotSelector)) { | ||
|
|
@@ -183,79 +195,280 @@ const plugin = (opts = {}) => { | |
| }); | ||
|
|
||
| if (hasNotSelector) { | ||
| debugUtils.log('Skipping - already has not selector', atRule); | ||
| atRule[processed] = true; | ||
| return; | ||
| } | ||
|
|
||
| const conditions = mediaProcessor.getMediaConditions(atRule); | ||
| debugUtils.log(`Extracted conditions: ${JSON.stringify(conditions)}`, atRule); | ||
|
|
||
| if (conditions.length > 0) { | ||
| debugUtils.stats.mediaQueriesProcessed++; | ||
|
|
||
| // Create container version first | ||
| const containerConditions = | ||
| mediaProcessor.convertToContainerConditions(conditions); | ||
|
|
||
| debugUtils.log(`Container conditions: ${containerConditions}`, atRule); | ||
|
|
||
| if (containerConditions) { | ||
| debugUtils.log('Creating container query...', atRule); | ||
|
|
||
| const containerQuery = new helpers.AtRule({ | ||
| name: 'container', | ||
| params: containerConditions, | ||
| source: atRule.source, | ||
| from: helpers.result.opts.from | ||
| }); | ||
|
|
||
| // Clone and process rules for container query - keep selectors clean | ||
| atRule.walkRules(rule => { | ||
| const containerRule = rule.clone({ | ||
| source: rule.source, | ||
| from: helpers.result.opts.from, | ||
| selector: selectorHelper.updateBodySelectors( | ||
| rule.selector, | ||
| [ containerBodySelector ] | ||
| ) | ||
| }); | ||
|
|
||
| ruleProcessor.processDeclarations(containerRule, { | ||
| isContainer: true, | ||
| from: helpers.result.opts.from | ||
| // For nested media queries | ||
| if (isNested) { | ||
| debugUtils.log('Processing nested media query declarations...', atRule); | ||
|
|
||
| atRule.each(node => { | ||
| if (node.type === 'decl') { | ||
| debugUtils.log(` Processing declaration: ${node.prop}: ${node.value}`, atRule); | ||
|
|
||
| const containerDecl = node.clone({ | ||
| source: node.source, | ||
| from: helpers.result.opts.from | ||
| }); | ||
|
|
||
| // Convert viewport units if needed | ||
| let value = containerDecl.value; | ||
| if (Object.keys(unitConverter.units) | ||
| .some(unit => value.includes(unit))) { | ||
| value = unitConverter.convertUnitsInExpression(value); | ||
| containerDecl.value = value; | ||
| debugUtils.log(` Converted value to: ${value}`, atRule); | ||
| } | ||
|
|
||
| containerQuery.append(containerDecl); | ||
| } | ||
| }); | ||
|
|
||
| containerRule.raws.before = '\n '; | ||
| containerRule.raws.after = '\n '; | ||
| containerRule.walkDecls(decl => { | ||
| decl.raws.before = '\n '; | ||
| debugUtils.log(` Total declarations in container query: ${containerQuery.nodes?.length || 0}`, atRule); | ||
|
|
||
| const parentRule = atRule.parent; | ||
|
|
||
| // Find the root nesting level | ||
| let rootParent = parentRule; | ||
| let isSingleLevel = true; | ||
| while (rootParent.parent && rootParent.parent.type === 'rule') { | ||
| rootParent = rootParent.parent; | ||
| isSingleLevel = false; | ||
| } | ||
|
|
||
| // For single-level nesting (Tailwind case), use simple approach | ||
| if (isSingleLevel) { | ||
| // Add container query inside the parent rule, after the media query | ||
| atRule.after(containerQuery); | ||
|
|
||
| const originalSelector = parentRule.selector; | ||
|
|
||
| let conditionalRule = null; | ||
| let alreadyHasWrapper = false; | ||
|
|
||
| let prevNode = parentRule.prev(); | ||
| const targetSelector = selectorHelper | ||
| .addTargetToSelectors( | ||
| originalSelector, | ||
| conditionalNotSelector | ||
| ); | ||
|
|
||
| while (prevNode) { | ||
| if (prevNode.type === 'rule' && prevNode.selector === targetSelector) { | ||
| conditionalRule = prevNode; | ||
| alreadyHasWrapper = true; | ||
| debugUtils.log('Found existing conditional wrapper, reusing it', atRule); | ||
| break; | ||
| } | ||
| prevNode = prevNode.prev(); | ||
| } | ||
|
|
||
| if (!alreadyHasWrapper) { | ||
| conditionalRule = new helpers.Rule({ | ||
| selector: targetSelector, | ||
| source: parentRule.source, | ||
| from: helpers.result.opts.from | ||
| }); | ||
|
|
||
| parentRule.before(conditionalRule); | ||
| debugUtils.log('Created new conditional wrapper', atRule); | ||
| } | ||
|
|
||
| const clonedMedia = atRule.clone(); | ||
| clonedMedia[processed] = true; | ||
| conditionalRule.append(clonedMedia); | ||
| atRule.remove(); | ||
|
|
||
| debugUtils.log('Added conditional wrapper for nested media query', atRule); | ||
|
|
||
| } else { | ||
| // Multi-level nesting - hoist to root with full structure | ||
| const rootSelector = rootParent.selector; | ||
|
|
||
| // Check if wrapper exists at root level | ||
| let conditionalRule = null; | ||
| let alreadyHasWrapper = false; | ||
|
|
||
| let prevNode = rootParent.prev(); | ||
| const targetSelector = selectorHelper | ||
| .addTargetToSelectors( | ||
| rootSelector, | ||
| conditionalNotSelector | ||
| ); | ||
|
|
||
| while (prevNode) { | ||
| if (prevNode.type === 'rule' && prevNode.selector === targetSelector) { | ||
| conditionalRule = prevNode; | ||
| alreadyHasWrapper = true; | ||
| debugUtils.log('Found existing conditional wrapper, reusing it', atRule); | ||
| break; | ||
| } | ||
| prevNode = prevNode.prev(); | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is for the prevNode, its the root one that gets the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The loop searches for an existing conditional wrapper created by a previously processed media query. Since wrappers are inserted with |
||
|
|
||
| if (!alreadyHasWrapper) { | ||
| // Create wrapper with full nested structure | ||
| conditionalRule = new helpers.Rule({ | ||
| selector: targetSelector, | ||
| source: rootParent.source, | ||
| from: helpers.result.opts.from | ||
| }); | ||
|
|
||
| // Clone children of root parent (before container query is added) | ||
| rootParent.each(node => { | ||
| const clonedNode = node.clone(); | ||
|
|
||
| // Remove media queries that are NOT the current one being processed | ||
| clonedNode.walkAtRules('media', (mediaRule) => { | ||
| // Keep the current media query we're processing, remove others | ||
| if (mediaRule !== atRule && !isSameMediaQuery(mediaRule, atRule)) { | ||
| mediaRule.remove(); | ||
| } else { | ||
| mediaRule[processed] = true; | ||
| } | ||
| }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't we check if the media has already been processed. |
||
|
|
||
| // If this node itself is a media query | ||
| // and not the one we're processing, skip it | ||
| if (clonedNode.type === 'atrule' && clonedNode.name === 'media') { | ||
| if (!isSameMediaQuery(clonedNode, atRule)) { | ||
| return; // Skip appending | ||
| } | ||
| clonedNode[processed] = true; | ||
| } | ||
|
|
||
| conditionalRule.append(clonedNode); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have doubts about this block, we mark rules as processed but shouldn't we go through each node individually to handle everything?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No problem! Let me walk through what's happening here for the multi-level nesting case: .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. We clone the entire nested structure from .foo down 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 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand, and thanks! |
||
| }); | ||
|
|
||
| rootParent.before(conditionalRule); | ||
| debugUtils.log('Created new conditional wrapper at root level', atRule); | ||
| } else { | ||
| // Wrapper exists, add media query to matching nested location | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suppose this is the part where we duplicate media queries maybe.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The duplication issue was in the cloning logic above this line (inside |
||
| const targetInWrapper = conditionalRule.first; | ||
| if (targetInWrapper && targetInWrapper.type === 'rule') { | ||
| // Build path from parentRule to rootParent | ||
| const pathSelectors = []; | ||
| let current = parentRule; | ||
| while (current !== rootParent) { | ||
| pathSelectors.unshift(current.selector); | ||
| current = current.parent; | ||
| } | ||
|
|
||
| // Navigate to matching location in wrapper | ||
| let navNode = targetInWrapper; | ||
| for (const selector of pathSelectors) { | ||
| let found = false; | ||
| navNode.each(node => { | ||
| if (node.type === 'rule' && node.selector === selector) { | ||
| navNode = node; | ||
| found = true; | ||
| return false; | ||
| } | ||
| }); | ||
| if (!found) { | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| // Add cloned media to correct location | ||
| const clonedMedia = atRule.clone(); | ||
| clonedMedia[processed] = true; | ||
| navNode.append(clonedMedia); | ||
| } | ||
| } | ||
|
|
||
| // Remove from original | ||
| atRule.remove(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove what from original, the atRule node?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, that is correct. Otherwise, we would have the original and the clone, which could cause specificity conflicts. |
||
|
|
||
| // Now add container query to original (after cloning) | ||
| parentRule.append(containerQuery); | ||
|
|
||
| debugUtils.log('Added conditional wrapper for nested media query', atRule); | ||
| } | ||
|
|
||
| } else { | ||
| // Original logic for top-level media queries | ||
| atRule.walkRules(rule => { | ||
| const containerRule = rule.clone({ | ||
| source: rule.source, | ||
| from: helpers.result.opts.from, | ||
| selector: selectorHelper.updateBodySelectors( | ||
| rule.selector, | ||
| [ containerBodySelector ] | ||
| ) | ||
| }); | ||
|
|
||
| ruleProcessor.processDeclarations(containerRule, { | ||
| isContainer: true, | ||
| from: helpers.result.opts.from | ||
| }); | ||
|
|
||
| containerRule.raws.before = '\n '; | ||
| containerRule.raws.after = '\n '; | ||
| containerRule.walkDecls(decl => { | ||
| decl.raws.before = '\n '; | ||
| }); | ||
|
|
||
| containerQuery.append(containerRule); | ||
| }); | ||
|
|
||
| containerQuery.append(containerRule); | ||
| }); | ||
|
|
||
| // Add container query | ||
| atRule.after(containerQuery); | ||
| // Add container query | ||
| atRule.after(containerQuery); | ||
| } | ||
| } | ||
|
|
||
| // Now handle viewport media query modifications | ||
| // We want the original media query to get the not selector | ||
| atRule.walkRules(rule => { | ||
| // Skip if already modified with not selector | ||
| if (rule.selector.includes(conditionalNotSelector)) { | ||
| return; | ||
| } | ||
| if (!isNested) { | ||
| atRule.walkRules(rule => { | ||
| // Skip if already modified with not selector | ||
| if (rule.selector.includes(conditionalNotSelector)) { | ||
| return; | ||
| } | ||
|
|
||
| const viewportRule = rule.clone({ | ||
| source: rule.source, | ||
| from: helpers.result.opts.from | ||
| }); | ||
| const viewportRule = rule.clone({ | ||
| source: rule.source, | ||
| from: helpers.result.opts.from | ||
| }); | ||
|
|
||
| viewportRule.selector = selectorHelper.addTargetToSelectors( | ||
| rule.selector, | ||
| conditionalNotSelector | ||
| ); | ||
| viewportRule.selector = selectorHelper.addTargetToSelectors( | ||
| rule.selector, | ||
| conditionalNotSelector | ||
| ); | ||
|
|
||
| rule.replaceWith(viewportRule); | ||
| }); | ||
| rule.replaceWith(viewportRule); | ||
| }); | ||
| } | ||
| } else { | ||
| debugUtils.log('No conditions found - skipping', atRule); | ||
| } | ||
|
|
||
| // Only mark the atRule as processed after all transformations | ||
| atRule[processed] = true; | ||
| debugUtils.logMediaQuery(atRule, 'END'); | ||
| } | ||
| }, | ||
|
|
||
|
|
||
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.
👍🏼