├── .gitignore ├── LICENSE ├── README.md ├── media-query-optimizer.js ├── package.json └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Filament Group 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | :warning: This project is archived and the repository is no longer maintained. 2 | 3 | # postcss-media-query-optimizer 4 | 5 | A postcss plugin that will optimize your media queries. 6 | 7 | ## Features 8 | 9 | * Removes `@media (max-width: 0) {}` blocks 10 | * Removes max < min: `@media (min-width: 400px) and (max-width: 320px) {}` 11 | * Simplifies `@media (min-width: 400px) and (min-width: 320px) {}` to `@media (min-width: 400px) {}` (You probably don’t write this kind of code but for us it came from Mixin usage in SASS. Also, this solves an IE11 issue that uses the last `*-width` if multiple `*-width`’s exist). 12 | * Simplifies `@media (min-width: 0) {}` to `@media all {}` 13 | * Simplifies `@media (min-width: 0) and (max-width: 400px) {}` to `@media (max-width: 400px) {}` 14 | 15 | ## Limitations 16 | 17 | * Only supports `em` and `px` units in media queries (for now). 18 | * Future TODO: Does not yet optimize across comma separated (OR) expressions. Would be better if `(min-width: 20em), (min-width: 30em)` would simplify to `(min-width: 20em)`. 19 | 20 | ## Usage 21 | 22 | Install from npm: [`postcss-media-query-optimizer`](https://www.npmjs.com/package/postcss-media-query-optimizer) 23 | 24 | ``` 25 | npm install postcss-media-query-optimizer --save-dev 26 | ``` 27 | 28 | ### Gulp 29 | 30 | _TODO_ 31 | 32 | ### Grunt 33 | 34 | ``` 35 | module.exports = function(grunt) { 36 | grunt.loadNpmTasks("grunt-postcss"); 37 | 38 | grunt.initConfig({ 39 | postcss: { 40 | options: { 41 | processors: [ 42 | require("../../Code/postcss-media-query-optimizer")() 43 | ] 44 | }, 45 | dist: { 46 | src: "**/*.css" 47 | } 48 | } 49 | // … 50 | }); 51 | }; 52 | ``` 53 | 54 | ## Run tests 55 | 56 | ``` 57 | npx ava 58 | ``` 59 | 60 | ## Credits 61 | 62 | * This plugin borrows heavily from [`postcss-media-minmax`](https://github.com/postcss/postcss-media-minmax). 63 | * Uhh, a thing I found after I made this that performs a similar task: [`postcss-mq-optimize`](https://www.npmjs.com/package/postcss-mq-optimize) 64 | -------------------------------------------------------------------------------- /media-query-optimizer.js: -------------------------------------------------------------------------------- 1 | const postcss = require("postcss"); 2 | const matchAll = require("string.prototype.matchall"); 3 | 4 | const regex = { 5 | minWidth: /\(\s*min\-width\s*:\s*(\s*[0-9\.]+)([\w]*)\s*\)/g, 6 | maxWidth: /\(\s*max\-width\s*:\s*(\s*[0-9\.]+)([\w]*)\s*\)/g 7 | }; 8 | 9 | function getValue(pxValue, units) { 10 | if( pxValue === 0 ) { 11 | return `0`; 12 | } 13 | if( units === "em" ) { 14 | return `${pxValue / 16}em`; 15 | } 16 | if( units === "px" ) { 17 | return `${pxValue}px`; 18 | } 19 | 20 | throw Error("media-query-optimizer only supports px and em units (for now).") 21 | } 22 | 23 | module.exports = postcss.plugin("postcss-media-query-optimizer", function () { 24 | return function(css) { 25 | css.walkAtRules("media", function(rule) { 26 | let finalSelector = []; 27 | 28 | rule.params.split(",").forEach(function(expression) { 29 | let usingEms = false; 30 | let minPx = undefined; 31 | let maxPx = undefined; 32 | let needsCorrection = false; 33 | 34 | expression = expression.trim(); 35 | 36 | [...matchAll( expression, regex.minWidth )].forEach(function(minMatch, i) { 37 | let minValue = parseFloat(minMatch[1]); 38 | if( minMatch[2] === "em" ) { 39 | usingEms = true; 40 | minValue *= 16; 41 | } 42 | 43 | if( minPx === undefined ) { 44 | minPx = minValue; 45 | } else { 46 | minPx = Math.max(minPx, minValue); 47 | } 48 | 49 | if(i > 0) { 50 | needsCorrection = true; 51 | } 52 | }); 53 | 54 | [...matchAll( expression, regex.maxWidth )].forEach(function(maxMatch, i) { 55 | let maxValue = parseFloat(maxMatch[1]); 56 | if( maxMatch[2] === "em" ) { 57 | usingEms = true; 58 | maxValue *= 16; 59 | } 60 | 61 | if( maxPx === undefined ) { 62 | maxPx = maxValue; 63 | } else { 64 | maxPx = Math.min(maxPx, maxValue); 65 | } 66 | 67 | if(i > 0) { 68 | needsCorrection = true; 69 | } 70 | }); 71 | 72 | if( maxPx !== undefined && maxPx === 0 ) { 73 | return; 74 | } 75 | 76 | if(minPx !== undefined && maxPx !== undefined && minPx > maxPx) { 77 | return; 78 | } 79 | 80 | // console.log( expression, needsCorrection, minPx, maxPx ); 81 | if(minPx !== undefined && minPx === 0) { 82 | if( maxPx === undefined ) { 83 | finalSelector.push("all"); 84 | return; 85 | } else { 86 | finalSelector.push(`(max-width: ${getValue(maxPx, usingEms ? "em" : "px")})`); 87 | return; 88 | } 89 | } else if(needsCorrection) { 90 | let results = []; 91 | if(minPx !== undefined) { 92 | results.push(`(min-width: ${getValue(minPx, usingEms ? "em" : "px")})`); 93 | } 94 | if(maxPx !== undefined) { 95 | results.push(`(max-width: ${getValue(maxPx, usingEms ? "em" : "px")})`); 96 | } 97 | 98 | if( results.length ) { 99 | finalSelector.push(results.join(" and ")); 100 | return; 101 | } 102 | } 103 | 104 | // passthrough 105 | finalSelector.push(expression); 106 | }); 107 | 108 | if( finalSelector.length === 0 ) { 109 | rule.remove(); 110 | } else { 111 | rule.params = finalSelector.join(", "); 112 | } 113 | }); 114 | 115 | }; 116 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-media-query-optimizer", 3 | "version": "1.0.1", 4 | "description": "A postcss plugin to optimize your media queries.", 5 | "main": "media-query-optimizer.js", 6 | "scripts": { 7 | "test": "ava" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "MIT", 12 | "dependencies": { 13 | "postcss": "^7.0.14", 14 | "string.prototype.matchall": "^3.0.1" 15 | }, 16 | "devDependencies": { 17 | "ava": "^1.4.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import postcss from "postcss"; 3 | import MediaQueryOptimizer from ".."; 4 | 5 | function getCSS(str) { 6 | return postcss() 7 | .use(MediaQueryOptimizer()) 8 | .process(str).css; 9 | } 10 | 11 | test("Control", t => { 12 | t.is(getCSS("* { color: red; } @media (min-width: 320px) { color: blue }"), "* { color: red; } @media (min-width: 320px) { color: blue }"); 13 | }); 14 | 15 | test("@media (max-width: 0)", t => { 16 | t.is(getCSS("* { color: red; } @media (max-width: 0) { color: blue }"), "* { color: red; }"); 17 | t.is(getCSS("@media (max-width: 0px) {}"), ""); 18 | }); 19 | 20 | test("@media (min-width: 0)", t => { 21 | t.is(getCSS("@media (min-width: 0) {}"), "@media all {}"); 22 | t.is(getCSS("@media (min-width: 0px) {}"), "@media all {}"); 23 | t.is(getCSS("@media (min-width: 0) and (min-width: 320px) {}"), "@media (min-width: 320px) {}"); 24 | }); 25 | 26 | test("Removed if Max less than min", t => { 27 | t.is(getCSS("@media (max-width: 320px) and (min-width: 321px) { color: blue }"), ""); 28 | }); 29 | 30 | test("Em values", t => { 31 | t.is(getCSS("@media (min-width: 20em) { color: blue }"), "@media (min-width: 20em) { color: blue }"); 32 | t.is(getCSS("@media (max-width: 20em) { color: blue }"), "@media (max-width: 20em) { color: blue }"); 33 | }); 34 | 35 | test("Px values", t => { 36 | t.is(getCSS("@media (min-width: 20px) { color: blue }"), "@media (min-width: 20px) { color: blue }"); 37 | t.is(getCSS("@media (max-width: 20px) { color: blue }"), "@media (max-width: 20px) { color: blue }"); 38 | }); 39 | 40 | test("Comma separated", t => { 41 | // TODO these should optimize across commas, this should become (min-width: 30em) 42 | t.is(getCSS("@media (min-width: 30em), (min-width: 48em) { color: blue }"), "@media (min-width: 30em), (min-width: 48em) { color: blue }"); 43 | t.is(getCSS("@media (max-width: 47.9375em) and (min-width: 30em), (min-width: 48em) { color: blue }"), "@media (max-width: 47.9375em) and (min-width: 30em), (min-width: 48em) { color: blue }"); 44 | }); 45 | 46 | test("From the wild", t => { 47 | t.is(getCSS(`@media (min-width: 59.375em) and (min-width: 37.5em) { 48 | .layout-full .layout_body { 49 | /* 600px */ 50 | padding-left: 5%; } }`), `@media (min-width: 59.375em) { 51 | .layout-full .layout_body { 52 | /* 600px */ 53 | padding-left: 5%; } }`); 54 | 55 | 56 | // Two queries 57 | t.is(getCSS(`@media (min-width: 59.375em) and (min-width: 37.5em) { 58 | .layout-full .layout_body { 59 | /* 600px */ 60 | padding-left: 5%; } } 61 | @media (min-width: 59.375em) and (min-width: 48em) { 62 | .layout-full .layout_body { 63 | /* 768px */ 64 | padding-left: 4%; } }`), `@media (min-width: 59.375em) { 65 | .layout-full .layout_body { 66 | /* 600px */ 67 | padding-left: 5%; } } 68 | @media (min-width: 59.375em) { 69 | .layout-full .layout_body { 70 | /* 768px */ 71 | padding-left: 4%; } }`); 72 | }); 73 | 74 | test("From the wild (reversed value order)", t => { 75 | // reversed values 76 | t.is(getCSS(`@media (min-width: 37.5em) and (min-width: 59.375em) { 77 | .layout-full .layout_body { 78 | /* 600px */ 79 | padding-left: 5%; } }`), `@media (min-width: 59.375em) { 80 | .layout-full .layout_body { 81 | /* 600px */ 82 | padding-left: 5%; } }`); 83 | 84 | // Two queries, reversed values 85 | t.is(getCSS(`@media (min-width: 37.5em) and (min-width: 59.375em) { 86 | .layout-full .layout_body { 87 | /* 600px */ 88 | padding-left: 5%; } } 89 | @media (min-width: 59.375em) and (min-width: 48em) { 90 | .layout-full .layout_body { 91 | /* 768px */ 92 | padding-left: 4%; } }`), `@media (min-width: 59.375em) { 93 | .layout-full .layout_body { 94 | /* 600px */ 95 | padding-left: 5%; } } 96 | @media (min-width: 59.375em) { 97 | .layout-full .layout_body { 98 | /* 768px */ 99 | padding-left: 4%; } }`); 100 | }); 101 | --------------------------------------------------------------------------------