Repository: facebook/stylex (2025-08-07T00:55:53Z)

Issue:
[style-value-parser] Support non-px values in media query transformer



Discussion:

Patch:
diff --git a/packages/style-value-parser/src/at-queries/media-query.js b/packages/style-value-parser/src/at-queries/media-query.js
index 5683e9ce5..a61addd93 100644
--- a/packages/style-value-parser/src/at-queries/media-query.js
+++ b/packages/style-value-parser/src/at-queries/media-query.js
@@ -363,6 +363,10 @@ function mergeIntervalsForAnd(
     height: [],
   };
 
+  const units: { [dim: string]: string } = {};
+
+  let hasAnyUnitConflicts = false;
+
   for (const rule of rules) {
     if (rule.type === 'not' && rule.rule.type === 'and') {
       const inner = rule.rule.rules;
@@ -402,6 +406,12 @@ function mergeIntervalsForAnd(
         isNumericLength(rule.value)
       ) {
         const val = rule.value as any;
+
+        if (intervals[dim].length === 0) {
+          units[dim] = val.unit;
+        } else if (units[dim] !== val.unit) {
+          hasAnyUnitConflicts = true;
+        }
         intervals[dim].push(
           rule.key === `min-${dim}`
             ? [val.value, Infinity]
@@ -416,6 +426,11 @@ function mergeIntervalsForAnd(
         isNumericLength(rule.rule.value)
       ) {
         const val = rule.rule.value as any;
+        if (intervals[dim].length === 0) {
+          units[dim] = val.unit;
+        } else if (units[dim] !== val.unit) {
+          hasAnyUnitConflicts = true;
+        }
         if (rule.rule.key === `min-${dim}`) {
           intervals[dim].push([-Infinity, val.value - epsilon]);
         } else {
@@ -447,9 +462,15 @@ function mergeIntervalsForAnd(
   }
 
   const result: Array<MediaQueryRule> = [];
+
+  if (hasAnyUnitConflicts) {
+    return rules;
+  }
+
   for (const dim of dimensions) {
     const dimIntervals = intervals[dim];
     if (dimIntervals.length === 0) continue;
+
     let lower: number = -Infinity;
     let upper: number = Infinity;
     for (const [l, u]: [number, number] of dimIntervals) {
@@ -463,14 +484,14 @@ function mergeIntervalsForAnd(
       result.push({
         type: 'pair',
         key: `min-${dim}`,
-        value: { value: lower, unit: 'px', type: 'integer' } as any,
+        value: { value: lower, unit: units[dim], type: 'integer' } as any,
       });
     }
     if (upper !== Infinity) {
       result.push({
         type: 'pair',
         key: `max-${dim}`,
-        value: { value: upper, unit: 'px', type: 'integer' } as any,
+        value: { value: upper, unit: units[dim], type: 'integer' } as any,
       });
     }
   }


Test patch:
diff --git a/packages/style-value-parser/src/at-queries/__tests__/media-query-transform-test.js b/packages/style-value-parser/src/at-queries/__tests__/media-query-transform-test.js
index caee2ec29..49421ae2c 100644
--- a/packages/style-value-parser/src/at-queries/__tests__/media-query-transform-test.js
+++ b/packages/style-value-parser/src/at-queries/__tests__/media-query-transform-test.js
@@ -577,4 +577,108 @@ describe('Media Query Transformer', () => {
     const result = lastMediaQueryWinsTransform(originalStyles);
     expect(JSON.stringify(result)).toBe(JSON.stringify(expectedStyles));
   });
+
+  test('media queries with em units', () => {
+    const originalStyles = {
+      foo: {
+        gridColumn: {
+          default: '1 / 2',
+          '@media (max-width: 90em) and (min-width: 60em)': '1 / 4',
+          '@media (max-width: 70em) and (min-width: 65em)': '1 / 3',
+        },
+      },
+    };
+
+    const expectedStyles = {
+      foo: {
+        gridColumn: {
+          default: '1 / 2',
+          '@media ((min-width: 60em) and (max-width: 64.99em)) or ((min-width: 70.01em) and (max-width: 90em))':
+            '1 / 4',
+          '@media (min-width: 65em) and (max-width: 70em)': '1 / 3',
+        },
+      },
+    };
+
+    const result = lastMediaQueryWinsTransform(originalStyles);
+    expect(JSON.stringify(result)).toBe(JSON.stringify(expectedStyles));
+  });
+
+  test('media queries with mixed units', () => {
+    const originalStyles = {
+      foo: {
+        gridColumn: {
+          default: '1 / 2',
+          '@media (max-width: 1200px) and (min-height: 50vh)': '1 / 4',
+          '@media (max-width: 800px) and (min-height: 30vh)': '1 / 3',
+        },
+      },
+    };
+
+    const expectedStyles = {
+      foo: {
+        gridColumn: {
+          default: '1 / 2',
+          '@media (min-width: 800.01px) and (max-width: 1200px) and (min-height: 50vh)':
+            '1 / 4',
+          '@media (max-width: 800px) and (min-height: 30vh)': '1 / 3',
+        },
+      },
+    };
+
+    const result = lastMediaQueryWinsTransform(originalStyles);
+    expect(JSON.stringify(result)).toBe(JSON.stringify(expectedStyles));
+  });
+
+  test('skips range simplification for media queries with conflicting units in same dimension across queries', () => {
+    const originalStyles = {
+      foo: {
+        gridColumn: {
+          default: '1 / 2',
+          '@media (min-width: 768px) and (max-width: 1200px)': '1 / 4',
+          '@media (min-width: 50em)': '1 / 3',
+        },
+      },
+    };
+
+    const expectedStyles = {
+      foo: {
+        gridColumn: {
+          default: '1 / 2',
+          '@media (min-width: 768px) and (max-width: 1200px) and (not (min-width: 50em))':
+            '1 / 4',
+          '@media (min-width: 50em)': '1 / 3',
+        },
+      },
+    };
+
+    const result = lastMediaQueryWinsTransform(originalStyles);
+    expect(JSON.stringify(result)).toBe(JSON.stringify(expectedStyles));
+  });
+
+  test('skips range simplification for media queries with conflicting units in same dimension and query', () => {
+    const originalStyles = {
+      foo: {
+        gridColumn: {
+          default: '1 / 2',
+          '@media (min-width: 768px) and (max-width: 1200em)': '1 / 4',
+          '@media (min-width: 50em)': '1 / 3',
+        },
+      },
+    };
+
+    const expectedStyles = {
+      foo: {
+        gridColumn: {
+          default: '1 / 2',
+          '@media (min-width: 768px) and (max-width: 1200em) and (not (min-width: 50em))':
+            '1 / 4',
+          '@media (min-width: 50em)': '1 / 3',
+        },
+      },
+    };
+
+    const result = lastMediaQueryWinsTransform(originalStyles);
+    expect(JSON.stringify(result)).toBe(JSON.stringify(expectedStyles));
+  });
 });