Skip to content

Commit 1ba1a2a

Browse files
committed
[babel-plugin] group css rules by media query
1 parent 71f164c commit 1ba1a2a

File tree

2 files changed

+188
-42
lines changed

2 files changed

+188
-42
lines changed

packages/@stylexjs/babel-plugin/__tests__/transform-process-test.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,5 +425,118 @@ describe('@stylexjs/babel-plugin', () => {
425425
.x57uvma.x57uvma, .x57uvma.x57uvma:root{--large-x1ec7iuc:20px;--medium-xypjos2:10px;--small-x19twipt:5px;}"
426426
`);
427427
});
428+
429+
test('media query grouping - rules with same media query are grouped together', () => {
430+
const { _code, metadata } = transform(
431+
`
432+
import * as stylex from '@stylexjs/stylex';
433+
export const styles = stylex.create({
434+
container: {
435+
'@media (max-width: 768px)': {
436+
width: '10px',
437+
height: '20px',
438+
},
439+
color: {
440+
default: 'black',
441+
'@media (max-width: 308px)': 'white',
442+
'@media (max-width: 768px)': 'red',
443+
},
444+
backgroundColor: {
445+
default: 'white',
446+
'@media (max-width: 768px)': 'blue',
447+
'@media (min-width: 1024px)': 'yellow',
448+
},
449+
fontSize: {
450+
default: '16px',
451+
'@media (max-width: 768px)': '14px',
452+
},
453+
padding: {
454+
default: '10px',
455+
'@media (min-width: 1024px)': '20px',
456+
},
457+
margin: {
458+
default: '5px',
459+
'@media (min-width: 1024px)': '10px',
460+
}
461+
}
462+
});
463+
`,
464+
);
465+
466+
const css = stylexPlugin.processStylexRules(metadata);
467+
468+
expect(css).toMatchInlineSnapshot(`
469+
":root, .xsg933n{--blue-xpqh4lw:blue;}
470+
:root, .xbiwvf9{--small-x19twipt:2px;--medium-xypjos2:4px;--large-x1ec7iuc:8px;}
471+
.margin-x16zck5j:not(#\\#){margin:5px}
472+
.padding-x7z7khe:not(#\\#){padding:10px}
473+
.backgroundColor-x12peec7:not(#\\#):not(#\\#){background-color:white}
474+
.color-x1mqxbix:not(#\\#):not(#\\#){color:black}
475+
.fontSize-x1j61zf2:not(#\\#):not(#\\#){font-size:16px}
476+
@media (min-width: 1024px){
477+
.margin-x1nff4mz.margin-x1nff4mz:not(#\\#){margin:10px}
478+
.padding-x1glw0n9.padding-x1glw0n9:not(#\\#){padding:20px}
479+
.backgroundColor-xkbfoqe.backgroundColor-xkbfoqe:not(#\\#):not(#\\#){background-color:yellow}
480+
}
481+
@media (max-width: 768px){
482+
.backgroundColor-xycim1f.backgroundColor-xycim1f:not(#\\#):not(#\\#){background-color:blue}
483+
.color-x9i7o1z.color-x9i7o1z:not(#\\#):not(#\\#){color:red}
484+
.fontSize-xt5ov9y.fontSize-xt5ov9y:not(#\\#):not(#\\#){font-size:14px}
485+
.height-x12z8348.height-x12z8348:not(#\\#):not(#\\#):not(#\\#){height:20px}
486+
.width-x7lwmry.width-x7lwmry:not(#\\#):not(#\\#):not(#\\#){width:10px}
487+
}
488+
@media (max-width: 308px){
489+
.color-x1760m8v.color-x1760m8v:not(#\\#):not(#\\#){color:white}
490+
}"
491+
`);
492+
});
493+
494+
test('media query grouping with layers - rules with same media query are grouped together', () => {
495+
const { _code, metadata } = transform(
496+
`
497+
import * as stylex from '@stylexjs/stylex';
498+
export const styles = stylex.create({
499+
container: {
500+
color: {
501+
default: 'black',
502+
'@media (max-width: 768px)': 'red',
503+
},
504+
backgroundColor: {
505+
default: 'white',
506+
'@media (max-width: 768px)': 'blue',
507+
},
508+
fontSize: {
509+
default: '16px',
510+
'@media (max-width: 768px)': '14px',
511+
}
512+
}
513+
});
514+
`,
515+
{
516+
useLayers: true,
517+
},
518+
);
519+
520+
const css = stylexPlugin.processStylexRules(metadata, true);
521+
522+
expect(css).toMatchInlineSnapshot(`
523+
"
524+
@layer priority1, priority2;
525+
@layer priority1{
526+
:root, .xsg933n{--blue-xpqh4lw:blue;}
527+
:root, .xbiwvf9{--small-x19twipt:2px;--medium-xypjos2:4px;--large-x1ec7iuc:8px;}
528+
}
529+
@layer priority2{
530+
.backgroundColor-x12peec7{background-color:white}
531+
.color-x1mqxbix{color:black}
532+
.fontSize-x1j61zf2{font-size:16px}
533+
@media (max-width: 768px){
534+
.backgroundColor-xycim1f.backgroundColor-xycim1f{background-color:blue}
535+
.color-x9i7o1z.color-x9i7o1z{color:red}
536+
.fontSize-xt5ov9y.fontSize-xt5ov9y{font-size:14px}
537+
}
538+
}"
539+
`);
540+
});
428541
});
429542
});

packages/@stylexjs/babel-plugin/src/index.js

Lines changed: 75 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -470,58 +470,91 @@ function processStylexRules(
470470
';\n'
471471
: '';
472472

473-
const collectedCSS = grouped
473+
const globalMediaQueryGroups = new Map();
474+
const globalNonMediaRules = [];
475+
476+
const perGroupOutput = grouped
474477
.map((group, index) => {
475478
const pri = group[0][2];
476-
const collectedCSS = Array.from(
477-
new Map(group.map(([a, b]) => [a, b])).values(),
478-
)
479-
.flatMap((rule) => {
480-
const { ltr, rtl } = rule;
481-
let ltrRule = ltr,
482-
rtlRule = rtl;
483-
484-
if (!useLayers) {
485-
ltrRule = addSpecificityLevel(ltrRule, index);
486-
rtlRule = rtlRule && addSpecificityLevel(rtlRule, index);
487-
}
479+
const rules = Array.from(new Map(group.map(([a, b]) => [a, b])).values());
480+
481+
const mediaQueryGroups = useLayers ? new Map() : globalMediaQueryGroups;
482+
const nonMediaRules = useLayers ? [] : globalNonMediaRules;
483+
484+
rules.forEach((rule) => {
485+
const { ltr, rtl } = rule;
486+
let ltrRule = ltr,
487+
rtlRule = rtl;
488488

489-
// check if the selector looks like .xtrlmmh, .xtrlmmh:root
490-
// if so, turn it into .xtrlmmh.xtrlmmh, .xtrlmmh.xtrlmmh:root
491-
// This is to ensure the themes always have precedence over the
492-
// default variable values
493-
ltrRule = ltrRule.replace(
489+
if (!useLayers) {
490+
ltrRule = addSpecificityLevel(ltrRule, index);
491+
rtlRule = rtlRule && addSpecificityLevel(rtlRule, index);
492+
}
493+
494+
ltrRule = ltrRule.replace(
495+
/\.([a-zA-Z0-9]+), \.([a-zA-Z0-9]+):root/g,
496+
'.$1.$1, .$1.$1:root',
497+
);
498+
if (rtlRule) {
499+
rtlRule = rtlRule.replace(
494500
/\.([a-zA-Z0-9]+), \.([a-zA-Z0-9]+):root/g,
495501
'.$1.$1, .$1.$1:root',
496502
);
497-
if (rtlRule) {
498-
rtlRule = rtlRule.replace(
499-
/\.([a-zA-Z0-9]+), \.([a-zA-Z0-9]+):root/g,
500-
'.$1.$1, .$1.$1:root',
501-
);
502-
}
503+
}
503504

504-
return rtlRule
505-
? enableLTRRTLComments
506-
? [
507-
`/* @ltr begin */${ltrRule}/* @ltr end */`,
508-
`/* @rtl begin */${rtlRule}/* @rtl end */`,
509-
]
510-
: [
511-
addAncestorSelector(ltrRule, "html:not([dir='rtl'])"),
512-
addAncestorSelector(rtlRule, "html[dir='rtl']"),
513-
]
514-
: [ltrRule];
515-
})
516-
.join('\n');
517-
518-
// Don't put @property, @keyframe, @position-try in layers
519-
return useLayers && pri > 0
520-
? `@layer priority${index + 1}{\n${collectedCSS}\n}`
521-
: collectedCSS;
505+
const processedRules = rtlRule
506+
? enableLTRRTLComments
507+
? [
508+
`/* @ltr begin */${ltrRule}/* @ltr end */`,
509+
`/* @rtl begin */${rtlRule}/* @rtl end */`,
510+
]
511+
: [
512+
addAncestorSelector(ltrRule, "html:not([dir='rtl'])"),
513+
addAncestorSelector(rtlRule, "html[dir='rtl']"),
514+
]
515+
: [ltrRule];
516+
517+
processedRules.forEach((processedRule) => {
518+
const mediaQueryMatch = processedRule.match(
519+
/^(@media[^{]+)\{([\s\S]*)\}$/m,
520+
);
521+
if (mediaQueryMatch) {
522+
const [, rawMQ, innerContent] = mediaQueryMatch;
523+
const mq = rawMQ.trim().replace(/\s+/g, ' ');
524+
if (!mediaQueryGroups.has(mq)) mediaQueryGroups.set(mq, []);
525+
const arr = mediaQueryGroups.get(mq);
526+
if (arr) arr.push(innerContent.trim());
527+
} else {
528+
nonMediaRules.push(processedRule);
529+
}
530+
});
531+
});
532+
533+
if (useLayers) {
534+
const allRules = [
535+
...nonMediaRules,
536+
...Array.from(mediaQueryGroups.entries())
537+
.filter(([, inner]) => inner.length > 0)
538+
.map(([mq, inner]) => `${mq}{\n${inner.join('\n')}\n}`),
539+
];
540+
const collected = allRules.join('\n');
541+
return pri > 0
542+
? `@layer priority${index + 1}{\n${collected}\n}`
543+
: collected;
544+
}
545+
return '';
522546
})
523547
.join('\n');
524548

549+
const collectedCSS = useLayers
550+
? perGroupOutput
551+
: [
552+
...globalNonMediaRules,
553+
...Array.from(globalMediaQueryGroups.entries())
554+
.filter(([, inner]) => inner.length > 0)
555+
.map(([mq, inner]) => `${mq}{\n${inner.join('\n')}\n}`),
556+
].join('\n');
557+
525558
return header + collectedCSS;
526559
}
527560

0 commit comments

Comments
 (0)