├── .babelrc ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── README.md ├── index.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src └── index.js ├── test ├── ignore-some-situations.js ├── index.js └── px-to-rem-and-vw.js └── workflow.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "modules": false 7 | } 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "proseWrap": "preserve", 8 | "arrowParens": "avoid", 9 | "bracketSpacing": true, 10 | "endOfLine": "auto", 11 | "ignorePath": ".prettierignore", 12 | "jsxBracketSameLine": false, 13 | "jsxSingleQuote": false, 14 | "requireConfig": false, 15 | "trailingComma": "es5" 16 | } 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.0.9](https://github.com/ben-lau/postcss-pixel-to-remvw/compare/v2.0.8...v2.0.9) (2024-07-26) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * 修复exclude失效的bug ([7129d99](https://github.com/ben-lau/postcss-pixel-to-remvw/commit/7129d99a70a416e16c6be2aab5b2c379d2eec5aa)) 7 | 8 | 9 | 10 | ## [2.0.8](https://github.com/ben-lau/postcss-pixel-to-remvw/compare/v2.0.7...v2.0.8) (2024-07-26) 11 | 12 | 13 | 14 | ## [2.0.7](https://github.com/ben-lau/postcss-pixel-to-remvw/compare/v2.0.6...v2.0.7) (2024-07-24) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * 修改因css注入让第一个元素并非跳过转化标识导致的问题 ([6ee2ed1](https://github.com/ben-lau/postcss-pixel-to-remvw/commit/6ee2ed17fb7b195b289995df009218fab11eb3fa)) 20 | 21 | 22 | 23 | ## [2.0.6](https://github.com/ben-lau/postcss-pixel-to-remvw/compare/v2.0.5...v2.0.6) (2023-03-17) 24 | 25 | 26 | ### Bug Fixes 27 | 28 | * 修复exclude为函数时的边界问题 ([1af8534](https://github.com/ben-lau/postcss-pixel-to-remvw/commit/1af8534caad63585fcb6b5fa4da64ce01f943696)) 29 | 30 | 31 | 32 | ## [2.0.5](https://github.com/ben-lau/postcss-pixel-to-remvw/compare/v2.0.4...v2.0.5) (2022-03-21) 33 | 34 | 35 | ### Bug Fixes 36 | 37 | * 修复文档遗漏部分,添加完整化版本生成和日志生成 ([10f4af5](https://github.com/ben-lau/postcss-pixel-to-remvw/commit/10f4af5e296e4775f4760241a5230e2cd1054b86)) 38 | 39 | 40 | 41 | ## [2.0.4](https://github.com/ben-lau/postcss-pixel-to-remvw/compare/v2.0.3...v2.0.4) (2022-03-20) 42 | 43 | 44 | ### Bug Fixes 45 | 46 | * 修复在baseSize中不提供vw或者rem时,保留了px的问题 ([581099b](https://github.com/ben-lau/postcss-pixel-to-remvw/commit/581099b09bb2f80531a0eb7f1205ab3456d63afb)) 47 | 48 | 49 | 50 | ## [2.0.3](https://github.com/ben-lau/postcss-pixel-to-remvw/compare/v2.0.2...v2.0.3) (2021-10-26) 51 | 52 | 53 | 54 | ## [2.0.2](https://github.com/ben-lau/postcss-pixel-to-remvw/compare/v2.0.1...v2.0.2) (2021-09-09) 55 | 56 | 57 | 58 | ## [2.0.1](https://github.com/ben-lau/postcss-pixel-to-remvw/compare/v2.0.0...v2.0.1) (2021-09-09) 59 | 60 | 61 | 62 | # [2.0.0](https://github.com/ben-lau/postcss-pixel-to-remvw/compare/v1.0.2...v2.0.0) (2021-08-27) 63 | 64 | 65 | ### Bug Fixes 66 | 67 | * update doc for typos ([ffb4b24](https://github.com/ben-lau/postcss-pixel-to-remvw/commit/ffb4b247520c49a3410ad71fbe301727563b5538)) 68 | * update readme ([79cc4e5](https://github.com/ben-lau/postcss-pixel-to-remvw/commit/79cc4e5edcafff2fcdd1e9bbb34a416c3260f7b0)) 69 | 70 | 71 | ### Features 72 | 73 | * postcss version 8 compact ([5ba4352](https://github.com/ben-lau/postcss-pixel-to-remvw/commit/5ba43520897e25bd010ed705e3d99465ff223fcf)) 74 | * update doc ([b8de4d8](https://github.com/ben-lau/postcss-pixel-to-remvw/commit/b8de4d8a6e92242a5f6f6f02cfa4f2dc5630918d)) 75 | 76 | 77 | 78 | ## [1.0.2](https://github.com/ben-lau/postcss-pixel-to-remvw/compare/v1.0.1...v1.0.2) (2021-08-27) 79 | 80 | 81 | ### Bug Fixes 82 | 83 | * postcss v7 plugin ([de339c4](https://github.com/ben-lau/postcss-pixel-to-remvw/commit/de339c405b9db88f496aa6a63f370c7e1c670dd0)) 84 | 85 | 86 | 87 | ## [1.0.1](https://github.com/ben-lau/postcss-pixel-to-remvw/compare/283ea567445aac4c833588ad73316e9585fe95b7...v1.0.1) (2021-08-27) 88 | 89 | 90 | ### Bug Fixes 91 | 92 | * change package name ([fb0c683](https://github.com/ben-lau/postcss-pixel-to-remvw/commit/fb0c683613895feb70c48c0203f23bb1a6936757)) 93 | * older postcss version ([028a4c7](https://github.com/ben-lau/postcss-pixel-to-remvw/commit/028a4c7465dfdd5bba12c049e4fd895774b3b306)) 94 | 95 | 96 | ### Features 97 | 98 | * first commit ([283ea56](https://github.com/ben-lau/postcss-pixel-to-remvw/commit/283ea567445aac4c833588ad73316e9585fe95b7)) 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # postcss-pixel-to-remvw 2 | 3 | [![Package Quality](https://packagequality.com/badge/postcss-pixel-to-remvw.png)](https://packagequality.com/#?package=postcss-pixel-to-remvw) 4 | 5 | a postcss plugin for converting px to rem and vw, also you can choose only convert one of then. 6 | 7 | **This version requires `postcss` version 8** 8 | 9 | For postcss v7, you have to use [postcss-pixel-to-remvw v1](https://github.com/ben-lau/postcss-pixel-to-remvw/tree/older-postcss) 10 | 11 | # Install 12 | 13 | ``` 14 | npm install postcss-pixel-to-remvw -D 15 | ``` 16 | 17 | # Example 18 | 19 | ## normal 20 | 21 | - Input 22 | 23 | ```css 24 | h1 { 25 | margin: 0 0 20px 20px; 26 | font-size: 32px; 27 | line-height: 1.2; 28 | letter-spacing: 1px; 29 | } 30 | ``` 31 | 32 | - Output 33 | 34 | ```css 35 | h1 { 36 | margin: 0 0 0.26667rem 0.26667rem; 37 | margin: 0 0 2.66667vw 2.66667vw; 38 | font-size: 0.42667rem; 39 | font-size: 4.26667vw; 40 | line-height: 1.2; 41 | letter-spacing: 0.01333rem; 42 | letter-spacing: 0.13333vw; 43 | } 44 | ``` 45 | 46 | # Disable convert marker 47 | 48 | **Now, the disabled convert marker does not have to be at the top of the file, you can place it outside any root-level node** 49 | 50 | ## not convert all 51 | 52 | - Input 53 | 54 | ```css 55 | /*disable-convert*/ 56 | h1 { 57 | margin: 0 0 20px 20px; 58 | font-size: 32px; 59 | line-height: 1.2; 60 | letter-spacing: 1px; 61 | } 62 | ``` 63 | 64 | - Output 65 | 66 | ```css 67 | /*disable-convert*/ 68 | h1 { 69 | margin: 0 0 20px 20px; 70 | font-size: 32px; 71 | line-height: 1.2; 72 | letter-spacing: 1px; 73 | } 74 | ``` 75 | 76 | ## not convert to vw (compact with the pc) 77 | 78 | - Input 79 | 80 | ```css 81 | /*disable-convert-vw*/ 82 | h1 { 83 | margin: 0 0 20px 20px; 84 | font-size: 32px; 85 | line-height: 1.2; 86 | letter-spacing: 1px; 87 | } 88 | ``` 89 | 90 | - Output 91 | 92 | ```css 93 | /*disable-convert-vw*/ 94 | h1 { 95 | margin: 0 0 0.26667rem 0.26667rem; 96 | font-size: 0.42667rem; 97 | line-height: 1.2; 98 | letter-spacing: 0.01333rem; 99 | } 100 | ``` 101 | 102 | ## not convert some 103 | 104 | - Input 105 | 106 | ```css 107 | /*disable-convert-vw*/ 108 | h1 { 109 | margin: 0 0 20px 20px; 110 | font-size: 32px; /*no*/ 111 | line-height: 1.2; 112 | letter-spacing: 1px; 113 | } 114 | ``` 115 | 116 | - Output 117 | 118 | ```css 119 | /*disable-convert-vw*/ 120 | h1 { 121 | margin: 0 0 0.26667rem 0.26667rem; 122 | font-size: 32px; /*no*/ 123 | line-height: 1.2; 124 | letter-spacing: 0.01333rem; 125 | } 126 | ``` 127 | 128 | # Usage 129 | 130 | ## In Webpack 131 | 132 | ```javascript 133 | export default { 134 | module: { 135 | loaders: [ 136 | { 137 | test: /\.css$/, 138 | loader: [ 139 | 'style-loader', 140 | 'css-loader', 141 | { 142 | loader: 'postcss-loader', 143 | options: { 144 | postcssOptions: { 145 | plugins: [ 146 | [ 147 | 'postcss-pixel-to-remvw', 148 | { 149 | // Options 150 | }, 151 | ], 152 | ], 153 | }, 154 | }, 155 | }, 156 | ], 157 | }, 158 | ], 159 | }, 160 | }; 161 | ``` 162 | 163 | ## common usage 164 | 165 | ```javascript 166 | const { writeFile, readFileSync } = require('fs'); 167 | const postcss = require('postcss'); 168 | const pxtoremvw = require('postcss-pixel-to-remvw'); 169 | 170 | const processedCss = postcss(pxtoremvw()).process(readFileSync('test.css')).css; 171 | 172 | writeFile('test.remvw.css', processedCss, err => { 173 | if (err) throw err; 174 | console.log('Rem file written.'); 175 | }); 176 | ``` 177 | 178 | # Configuration 179 | 180 | default: 181 | 182 | ```javascript 183 | { 184 | baseSize: { 185 | rem: 75, // 10rem = 750px 186 | vw: 7.5, // 100vw = 750px 187 | }, 188 | unitPrecision: 5, 189 | selectorBlackList: [], 190 | propList: ['*'], 191 | minPixelValue: 0, 192 | exclude: null, 193 | // Single rule does not convert 194 | keepRuleComment: 'no', 195 | // The entire file is not converted 196 | commentOfDisableAll: 'disable-convert', 197 | // The entire file is not converted to rem 198 | commentOfDisableRem: 'disable-convert-rem', 199 | // nThe entire file is not converted to vw 200 | commentOfDisableVW: 'disable-convert-vw', 201 | }; 202 | ``` 203 | 204 | - `baseSize` {Object} the base size config, default is { rem: 75, vw: 7.5 } 205 | - `rem` {Number | undefined} the root element font size, means 1rem = [your setting] px; It won't convert to rem while `rem` is undefined 206 | - `vw` {Number | undefined} the base ratio for viewport width, means 1vw = [your setting] px; It won't convert to vw while `vw` is undefined 207 | - `unitPrecision` {Number} the digital accurarcy of converted stylesheet 208 | - `selectorBlackList` {string[]} The selectors list to ignore conversion 209 | - If value is string, it checks to see if selector contains the string.['body'] will match .body-class 210 | - If value is regexp, it checks to see if the selector matches the regexp. [/^body$/] will match body but not .body 211 | - `propList` {string[]} The properties that can do the conversion. 212 | - Values need to be exact matches. 213 | - Use wildcard _ to enable all properties. Example: ['_'] 214 | - Use * at the start or end of a word. (['*position\*'] will match background-position-y) 215 | - Use ! to not match a property. Example: ['*', '!letter-spacing'] 216 | - Combine the "not" prefix with the other prefixes. Example: ['*', '!font*'] 217 | - `minPixelValue` {Number} the minimum pixel value to replace 218 | - `exclude` {String | Regexp | ()=>boolean} the file path to ignore conversion 219 | - If value is string, it checks to see if file path contains the string. 220 | - 'exclude' will match \project\postcss-pxtorem\exclude\path 221 | - If value is regexp, it checks to see if file path matches the regexp. 222 | - /exclude/i will match \project\postcss-pxtorem\exclude\path 223 | - If value is function, you can use exclude function to return a true and the file will be ignored. 224 | - the callback will pass the file path as a parameter, it should returns a Boolean result. 225 | - function (file) { return file.indexOf('exclude') !== -1; } 226 | - `keepRuleComment` {String} a comment stating that the property will not be converted 227 | - `commentOfDisableAll` {String} a comment stating that the whole file will not be converted 228 | - `commentOfDisableRem` {String} a comment stating that the whole file will not be converted to rem,but keep convert to vw 229 | - `commentOfDisableVW` {String} a comment stating that the whole file will not be converted to vw,but keep convert to rem 230 | 231 | # TODO 232 | 233 | - [x] compact with older version of postcss 234 | - [x] unit test 235 | - [x] baseSize 236 | - [x] keepRuleComment 237 | - [x] commentOfDisableAll 238 | - [x] commentOfDisableRem 239 | - [x] commentOfDisableVW 240 | - [ ] unitPrecision 241 | - [ ] selectorBlackList 242 | - [ ] propList 243 | - [ ] minPixelValue 244 | - [ ] exclude 245 | 246 | Thanks to [postcss-pxtorem](https://github.com/cuth/postcss-pxtorem) 247 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /**@typedef {{baseSize: {rem:number,vw:number},unitPrecision: number,selectorBlackList: string[],propList: string[],minPixelValue: number,exclude: string|RegExp|()=>boolean ,commentOfDisableAll:string,commentOfDisableRem:string,commentOfDisableVW:string}} Options */ 4 | 5 | /** 6 | * @type {Options} 7 | */ 8 | var defaults = { 9 | baseSize: { 10 | rem: 75, 11 | // 10rem = 750px 12 | vw: 7.5 // 100vw = 750px 13 | 14 | }, 15 | unitPrecision: 5, 16 | selectorBlackList: [], 17 | propList: ['*'], 18 | minPixelValue: 0, 19 | exclude: null, 20 | // Single rule does not convert 21 | keepRuleComment: 'no', 22 | // The entire file is not converted 23 | commentOfDisableAll: 'disable-convert', 24 | // The entire file is not converted to rem 25 | commentOfDisableRem: 'disable-convert-rem', 26 | // nThe entire file is not converted to vw 27 | commentOfDisableVW: 'disable-convert-vw' 28 | }; 29 | var REG_PX = /"[^"]+"|'[^']+'|url\([^)]+\)|(\d*\.?\d+)px/g; 30 | 31 | var isFunction = function isFunction(target) { 32 | return typeof target === 'function'; 33 | }; 34 | 35 | var isString = function isString(target) { 36 | return typeof target === 'string'; 37 | }; 38 | 39 | var isRegExp = function isRegExp(target) { 40 | return target instanceof RegExp; 41 | }; 42 | 43 | var toFixed = function toFixed(number, precision) { 44 | var multiplier = Math.pow(10, precision + 1); 45 | var wholeNumber = Math.floor(number * multiplier); 46 | return Math.round(wholeNumber / 10) * 10 / multiplier; 47 | }; 48 | 49 | var filterPropList = { 50 | exact: function exact(list) { 51 | return list.filter(function (m) { 52 | return m.match(/^[^*!]+$/); 53 | }); 54 | }, 55 | contain: function contain(list) { 56 | return list.filter(function (m) { 57 | return m.match(/^\*.+\*$/); 58 | }).map(function (m) { 59 | return m.substr(1, m.length - 2); 60 | }); 61 | }, 62 | endWith: function endWith(list) { 63 | return list.filter(function (m) { 64 | return m.match(/^\*[^*]+$/); 65 | }).map(function (m) { 66 | return m.substr(1); 67 | }); 68 | }, 69 | startWith: function startWith(list) { 70 | return list.filter(function (m) { 71 | return m.match(/^[^*!]+\*$/); 72 | }).map(function (m) { 73 | return m.substr(0, m.length - 1); 74 | }); 75 | }, 76 | notExact: function notExact(list) { 77 | return list.filter(function (m) { 78 | return m.match(/^![^*].*$/); 79 | }).map(function (m) { 80 | return m.substr(1); 81 | }); 82 | }, 83 | notContain: function notContain(list) { 84 | return list.filter(function (m) { 85 | return m.match(/^!\*.+\*$/); 86 | }).map(function (m) { 87 | return m.substr(2, m.length - 3); 88 | }); 89 | }, 90 | notEndWith: function notEndWith(list) { 91 | return list.filter(function (m) { 92 | return m.match(/^!\*[^*]+$/); 93 | }).map(function (m) { 94 | return m.substr(2); 95 | }); 96 | }, 97 | notStartWith: function notStartWith(list) { 98 | return list.filter(function (m) { 99 | return m.match(/^![^*]+\*$/); 100 | }).map(function (m) { 101 | return m.substr(1, m.length - 2); 102 | }); 103 | } 104 | }; // px replacer creator 105 | 106 | var createPxReplacer = function createPxReplacer(perRatio, minPixelValue, unitPrecision, unit) { 107 | return function (origin, $1) { 108 | var pixels = parseFloat($1); 109 | 110 | if (!$1 || pixels <= minPixelValue) { 111 | return origin; 112 | } else { 113 | return "".concat(toFixed(pixels / perRatio, unitPrecision)).concat(unit); 114 | } 115 | }; 116 | }; 117 | 118 | var inBlackList = function inBlackList(blacklist, selector) { 119 | if (typeof selector !== 'string') { 120 | return false; 121 | } 122 | 123 | return blacklist.some(function (regex) { 124 | if (typeof regex === 'string') { 125 | return selector.indexOf(regex) !== -1; 126 | } 127 | 128 | return selector.match(regex); 129 | }); 130 | }; 131 | 132 | var createPropListMatcher = function createPropListMatcher(propList) { 133 | var hasWild = propList.indexOf('*') > -1; 134 | var matchAll = hasWild && propList.length === 1; 135 | var lists = { 136 | exact: filterPropList.exact(propList), 137 | contain: filterPropList.contain(propList), 138 | startWith: filterPropList.startWith(propList), 139 | endWith: filterPropList.endWith(propList), 140 | notExact: filterPropList.notExact(propList), 141 | notContain: filterPropList.notContain(propList), 142 | notStartWith: filterPropList.notStartWith(propList), 143 | notEndWith: filterPropList.notEndWith(propList) 144 | }; 145 | return function (prop) { 146 | if (matchAll) return true; 147 | return (hasWild || lists.exact.indexOf(prop) > -1 || lists.contain.some(function (m) { 148 | return prop.indexOf(m) > -1; 149 | }) || lists.startWith.some(function (m) { 150 | return prop.indexOf(m) === 0; 151 | }) || lists.endWith.some(function (m) { 152 | return prop.indexOf(m) === prop.length - m.length; 153 | })) && !(lists.notExact.indexOf(prop) > -1 || lists.notContain.some(function (m) { 154 | return prop.indexOf(m) > -1; 155 | }) || lists.notStartWith.some(function (m) { 156 | return prop.indexOf(m) === 0; 157 | }) || lists.notEndWith.some(function (m) { 158 | return prop.indexOf(m) === prop.length - m.length; 159 | })); 160 | }; 161 | }; 162 | 163 | var declarationExists = function declarationExists(decls, prop, value) { 164 | return decls.some(function (decl) { 165 | return decl.prop === prop && decl.value === value; 166 | }); 167 | }; 168 | /** 169 | * @param {Partial} options 170 | */ 171 | 172 | 173 | var converter = function converter() { 174 | var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 175 | 176 | var _Object$assign = Object.assign({}, defaults, options), 177 | exclude = _Object$assign.exclude, 178 | unitPrecision = _Object$assign.unitPrecision, 179 | minPixelValue = _Object$assign.minPixelValue, 180 | baseSize = _Object$assign.baseSize, 181 | propList = _Object$assign.propList, 182 | selectorBlackList = _Object$assign.selectorBlackList, 183 | keepRuleComment = _Object$assign.keepRuleComment, 184 | commentOfDisableAll = _Object$assign.commentOfDisableAll, 185 | commentOfDisableVW = _Object$assign.commentOfDisableVW, 186 | commentOfDisableRem = _Object$assign.commentOfDisableRem; 187 | 188 | var satisfyPropList = createPropListMatcher(propList); 189 | var isExcludeFile = false; 190 | var remReplace = null; 191 | var vwReplace = null; 192 | return { 193 | postcssPlugin: 'postcss-px-to-remvw', 194 | Once: function Once(css) { 195 | var filePath = css.source.input.file; 196 | 197 | if (exclude && (isFunction(exclude) && exclude(filePath) || isString(exclude) && filePath.indexOf(exclude) !== -1 || isRegExp(exclude) && filePath.match(exclude) !== null)) { 198 | isExcludeFile = true; 199 | } else { 200 | isExcludeFile = false; 201 | } 202 | 203 | var remRootValue = baseSize.rem; 204 | var vwRootValue = baseSize.vw; 205 | remReplace = createPxReplacer(remRootValue, minPixelValue, unitPrecision, 'rem'); 206 | vwReplace = createPxReplacer(vwRootValue, minPixelValue, unitPrecision, 'vw'); // whole file, when exclude match this whole file, skip the comment marker 207 | 208 | isExcludeFile || (isExcludeFile = css.nodes.some(function (item) { 209 | return item.type === 'comment' && item.text.trim() === commentOfDisableAll; 210 | })); // not convert rem 211 | 212 | if (isExcludeFile || css.nodes.some(function (item) { 213 | return item.type === 'comment' && item.text.trim() === commentOfDisableRem; 214 | })) { 215 | remReplace = null; 216 | } // not convert vw 217 | 218 | 219 | if (isExcludeFile || css.nodes.some(function (item) { 220 | return item.type === 'comment' && item.text.trim() === commentOfDisableVW; 221 | })) { 222 | vwReplace = null; 223 | } 224 | }, 225 | Declaration: function Declaration(decl) { 226 | var next = decl.next(); 227 | 228 | if (isExcludeFile || decl.value.indexOf('px') === -1 || !satisfyPropList(decl.prop) || inBlackList(selectorBlackList, decl.parent.selector) || next && next.type === 'comment' && next.text === keepRuleComment) { 229 | return; 230 | } 231 | 232 | var value = decl.value; 233 | var needConvertVw = baseSize.vw && vwReplace; 234 | var needConvertRem = baseSize.rem && remReplace; 235 | 236 | if (needConvertVw) { 237 | var _value = value.replace(REG_PX, vwReplace); // if rem unit already exists, do not add or replace 238 | 239 | 240 | if (!declarationExists(decl.parent, decl.prop, _value)) { 241 | if (needConvertRem) { 242 | decl.cloneAfter({ 243 | value: _value 244 | }); 245 | } else { 246 | decl.value = _value; 247 | } 248 | } 249 | } 250 | 251 | if (needConvertRem) { 252 | var _value2 = value.replace(REG_PX, remReplace); // if rem unit already exists, do not add or replace 253 | 254 | 255 | if (!declarationExists(decl.parent, decl.prop, _value2)) { 256 | decl.value = _value2; 257 | } 258 | } 259 | } 260 | }; 261 | }; // https://github.com/postcss/postcss/blob/main/docs/writing-a-plugin.md 262 | 263 | 264 | converter.postcss = true; 265 | 266 | module.exports = converter; 267 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-pixel-to-remvw", 3 | "version": "2.0.9", 4 | "description": "a postcss plugin for converting px to rem and vw", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "rollup -c rollup.config.js", 8 | "test": "mocha", 9 | "to-self": "work-start --from workflow.config.js to-self", 10 | "release:patch": "work-start --from workflow.config.js release:patch", 11 | "release:minor": "work-start --from workflow.config.js release:minor", 12 | "release:major": "work-start --from workflow.config.js release:major", 13 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0", 14 | "version:major": "standard-version --skip.commit --skip.changelog --release-as major", 15 | "version:minor": "standard-version --skip.commit --skip.changelog --release-as minor", 16 | "version:patch": "standard-version --skip.commit --skip.changelog --release-as patch", 17 | "postinstall": "echo \"\u001b[36m\u001b[1m\nThanks for using postcss-pixel-to-remvw! \nIf you have any good suggestions or any questions, please let me know\u001b[0m\n\"" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/ben-lau/postcss-pixel-to-remvw.git" 22 | }, 23 | "keywords": [ 24 | "css", 25 | "pixel", 26 | "rem", 27 | "viewport-width", 28 | "px", 29 | "vw", 30 | "convert", 31 | "postcss", 32 | "postcss-plugin" 33 | ], 34 | "author": "Ben Lau", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/ben-lau/postcss-pixel-to-remvw/issues" 38 | }, 39 | "homepage": "https://github.com/ben-lau/postcss-pixel-to-remvw#readme", 40 | "dependencies": { 41 | "postcss": "^8.3.6" 42 | }, 43 | "devDependencies": { 44 | "@babel/core": "^7.15.0", 45 | "@babel/preset-env": "^7.15.0", 46 | "@rollup/plugin-babel": "^5.3.0", 47 | "babel-plugin-external-helpers": "^6.22.0", 48 | "babel-preset-latest": "^6.24.1", 49 | "conventional-changelog-cli": "^2.2.2", 50 | "expect": "^27.0.6", 51 | "husky": "^4.3.0", 52 | "mocha": "^9.1.0", 53 | "rollup": "^2.56.3", 54 | "rollup-plugin-babel": "^4.4.0", 55 | "rollup-plugin-node-resolve": "^5.2.0", 56 | "task-workflow": "^1.2.0", 57 | "standard-version": "^9.3.2" 58 | }, 59 | "engines": { 60 | "node": ">=6.0.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import pkg from './package.json'; 3 | import babel from '@rollup/plugin-babel'; 4 | 5 | export default { 6 | input: path.resolve(__dirname, './src/index.js'), 7 | output: { 8 | file: path.resolve(__dirname, pkg.main), 9 | format: 'cjs', 10 | exports: 'auto', 11 | }, 12 | plugins: [babel({ babelHelpers: 'bundled' })], 13 | }; 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /**@typedef {{baseSize: {rem:number,vw:number},unitPrecision: number,selectorBlackList: string[],propList: string[],minPixelValue: number,exclude: string|RegExp|()=>boolean ,commentOfDisableAll:string,commentOfDisableRem:string,commentOfDisableVW:string}} Options */ 2 | 3 | /** 4 | * @type {Options} 5 | */ 6 | const defaults = { 7 | baseSize: { 8 | rem: 75, // 10rem = 750px 9 | vw: 7.5, // 100vw = 750px 10 | }, 11 | unitPrecision: 5, 12 | selectorBlackList: [], 13 | propList: ['*'], 14 | minPixelValue: 0, 15 | exclude: null, 16 | // Single rule does not convert 17 | keepRuleComment: 'no', 18 | // The entire file is not converted 19 | commentOfDisableAll: 'disable-convert', 20 | // The entire file is not converted to rem 21 | commentOfDisableRem: 'disable-convert-rem', 22 | // nThe entire file is not converted to vw 23 | commentOfDisableVW: 'disable-convert-vw', 24 | }; 25 | 26 | const REG_PX = /"[^"]+"|'[^']+'|url\([^)]+\)|(\d*\.?\d+)px/g; 27 | 28 | const isFunction = target => typeof target === 'function'; 29 | 30 | const isString = target => typeof target === 'string'; 31 | 32 | const isRegExp = target => target instanceof RegExp; 33 | 34 | const toFixed = (number, precision) => { 35 | const multiplier = 10 ** (precision + 1); 36 | const wholeNumber = Math.floor(number * multiplier); 37 | return (Math.round(wholeNumber / 10) * 10) / multiplier; 38 | }; 39 | 40 | const filterPropList = { 41 | exact: list => list.filter(m => m.match(/^[^*!]+$/)), 42 | contain: list => 43 | list.filter(m => m.match(/^\*.+\*$/)).map(m => m.substr(1, m.length - 2)), 44 | endWith: list => list.filter(m => m.match(/^\*[^*]+$/)).map(m => m.substr(1)), 45 | startWith: list => 46 | list.filter(m => m.match(/^[^*!]+\*$/)).map(m => m.substr(0, m.length - 1)), 47 | notExact: list => 48 | list.filter(m => m.match(/^![^*].*$/)).map(m => m.substr(1)), 49 | notContain: list => 50 | list.filter(m => m.match(/^!\*.+\*$/)).map(m => m.substr(2, m.length - 3)), 51 | notEndWith: list => 52 | list.filter(m => m.match(/^!\*[^*]+$/)).map(m => m.substr(2)), 53 | notStartWith: list => 54 | list.filter(m => m.match(/^![^*]+\*$/)).map(m => m.substr(1, m.length - 2)), 55 | }; 56 | 57 | // px replacer creator 58 | const createPxReplacer = 59 | (perRatio, minPixelValue, unitPrecision, unit) => (origin, $1) => { 60 | const pixels = parseFloat($1); 61 | if (!$1 || pixels <= minPixelValue) { 62 | return origin; 63 | } else { 64 | return `${toFixed(pixels / perRatio, unitPrecision)}${unit}`; 65 | } 66 | }; 67 | 68 | const inBlackList = (blacklist, selector) => { 69 | if (typeof selector !== 'string') { 70 | return false; 71 | } 72 | return blacklist.some(regex => { 73 | if (typeof regex === 'string') { 74 | return selector.indexOf(regex) !== -1; 75 | } 76 | return selector.match(regex); 77 | }); 78 | }; 79 | 80 | const createPropListMatcher = propList => { 81 | const hasWild = propList.indexOf('*') > -1; 82 | const matchAll = hasWild && propList.length === 1; 83 | const lists = { 84 | exact: filterPropList.exact(propList), 85 | contain: filterPropList.contain(propList), 86 | startWith: filterPropList.startWith(propList), 87 | endWith: filterPropList.endWith(propList), 88 | notExact: filterPropList.notExact(propList), 89 | notContain: filterPropList.notContain(propList), 90 | notStartWith: filterPropList.notStartWith(propList), 91 | notEndWith: filterPropList.notEndWith(propList), 92 | }; 93 | return prop => { 94 | if (matchAll) return true; 95 | return ( 96 | (hasWild || 97 | lists.exact.indexOf(prop) > -1 || 98 | lists.contain.some(m => prop.indexOf(m) > -1) || 99 | lists.startWith.some(m => prop.indexOf(m) === 0) || 100 | lists.endWith.some(m => prop.indexOf(m) === prop.length - m.length)) && 101 | !( 102 | lists.notExact.indexOf(prop) > -1 || 103 | lists.notContain.some(m => prop.indexOf(m) > -1) || 104 | lists.notStartWith.some(m => prop.indexOf(m) === 0) || 105 | lists.notEndWith.some(m => prop.indexOf(m) === prop.length - m.length) 106 | ) 107 | ); 108 | }; 109 | }; 110 | 111 | const declarationExists = (decls, prop, value) => 112 | decls.some(decl => decl.prop === prop && decl.value === value); 113 | 114 | /** 115 | * @param {Partial} options 116 | */ 117 | const converter = (options = {}) => { 118 | const { 119 | exclude, 120 | unitPrecision, 121 | minPixelValue, 122 | baseSize, 123 | propList, 124 | selectorBlackList, 125 | keepRuleComment, 126 | commentOfDisableAll, 127 | commentOfDisableVW, 128 | commentOfDisableRem, 129 | } = Object.assign({}, defaults, options); 130 | 131 | const satisfyPropList = createPropListMatcher(propList); 132 | let isExcludeFile = false; 133 | let remReplace = null; 134 | let vwReplace = null; 135 | 136 | return { 137 | postcssPlugin: 'postcss-px-to-remvw', 138 | Once(css) { 139 | const filePath = css.source.input.file; 140 | 141 | if ( 142 | exclude && 143 | ((isFunction(exclude) && exclude(filePath)) || 144 | (isString(exclude) && filePath.indexOf(exclude) !== -1) || 145 | (isRegExp(exclude) && filePath.match(exclude) !== null)) 146 | ) { 147 | isExcludeFile = true; 148 | } else { 149 | isExcludeFile = false; 150 | } 151 | 152 | const remRootValue = baseSize.rem; 153 | const vwRootValue = baseSize.vw; 154 | 155 | remReplace = createPxReplacer( 156 | remRootValue, 157 | minPixelValue, 158 | unitPrecision, 159 | 'rem' 160 | ); 161 | vwReplace = createPxReplacer( 162 | vwRootValue, 163 | minPixelValue, 164 | unitPrecision, 165 | 'vw' 166 | ); 167 | 168 | // whole file, when exclude match this whole file, skip the comment marker 169 | isExcludeFile ||= css.nodes.some( 170 | item => 171 | item.type === 'comment' && item.text.trim() === commentOfDisableAll 172 | ); 173 | 174 | // not convert rem 175 | if ( 176 | isExcludeFile || 177 | css.nodes.some( 178 | item => 179 | item.type === 'comment' && item.text.trim() === commentOfDisableRem 180 | ) 181 | ) { 182 | remReplace = null; 183 | } 184 | 185 | // not convert vw 186 | if ( 187 | isExcludeFile || 188 | css.nodes.some( 189 | item => 190 | item.type === 'comment' && item.text.trim() === commentOfDisableVW 191 | ) 192 | ) { 193 | vwReplace = null; 194 | } 195 | }, 196 | Declaration(decl) { 197 | const next = decl.next(); 198 | if ( 199 | isExcludeFile || 200 | decl.value.indexOf('px') === -1 || 201 | !satisfyPropList(decl.prop) || 202 | inBlackList(selectorBlackList, decl.parent.selector) || 203 | (next && next.type === 'comment' && next.text === keepRuleComment) 204 | ) { 205 | return; 206 | } 207 | const value = decl.value; 208 | const needConvertVw = baseSize.vw && vwReplace; 209 | const needConvertRem = baseSize.rem && remReplace; 210 | 211 | if (needConvertVw) { 212 | const _value = value.replace(REG_PX, vwReplace); 213 | 214 | // if rem unit already exists, do not add or replace 215 | if (!declarationExists(decl.parent, decl.prop, _value)) { 216 | if (needConvertRem) { 217 | decl.cloneAfter({ value: _value }); 218 | } else { 219 | decl.value = _value; 220 | } 221 | } 222 | } 223 | 224 | if (needConvertRem) { 225 | const _value = value.replace(REG_PX, remReplace); 226 | 227 | // if rem unit already exists, do not add or replace 228 | if (!declarationExists(decl.parent, decl.prop, _value)) { 229 | decl.value = _value; 230 | } 231 | } 232 | }, 233 | }; 234 | }; 235 | 236 | // https://github.com/postcss/postcss/blob/main/docs/writing-a-plugin.md 237 | converter.postcss = true; 238 | 239 | export default converter; 240 | -------------------------------------------------------------------------------- /test/ignore-some-situations.js: -------------------------------------------------------------------------------- 1 | const postcss = require('postcss'); 2 | const expect = require('expect'); 3 | const pxtorem = require('../index.js'); 4 | 5 | describe('ignore some situations', () => { 6 | it('should not convert some property with disabled comment', () => { 7 | const input = ` 8 | h1 { 9 | margin: 0 0 20px 20px; 10 | font-size: 32px; /*no*/ 11 | line-height: 1.2; 12 | letter-spacing: 1px; 13 | }`; 14 | const output = ` 15 | h1 { 16 | margin: 0 0 0.26667rem 0.26667rem; 17 | margin: 0 0 2.66667vw 2.66667vw; 18 | font-size: 32px; /*no*/ 19 | line-height: 1.2; 20 | letter-spacing: 0.01333rem; 21 | letter-spacing: 0.13333vw; 22 | }`; 23 | const processed = postcss(pxtorem()).process(input).css; 24 | 25 | expect(processed).toBe(output); 26 | }); 27 | 28 | it('should not convert some property with new disabled comment', () => { 29 | const input = ` 30 | h1 { 31 | margin: 0 0 20px 20px; 32 | font-size: 32px; /*no-convert*/ 33 | line-height: 1.2; 34 | letter-spacing: 1px; 35 | }`; 36 | const output = ` 37 | h1 { 38 | margin: 0 0 0.26667rem 0.26667rem; 39 | margin: 0 0 2.66667vw 2.66667vw; 40 | font-size: 32px; /*no-convert*/ 41 | line-height: 1.2; 42 | letter-spacing: 0.01333rem; 43 | letter-spacing: 0.13333vw; 44 | }`; 45 | const processed = postcss( 46 | pxtorem({ keepRuleComment: 'no-convert' }) 47 | ).process(input).css; 48 | 49 | expect(processed).toBe(output); 50 | }); 51 | 52 | it('should not convert to rem in whole file with disabled comment at the top', () => { 53 | const input = ` 54 | /*disable-convert-rem*/ 55 | h1 { 56 | margin: 0 0 20px 20px; 57 | font-size: 32px; 58 | line-height: 1.2; 59 | letter-spacing: 1px; 60 | }`; 61 | const output = ` 62 | /*disable-convert-rem*/ 63 | h1 { 64 | margin: 0 0 2.66667vw 2.66667vw; 65 | font-size: 4.26667vw; 66 | line-height: 1.2; 67 | letter-spacing: 0.13333vw; 68 | }`; 69 | const processed = postcss(pxtorem()).process(input).css; 70 | 71 | expect(processed).toBe(output); 72 | }); 73 | 74 | it('should not convert to rem in whole file with disabled comment anywhere', () => { 75 | const input = ` 76 | h1 { 77 | margin: 0 0 20px 20px; 78 | font-size: 32px; 79 | line-height: 1.2; 80 | letter-spacing: 1px; 81 | } 82 | 83 | h2 { 84 | margin: 0 0 20px 20px; 85 | font-size: 32px; 86 | line-height: 1.2; 87 | letter-spacing: 1px; 88 | } 89 | 90 | /*disable-convert-rem*/ 91 | h3 { 92 | margin: 0 0 20px 20px; 93 | font-size: 32px; 94 | line-height: 1.2; 95 | letter-spacing: 1px; 96 | }`; 97 | const output = ` 98 | h1 { 99 | margin: 0 0 2.66667vw 2.66667vw; 100 | font-size: 4.26667vw; 101 | line-height: 1.2; 102 | letter-spacing: 0.13333vw; 103 | } 104 | 105 | h2 { 106 | margin: 0 0 2.66667vw 2.66667vw; 107 | font-size: 4.26667vw; 108 | line-height: 1.2; 109 | letter-spacing: 0.13333vw; 110 | } 111 | 112 | /*disable-convert-rem*/ 113 | h3 { 114 | margin: 0 0 2.66667vw 2.66667vw; 115 | font-size: 4.26667vw; 116 | line-height: 1.2; 117 | letter-spacing: 0.13333vw; 118 | }`; 119 | const processed = postcss(pxtorem()).process(input).css; 120 | 121 | expect(processed).toBe(output); 122 | }); 123 | 124 | it('should not convert to rem in whole file with new disabled comment at the top', () => { 125 | const input = ` 126 | /*no-convert-rem*/ 127 | h1 { 128 | margin: 0 0 20px 20px; 129 | font-size: 32px; 130 | line-height: 1.2; 131 | letter-spacing: 1px; 132 | }`; 133 | const output = ` 134 | /*no-convert-rem*/ 135 | h1 { 136 | margin: 0 0 2.66667vw 2.66667vw; 137 | font-size: 4.26667vw; 138 | line-height: 1.2; 139 | letter-spacing: 0.13333vw; 140 | }`; 141 | const processed = postcss( 142 | pxtorem({ commentOfDisableRem: 'no-convert-rem' }) 143 | ).process(input).css; 144 | 145 | expect(processed).toBe(output); 146 | }); 147 | 148 | it('should not convert to rem in whole file with new disabled comment anywhere', () => { 149 | const input = ` 150 | h1 { 151 | margin: 0 0 20px 20px; 152 | font-size: 32px; 153 | line-height: 1.2; 154 | letter-spacing: 1px; 155 | } 156 | h2 { 157 | margin: 0 0 20px 20px; 158 | font-size: 32px; 159 | line-height: 1.2; 160 | letter-spacing: 1px; 161 | } 162 | /*no-convert-rem*/ 163 | h3 { 164 | margin: 0 0 20px 20px; 165 | font-size: 32px; 166 | line-height: 1.2; 167 | letter-spacing: 1px; 168 | }`; 169 | const output = ` 170 | h1 { 171 | margin: 0 0 2.66667vw 2.66667vw; 172 | font-size: 4.26667vw; 173 | line-height: 1.2; 174 | letter-spacing: 0.13333vw; 175 | } 176 | h2 { 177 | margin: 0 0 2.66667vw 2.66667vw; 178 | font-size: 4.26667vw; 179 | line-height: 1.2; 180 | letter-spacing: 0.13333vw; 181 | } 182 | /*no-convert-rem*/ 183 | h3 { 184 | margin: 0 0 2.66667vw 2.66667vw; 185 | font-size: 4.26667vw; 186 | line-height: 1.2; 187 | letter-spacing: 0.13333vw; 188 | }`; 189 | const processed = postcss( 190 | pxtorem({ commentOfDisableRem: 'no-convert-rem' }) 191 | ).process(input).css; 192 | 193 | expect(processed).toBe(output); 194 | }); 195 | 196 | it('should not convert to vw in whole file with disabled comment at the top', () => { 197 | const input = ` 198 | /*disable-convert-vw*/ 199 | h1 { 200 | margin: 0 0 20px 20px; 201 | font-size: 32px; 202 | line-height: 1.2; 203 | letter-spacing: 1px; 204 | }`; 205 | const output = ` 206 | /*disable-convert-vw*/ 207 | h1 { 208 | margin: 0 0 0.26667rem 0.26667rem; 209 | font-size: 0.42667rem; 210 | line-height: 1.2; 211 | letter-spacing: 0.01333rem; 212 | }`; 213 | const processed = postcss(pxtorem()).process(input).css; 214 | 215 | expect(processed).toBe(output); 216 | }); 217 | 218 | it('should not convert to vw in whole file with disabled comment anywhere', () => { 219 | const input = ` 220 | h1 { 221 | margin: 0 0 20px 20px; 222 | font-size: 32px; 223 | line-height: 1.2; 224 | letter-spacing: 1px; 225 | } 226 | h2 { 227 | margin: 0 0 20px 20px; 228 | font-size: 32px; 229 | line-height: 1.2; 230 | letter-spacing: 1px; 231 | } 232 | /*disable-convert-vw*/ 233 | h3 { 234 | margin: 0 0 20px 20px; 235 | font-size: 32px; 236 | line-height: 1.2; 237 | letter-spacing: 1px; 238 | }`; 239 | const output = ` 240 | h1 { 241 | margin: 0 0 0.26667rem 0.26667rem; 242 | font-size: 0.42667rem; 243 | line-height: 1.2; 244 | letter-spacing: 0.01333rem; 245 | } 246 | h2 { 247 | margin: 0 0 0.26667rem 0.26667rem; 248 | font-size: 0.42667rem; 249 | line-height: 1.2; 250 | letter-spacing: 0.01333rem; 251 | } 252 | /*disable-convert-vw*/ 253 | h3 { 254 | margin: 0 0 0.26667rem 0.26667rem; 255 | font-size: 0.42667rem; 256 | line-height: 1.2; 257 | letter-spacing: 0.01333rem; 258 | }`; 259 | const processed = postcss(pxtorem()).process(input).css; 260 | 261 | expect(processed).toBe(output); 262 | }); 263 | 264 | it('should not convert to vw in whole file with new disabled comment at the top', () => { 265 | const input = ` 266 | /*no-convert-vw*/ 267 | h1 { 268 | margin: 0 0 20px 20px; 269 | font-size: 32px; 270 | line-height: 1.2; 271 | letter-spacing: 1px; 272 | }`; 273 | const output = ` 274 | /*no-convert-vw*/ 275 | h1 { 276 | margin: 0 0 0.26667rem 0.26667rem; 277 | font-size: 0.42667rem; 278 | line-height: 1.2; 279 | letter-spacing: 0.01333rem; 280 | }`; 281 | const processed = postcss( 282 | pxtorem({ commentOfDisableVW: 'no-convert-vw' }) 283 | ).process(input).css; 284 | 285 | expect(processed).toBe(output); 286 | }); 287 | 288 | it('should not convert to vw in whole file with new disabled comment anywhere', () => { 289 | const input = ` 290 | h1 { 291 | margin: 0 0 20px 20px; 292 | font-size: 32px; 293 | line-height: 1.2; 294 | letter-spacing: 1px; 295 | } 296 | h2 { 297 | margin: 0 0 20px 20px; 298 | font-size: 32px; 299 | line-height: 1.2; 300 | letter-spacing: 1px; 301 | } 302 | /*no-convert-vw*/ 303 | h3 { 304 | margin: 0 0 20px 20px; 305 | font-size: 32px; 306 | line-height: 1.2; 307 | letter-spacing: 1px; 308 | }`; 309 | const output = ` 310 | h1 { 311 | margin: 0 0 0.26667rem 0.26667rem; 312 | font-size: 0.42667rem; 313 | line-height: 1.2; 314 | letter-spacing: 0.01333rem; 315 | } 316 | h2 { 317 | margin: 0 0 0.26667rem 0.26667rem; 318 | font-size: 0.42667rem; 319 | line-height: 1.2; 320 | letter-spacing: 0.01333rem; 321 | } 322 | /*no-convert-vw*/ 323 | h3 { 324 | margin: 0 0 0.26667rem 0.26667rem; 325 | font-size: 0.42667rem; 326 | line-height: 1.2; 327 | letter-spacing: 0.01333rem; 328 | }`; 329 | const processed = postcss( 330 | pxtorem({ commentOfDisableVW: 'no-convert-vw' }) 331 | ).process(input).css; 332 | 333 | expect(processed).toBe(output); 334 | }); 335 | 336 | it('should not convert to rem in whole file with disabled comment at the top and some property with disabled comment', () => { 337 | const input = ` 338 | /*disable-convert-rem*/ 339 | h1 { 340 | margin: 0 0 20px 20px; 341 | font-size: 32px; /*no*/ 342 | line-height: 1.2; 343 | letter-spacing: 1px; 344 | }`; 345 | const output = ` 346 | /*disable-convert-rem*/ 347 | h1 { 348 | margin: 0 0 2.66667vw 2.66667vw; 349 | font-size: 32px; /*no*/ 350 | line-height: 1.2; 351 | letter-spacing: 0.13333vw; 352 | }`; 353 | const processed = postcss(pxtorem()).process(input).css; 354 | 355 | expect(processed).toBe(output); 356 | }); 357 | 358 | it('should not convert to rem in whole file with disabled comment anywhere and some property with disabled comment', () => { 359 | const input = ` 360 | h1 { 361 | margin: 0 0 20px 20px; 362 | font-size: 32px; /*no*/ 363 | line-height: 1.2; 364 | letter-spacing: 1px; 365 | } 366 | h2 { 367 | margin: 0 0 20px 20px; 368 | font-size: 32px; /*no*/ 369 | line-height: 1.2; 370 | letter-spacing: 1px; 371 | } 372 | /*disable-convert-rem*/ 373 | h3 { 374 | margin: 0 0 20px 20px; 375 | font-size: 32px; /*no*/ 376 | line-height: 1.2; 377 | letter-spacing: 1px; 378 | }`; 379 | const output = ` 380 | h1 { 381 | margin: 0 0 2.66667vw 2.66667vw; 382 | font-size: 32px; /*no*/ 383 | line-height: 1.2; 384 | letter-spacing: 0.13333vw; 385 | } 386 | h2 { 387 | margin: 0 0 2.66667vw 2.66667vw; 388 | font-size: 32px; /*no*/ 389 | line-height: 1.2; 390 | letter-spacing: 0.13333vw; 391 | } 392 | /*disable-convert-rem*/ 393 | h3 { 394 | margin: 0 0 2.66667vw 2.66667vw; 395 | font-size: 32px; /*no*/ 396 | line-height: 1.2; 397 | letter-spacing: 0.13333vw; 398 | }`; 399 | const processed = postcss(pxtorem()).process(input).css; 400 | 401 | expect(processed).toBe(output); 402 | }); 403 | 404 | it('should not convert to rem in whole file with new disabled comment at the top and some property with new disabled comment', () => { 405 | const input = ` 406 | /*no-convert-rem*/ 407 | h1 { 408 | margin: 0 0 20px 20px; 409 | font-size: 32px; /*no-convert*/ 410 | line-height: 1.2; 411 | letter-spacing: 1px; 412 | }`; 413 | const output = ` 414 | /*no-convert-rem*/ 415 | h1 { 416 | margin: 0 0 2.66667vw 2.66667vw; 417 | font-size: 32px; /*no-convert*/ 418 | line-height: 1.2; 419 | letter-spacing: 0.13333vw; 420 | }`; 421 | const processed = postcss( 422 | pxtorem({ 423 | keepRuleComment: 'no-convert', 424 | commentOfDisableRem: 'no-convert-rem', 425 | }) 426 | ).process(input).css; 427 | 428 | expect(processed).toBe(output); 429 | }); 430 | 431 | it('should not convert to rem in whole file with new disabled comment anywhere and some property with new disabled comment', () => { 432 | const input = ` 433 | h1 { 434 | margin: 0 0 20px 20px; 435 | font-size: 32px; /*no-convert*/ 436 | line-height: 1.2; 437 | letter-spacing: 1px; 438 | } 439 | h2 { 440 | margin: 0 0 20px 20px; 441 | font-size: 32px; /*no-convert*/ 442 | line-height: 1.2; 443 | letter-spacing: 1px; 444 | } 445 | /*no-convert-rem*/ 446 | h3 { 447 | margin: 0 0 20px 20px; 448 | font-size: 32px; /*no-convert*/ 449 | line-height: 1.2; 450 | letter-spacing: 1px; 451 | }`; 452 | const output = ` 453 | h1 { 454 | margin: 0 0 2.66667vw 2.66667vw; 455 | font-size: 32px; /*no-convert*/ 456 | line-height: 1.2; 457 | letter-spacing: 0.13333vw; 458 | } 459 | h2 { 460 | margin: 0 0 2.66667vw 2.66667vw; 461 | font-size: 32px; /*no-convert*/ 462 | line-height: 1.2; 463 | letter-spacing: 0.13333vw; 464 | } 465 | /*no-convert-rem*/ 466 | h3 { 467 | margin: 0 0 2.66667vw 2.66667vw; 468 | font-size: 32px; /*no-convert*/ 469 | line-height: 1.2; 470 | letter-spacing: 0.13333vw; 471 | }`; 472 | const processed = postcss( 473 | pxtorem({ 474 | keepRuleComment: 'no-convert', 475 | commentOfDisableRem: 'no-convert-rem', 476 | }) 477 | ).process(input).css; 478 | 479 | expect(processed).toBe(output); 480 | }); 481 | 482 | it('should not convert to vw in whole file with disabled comment at the top and some property with disabled comment', () => { 483 | const input = ` 484 | /*disable-convert-vw*/ 485 | h1 { 486 | margin: 0 0 20px 20px; 487 | font-size: 32px; /*no*/ 488 | line-height: 1.2; 489 | letter-spacing: 1px; 490 | }`; 491 | const output = ` 492 | /*disable-convert-vw*/ 493 | h1 { 494 | margin: 0 0 0.26667rem 0.26667rem; 495 | font-size: 32px; /*no*/ 496 | line-height: 1.2; 497 | letter-spacing: 0.01333rem; 498 | }`; 499 | const processed = postcss(pxtorem()).process(input).css; 500 | 501 | expect(processed).toBe(output); 502 | }); 503 | 504 | it('should not convert to vw in whole file with disabled comment anywhere and some property with disabled comment', () => { 505 | const input = ` 506 | h1 { 507 | margin: 0 0 20px 20px; 508 | font-size: 32px; /*no*/ 509 | line-height: 1.2; 510 | letter-spacing: 1px; 511 | } 512 | h2 { 513 | margin: 0 0 20px 20px; 514 | font-size: 32px; /*no*/ 515 | line-height: 1.2; 516 | letter-spacing: 1px; 517 | } 518 | /*disable-convert-vw*/ 519 | h3 { 520 | margin: 0 0 20px 20px; 521 | font-size: 32px; /*no*/ 522 | line-height: 1.2; 523 | letter-spacing: 1px; 524 | }`; 525 | const output = ` 526 | h1 { 527 | margin: 0 0 0.26667rem 0.26667rem; 528 | font-size: 32px; /*no*/ 529 | line-height: 1.2; 530 | letter-spacing: 0.01333rem; 531 | } 532 | h2 { 533 | margin: 0 0 0.26667rem 0.26667rem; 534 | font-size: 32px; /*no*/ 535 | line-height: 1.2; 536 | letter-spacing: 0.01333rem; 537 | } 538 | /*disable-convert-vw*/ 539 | h3 { 540 | margin: 0 0 0.26667rem 0.26667rem; 541 | font-size: 32px; /*no*/ 542 | line-height: 1.2; 543 | letter-spacing: 0.01333rem; 544 | }`; 545 | const processed = postcss(pxtorem()).process(input).css; 546 | 547 | expect(processed).toBe(output); 548 | }); 549 | 550 | it('should not convert to vw in whole file with new disabled comment at the top and some property with new disabled comment', () => { 551 | const input = ` 552 | /*no-convert-vw*/ 553 | h1 { 554 | margin: 0 0 20px 20px; 555 | font-size: 32px; /*no-convert*/ 556 | line-height: 1.2; 557 | letter-spacing: 1px; 558 | }`; 559 | const output = ` 560 | /*no-convert-vw*/ 561 | h1 { 562 | margin: 0 0 0.26667rem 0.26667rem; 563 | font-size: 32px; /*no-convert*/ 564 | line-height: 1.2; 565 | letter-spacing: 0.01333rem; 566 | }`; 567 | const processed = postcss( 568 | pxtorem({ 569 | keepRuleComment: 'no-convert', 570 | commentOfDisableVW: 'no-convert-vw', 571 | }) 572 | ).process(input).css; 573 | 574 | expect(processed).toBe(output); 575 | }); 576 | 577 | it('should not convert to vw in whole file with new disabled comment anywhere and some property with new disabled comment', () => { 578 | const input = ` 579 | h1 { 580 | margin: 0 0 20px 20px; 581 | font-size: 32px; /*no-convert*/ 582 | line-height: 1.2; 583 | letter-spacing: 1px; 584 | } 585 | h2 { 586 | margin: 0 0 20px 20px; 587 | font-size: 32px; /*no-convert*/ 588 | line-height: 1.2; 589 | letter-spacing: 1px; 590 | } 591 | /*no-convert-vw*/ 592 | h3 { 593 | margin: 0 0 20px 20px; 594 | font-size: 32px; /*no-convert*/ 595 | line-height: 1.2; 596 | letter-spacing: 1px; 597 | }`; 598 | const output = ` 599 | h1 { 600 | margin: 0 0 0.26667rem 0.26667rem; 601 | font-size: 32px; /*no-convert*/ 602 | line-height: 1.2; 603 | letter-spacing: 0.01333rem; 604 | } 605 | h2 { 606 | margin: 0 0 0.26667rem 0.26667rem; 607 | font-size: 32px; /*no-convert*/ 608 | line-height: 1.2; 609 | letter-spacing: 0.01333rem; 610 | } 611 | /*no-convert-vw*/ 612 | h3 { 613 | margin: 0 0 0.26667rem 0.26667rem; 614 | font-size: 32px; /*no-convert*/ 615 | line-height: 1.2; 616 | letter-spacing: 0.01333rem; 617 | }`; 618 | const processed = postcss( 619 | pxtorem({ 620 | keepRuleComment: 'no-convert', 621 | commentOfDisableVW: 'no-convert-vw', 622 | }) 623 | ).process(input).css; 624 | 625 | expect(processed).toBe(output); 626 | }); 627 | 628 | it('should not convert to rem while `rem` in baseSize is undefined', () => { 629 | const input = ` 630 | h1 { 631 | margin: 0 0 20px 20px; 632 | font-size: 32px; 633 | line-height: 1.2; 634 | letter-spacing: 1px; 635 | }`; 636 | const output = ` 637 | h1 { 638 | margin: 0 0 1.33333vw 1.33333vw; 639 | font-size: 2.13333vw; 640 | line-height: 1.2; 641 | letter-spacing: 0.06667vw; 642 | }`; 643 | const processed = postcss( 644 | pxtorem({ 645 | baseSize: { 646 | vw: 15, 647 | }, 648 | }) 649 | ).process(input).css; 650 | 651 | expect(processed).toBe(output); 652 | }); 653 | 654 | it('should not convert to vw while `vw` in baseSize is undefined', () => { 655 | const input = ` 656 | h1 { 657 | margin: 0 0 20px 20px; 658 | font-size: 32px; 659 | line-height: 1.2; 660 | letter-spacing: 1px; 661 | }`; 662 | const output = ` 663 | h1 { 664 | margin: 0 0 0.53333rem 0.53333rem; 665 | font-size: 0.85333rem; 666 | line-height: 1.2; 667 | letter-spacing: 0.02667rem; 668 | }`; 669 | const processed = postcss( 670 | pxtorem({ 671 | baseSize: { 672 | rem: 37.5, 673 | }, 674 | }) 675 | ).process(input).css; 676 | 677 | expect(processed).toBe(output); 678 | }); 679 | }); 680 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | require('./px-to-rem-and-vw'); 2 | require('./ignore-some-situations'); 3 | -------------------------------------------------------------------------------- /test/px-to-rem-and-vw.js: -------------------------------------------------------------------------------- 1 | const postcss = require('postcss'); 2 | const expect = require('expect'); 3 | const pxtorem = require('../index.js'); 4 | 5 | describe('basic convert', () => { 6 | it('should work in basic example', () => { 7 | const input = ` 8 | h1 { 9 | margin: 0 0 20px 20px; 10 | font-size: 32px; 11 | line-height: 1.2; 12 | letter-spacing: 1px; 13 | }`; 14 | const output = ` 15 | h1 { 16 | margin: 0 0 0.26667rem 0.26667rem; 17 | margin: 0 0 2.66667vw 2.66667vw; 18 | font-size: 0.42667rem; 19 | font-size: 4.26667vw; 20 | line-height: 1.2; 21 | letter-spacing: 0.01333rem; 22 | letter-spacing: 0.13333vw; 23 | }`; 24 | const processed = postcss(pxtorem()).process(input).css; 25 | 26 | expect(processed).toBe(output); 27 | }); 28 | 29 | it('should work in basic example2', () => { 30 | const input = ` 31 | h1 { 32 | width: 20px; 33 | height: 12vw; 34 | font-size: 0.5rem; 35 | font-size: 1px; 36 | }`; 37 | const output = ` 38 | h1 { 39 | width: 0.26667rem; 40 | width: 2.66667vw; 41 | height: 12vw; 42 | font-size: 0.5rem; 43 | font-size: 0.01333rem; 44 | font-size: 0.13333vw; 45 | }`; 46 | const processed = postcss(pxtorem()).process(input).css; 47 | 48 | expect(processed).toBe(output); 49 | }); 50 | 51 | it('should work in customize base size', () => { 52 | const input = ` 53 | h1 { 54 | margin: 0 0 20px 20px; 55 | font-size: 32px; 56 | line-height: 1.2; 57 | letter-spacing: 1px; 58 | }`; 59 | const output = ` 60 | h1 { 61 | margin: 0 0 0.1626rem 0.1626rem; 62 | margin: 0 0 1.62602vw 1.62602vw; 63 | font-size: 0.26016rem; 64 | font-size: 2.60163vw; 65 | line-height: 1.2; 66 | letter-spacing: 0.00813rem; 67 | letter-spacing: 0.0813vw; 68 | }`; 69 | const processed = postcss( 70 | pxtorem({ 71 | baseSize: { 72 | rem: 123, 73 | vw: 12.3, 74 | }, 75 | }) 76 | ).process(input).css; 77 | 78 | expect(processed).toBe(output); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /workflow.config.js: -------------------------------------------------------------------------------- 1 | const { Workflow, Tasks } = require('task-workflow'); 2 | 3 | new Workflow('to-self', { 4 | description: '提交到远程', 5 | steps: [ 6 | { 7 | name: '构建', 8 | skip: async () => 9 | !(await Tasks.AskFor.shouldContinue({ message: '是否需要构建?' })()), 10 | use: Tasks.Shell.run({ cmd: 'npm run build' }), 11 | }, 12 | { name: '测试', use: Tasks.Shell.run({ cmd: 'npm run test' }) }, 13 | { name: '获取提交信息', use: Tasks.AskFor.commitMessage() }, 14 | { name: '提交', use: Tasks.Git.commit(message => [{ message }]) }, 15 | { name: '推送', use: Tasks.Git.push() }, 16 | ], 17 | }); 18 | 19 | new Workflow('release:major', { 20 | description: 'major版本', 21 | steps: [ 22 | { 23 | name: '构建', 24 | skip: async () => 25 | !(await Tasks.AskFor.shouldContinue({ message: '是否需要构建?' })()), 26 | use: Tasks.Shell.run({ cmd: 'npm run build' }), 27 | }, 28 | { name: '测试', use: Tasks.Shell.run({ cmd: 'npm run test' }) }, 29 | { name: '获取提交信息', use: Tasks.AskFor.commitMessage() }, 30 | { name: '提交', use: Tasks.Git.commit(message => [{ message }]) }, 31 | { 32 | name: '打版本', 33 | use: Tasks.Shell.run({ 34 | cmd: 'npm run version:major', 35 | }), 36 | }, 37 | { 38 | name: '生成日志', 39 | use: Tasks.Shell.run({ 40 | cmd: 'npm run changelog', 41 | }), 42 | }, 43 | { 44 | name: '提交', 45 | use: Tasks.Git.commit({ 46 | message: 'chore(release): update version and changelog', 47 | }), 48 | }, 49 | { name: '推送', use: Tasks.Git.push() }, 50 | { 51 | name: '推送tag', 52 | use: Tasks.Shell.run({ cmd: 'git push --tags' }), 53 | }, 54 | { 55 | name: '发布到npm', 56 | skip: async () => 57 | !(await Tasks.AskFor.shouldContinue({ message: '是否更新至npm?' })()), 58 | use: Tasks.Shell.run({ cmd: 'npm publish' }), 59 | }, 60 | ], 61 | }); 62 | 63 | new Workflow('release:minor', { 64 | description: 'minor版本', 65 | steps: [ 66 | { 67 | name: '构建', 68 | skip: async () => 69 | !(await Tasks.AskFor.shouldContinue({ message: '是否需要构建?' })()), 70 | use: Tasks.Shell.run({ cmd: 'npm run build' }), 71 | }, 72 | { name: '测试', use: Tasks.Shell.run({ cmd: 'npm run test' }) }, 73 | { name: '获取提交信息', use: Tasks.AskFor.commitMessage() }, 74 | { name: '提交', use: Tasks.Git.commit(message => [{ message }]) }, 75 | { 76 | name: '打版本', 77 | use: Tasks.Shell.run({ 78 | cmd: 'npm run version:minor', 79 | }), 80 | }, 81 | { 82 | name: '生成日志', 83 | use: Tasks.Shell.run({ 84 | cmd: 'npm run changelog', 85 | }), 86 | }, 87 | { 88 | name: '提交', 89 | use: Tasks.Git.commit({ 90 | message: 'chore(release): update version and changelog', 91 | }), 92 | }, 93 | { name: '推送', use: Tasks.Git.push() }, 94 | { 95 | name: '推送tag', 96 | use: Tasks.Shell.run({ cmd: 'git push --tags' }), 97 | }, 98 | { 99 | name: '发布到npm', 100 | skip: async () => 101 | !(await Tasks.AskFor.shouldContinue({ message: '是否更新至npm?' })()), 102 | use: Tasks.Shell.run({ cmd: 'npm publish' }), 103 | }, 104 | ], 105 | }); 106 | 107 | new Workflow('release:patch', { 108 | description: 'patch版本', 109 | steps: [ 110 | { 111 | name: '构建', 112 | skip: async () => 113 | !(await Tasks.AskFor.shouldContinue({ message: '是否需要构建?' })()), 114 | use: Tasks.Shell.run({ cmd: 'npm run build' }), 115 | }, 116 | { name: '测试', use: Tasks.Shell.run({ cmd: 'npm run test' }) }, 117 | { name: '获取提交信息', use: Tasks.AskFor.commitMessage() }, 118 | { name: '提交', use: Tasks.Git.commit(message => [{ message }]) }, 119 | { 120 | name: '打版本', 121 | use: Tasks.Shell.run({ 122 | cmd: 'npm run version:patch', 123 | }), 124 | }, 125 | { 126 | name: '生成日志', 127 | use: Tasks.Shell.run({ 128 | cmd: 'npm run changelog', 129 | }), 130 | }, 131 | { 132 | name: '提交', 133 | use: Tasks.Git.commit({ 134 | message: 'chore(release): update version and changelog', 135 | }), 136 | }, 137 | { name: '推送', use: Tasks.Git.push() }, 138 | { 139 | name: '推送tag', 140 | use: Tasks.Shell.run({ cmd: 'git push --tags' }), 141 | }, 142 | { 143 | name: '发布到npm', 144 | skip: async () => 145 | !(await Tasks.AskFor.shouldContinue({ message: '是否更新至npm?' })()), 146 | use: Tasks.Shell.run({ cmd: 'npm publish' }), 147 | }, 148 | ], 149 | }); 150 | --------------------------------------------------------------------------------