├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── lib ├── utils.js └── wxmlTagMap.js ├── package.json └── test └── index.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{json,yml}] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn-error.log 4 | package-lock.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .npmignore 2 | .gitignore 3 | .editorconfig 4 | 5 | node_modules/ 6 | npm-debug.log 7 | yarn.lock 8 | 9 | *.test.js 10 | .travis.yml 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: yarn 3 | node_js: 4 | - stable 5 | - "6" 6 | - "4" 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-present, 美团点评 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PostCSS Wxss 2 | 3 | [PostCSS] plugin for wxss. 4 | 5 | 专门为 wxss 格式化处理的的一个 postcss 插件,特别是在做 css 转 wxss 的时候好用到爆。 6 | 7 | ## 实现的功能 8 | - 清理 wxss 不支持的选择器。 9 | - 清理 wxss 不支持的注释。 10 | - 转换 rem 单位到 rpx。 11 | - 转换 Web 的标签选择器到小程序的 class 选择器。 12 | - style scoped(postcss插件部分)。 13 | 14 | ``` css 15 | /* 被清理 */ 16 | * { 17 | margin: 100px 18 | } 19 | 20 | /* 保持原样 */ 21 | view { 22 | width: 50rpx; 23 | } 24 | .container { 25 | width: 7.5rem; 26 | font-size: .24rem 27 | } 28 | 29 | /* Web 标签转换 */ 30 | div { 31 | width: 50rpx; 32 | } 33 | ul li { 34 | width: 50rpx; 35 | } 36 | body { 37 | width: 50rpx; 38 | } 39 | ``` 40 | 41 | ``` css 42 | view { 43 | width: 50rpx; 44 | } 45 | .container { 46 | width: 50rpx; 47 | font-size: 24.4rpx 48 | } 49 | ._div { 50 | width: 50rpx; 51 | } 52 | ._ul ._li { 53 | width: 50rpx; 54 | } 55 | page { 56 | width: 50rpx; 57 | } 58 | ``` 59 | 60 | ## Usage 61 | 62 | ```js 63 | postcss([ require('postcss-mpvue-wxss') ]) 64 | ``` 65 | 66 | or use `.postcssrc.js` 67 | ``` 68 | // https://github.com/michael-ciniawsky/postcss-load-config 69 | const optopns = {} 70 | 71 | module.exports = { 72 | "plugins": { 73 | // to edit target browsers: use "browserslist" field in package.json 74 | "postcss-mpvue-wxss": optopns 75 | } 76 | } 77 | ``` 78 | 79 | with options: 80 | 81 | ``` 82 | const replaceTagSelectorMap = require('postcss-mpvue-wxss/lib/wxmlTagMap') 83 | 84 | const optopns = { 85 | cleanSelector: ['*'], 86 | remToRpx: 100, 87 | replaceTagSelector: Object.assign(replaceTagSelectorMap, { 88 | '*': 'view, text' // 将覆盖前面的 * 选择器被清理规则 89 | }) 90 | } 91 | ``` 92 | 93 | 更多详细文档请查阅 [postcss-mpvue-wxss](http://mpvue.com/build/postcss-mpvue-wxss)。 94 | 95 | bug 或者交流建议等请反馈到 [mpvue/issues](https://github.com/Meituan-Dianping/mpvue/issues)。 96 | 97 | See [PostCSS] docs for examples for your environment. 98 | 99 | [PostCSS]: https://github.com/postcss/postcss 100 | 101 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // postcss-mpvuew-wxss.js 2 | // https://github.com/postcss/postcss/blob/master/docs/writing-a-plugin.md 3 | 4 | const postcss = require('postcss'); 5 | const selectorParser = require('postcss-selector-parser'); 6 | const replaceTagSelector = require('./lib/wxmlTagMap'); 7 | const { isRegExp } = require('./lib/utils'); 8 | 9 | const defConfig = { 10 | cleanSelector: ['*'], 11 | cleanAtRule: [{ 12 | name: 'media', 13 | params: ['print'] 14 | }], 15 | remToRpx: 100, 16 | replaceTagSelector 17 | }; 18 | 19 | const remReg = /(\d*\.?\d+)rem/ig; 20 | const replaceRemOption = { fast: 'rem' }; 21 | 22 | module.exports = postcss.plugin('postcss-mpvue-wxss', function (options) { 23 | // Work with options here 24 | options = Object.assign({}, defConfig, options); 25 | 26 | return function (root) { 27 | // Transform CSS AST here: root, result 28 | root.walkAtRules(rule => { 29 | // 清理不支持的@开头规则 30 | for (cleanRule of options.cleanAtRule) { 31 | if (cleanRule.name !== rule.name) { 32 | continue; 33 | } 34 | if (!cleanRule.params) { 35 | return rule.remove(); 36 | } 37 | for (param of cleanRule.params) { 38 | if (isRegExp(param) && param.test(rule.params) || param === rule.params) { 39 | return rule.remove(); 40 | } 41 | } 42 | } 43 | }); 44 | 45 | root.walkRules(rule => { 46 | const { selector } = rule || {}; 47 | 48 | // rem 转换 rpx 49 | rule.replaceValues(remReg, replaceRemOption, str => { 50 | return options.remToRpx * parseFloat(str) + 'rpx'; 51 | }); 52 | 53 | rule.selector = selectorParser(function (selectors) { 54 | selectors.each(function (selector) { 55 | selector.each(function (n) { 56 | // 转换 tag 选择器 57 | if (n.type === 'tag') { 58 | const k = n.value; 59 | const v = options.replaceTagSelector[k]; 60 | if (v) { 61 | n.value = v === 'replaceToClass' ? `._${k}` : v; 62 | } 63 | } 64 | 65 | // 清理不支持的选择器 66 | if (options.cleanSelector.includes(n.value)) { 67 | // return n.value = 'view'; 68 | return rule.remove(); 69 | } 70 | }) 71 | }) 72 | }).process(selector).result; 73 | }); 74 | 75 | // 清理所有的注释 76 | root.walkComments(comment => { 77 | comment.remove(); 78 | }); 79 | }; 80 | }); 81 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | isRegExp: function (arg) { 3 | return Object.prototype.toString.call(arg) === '[object RegExp]'; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/wxmlTagMap.js: -------------------------------------------------------------------------------- 1 | // This map form [mpvue](https://github.com/Meituan-Dianping/mpvue) src/platforms/mpvue/compiler/codegen 2 | 3 | module.exports = { 4 | 'br': 'replaceToClass', 5 | 'hr': 'replaceToClass', 6 | 7 | 'p': 'replaceToClass', 8 | 'h1': 'replaceToClass', 9 | 'h2': 'replaceToClass', 10 | 'h3': 'replaceToClass', 11 | 'h4': 'replaceToClass', 12 | 'h5': 'replaceToClass', 13 | 'h6': 'replaceToClass', 14 | 'abbr': 'replaceToClass', 15 | 'address': 'replaceToClass', 16 | 'b': 'replaceToClass', 17 | 'bdi': 'replaceToClass', 18 | 'bdo': 'replaceToClass', 19 | 'blockquote': 'replaceToClass', 20 | 'cite': 'replaceToClass', 21 | 'code': 'replaceToClass', 22 | 'del': 'replaceToClass', 23 | 'ins': 'replaceToClass', 24 | 'dfn': 'replaceToClass', 25 | 'em': 'replaceToClass', 26 | 'strong': 'replaceToClass', 27 | 'samp': 'replaceToClass', 28 | 'kbd': 'replaceToClass', 29 | 'var': 'replaceToClass', 30 | 'i': 'replaceToClass', 31 | 'mark': 'replaceToClass', 32 | 'pre': 'replaceToClass', 33 | 'q': 'replaceToClass', 34 | 'ruby': 'replaceToClass', 35 | 'rp': 'replaceToClass', 36 | 'rt': 'replaceToClass', 37 | 's': 'replaceToClass', 38 | 'small': 'replaceToClass', 39 | 'sub': 'replaceToClass', 40 | 'sup': 'replaceToClass', 41 | 'time': 'replaceToClass', 42 | 'u': 'replaceToClass', 43 | 'wbr': 'replaceToClass', 44 | 45 | // 表单元素 46 | 'form': 'replaceToClass', 47 | 'input': 'replaceToClass', 48 | 'textarea': 'replaceToClass', 49 | 'button': 'replaceToClass', 50 | 'select': 'replaceToClass', 51 | 'option': 'replaceToClass', 52 | 'optgroup': 'replaceToClass', 53 | 'label': 'replaceToClass', 54 | 'fieldset': 'replaceToClass', 55 | 'datalist': 'replaceToClass', 56 | 'legend': 'replaceToClass', 57 | 'output': 'replaceToClass', 58 | 59 | // 框架 60 | 'iframe': 'replaceToClass', 61 | // 图像 62 | 'img': 'replaceToClass', 63 | 'canvas': 'replaceToClass', 64 | 'figure': 'replaceToClass', 65 | 'figcaption': 'replaceToClass', 66 | 67 | // 音视频 68 | 'audio': 'replaceToClass', 69 | 'source': 'replaceToClass', 70 | 'video': 'replaceToClass', 71 | 'track': 'replaceToClass', 72 | // 链接 73 | 'a': 'replaceToClass', 74 | 'nav': 'replaceToClass', 75 | 'link': 'replaceToClass', 76 | // 列表 77 | 'ul': 'replaceToClass', 78 | 'ol': 'replaceToClass', 79 | 'li': 'replaceToClass', 80 | 'dl': 'replaceToClass', 81 | 'dt': 'replaceToClass', 82 | 'dd': 'replaceToClass', 83 | 'menu': 'replaceToClass', 84 | 'command': 'replaceToClass', 85 | 86 | // 表格table 87 | 'table': 'replaceToClass', 88 | 'caption': 'replaceToClass', 89 | 'th': 'replaceToClass', 90 | 'td': 'replaceToClass', 91 | 'tr': 'replaceToClass', 92 | 'thead': 'replaceToClass', 93 | 'tbody': 'replaceToClass', 94 | 'tfoot': 'replaceToClass', 95 | 'col': 'replaceToClass', 96 | 'colgroup': 'replaceToClass', 97 | // 样式 节 98 | 'div': 'replaceToClass', 99 | 'main': 'replaceToClass', 100 | 'span': 'replaceToClass', 101 | 'header': 'replaceToClass', 102 | 'footer': 'replaceToClass', 103 | 'section': 'replaceToClass', 104 | 'article': 'replaceToClass', 105 | 'aside': 'replaceToClass', 106 | 'details': 'replaceToClass', 107 | 'dialog': 'replaceToClass', 108 | 'summary': 'replaceToClass', 109 | 110 | 'progress': 'replaceToClass', 111 | 'meter': 'replaceToClass', // todo 112 | 'head': 'replaceToClass', // todo 113 | 'meta': 'replaceToClass', // todo 114 | 'base': 'replaceToClass', // todo 115 | 'map': 'replaceToClass', // TODO不是很恰当 116 | 'area': 'replaceToClass', // j结合map使用 117 | 118 | 'script': 'replaceToClass', 119 | 'noscript': 'replaceToClass', 120 | 'embed': 'replaceToClass', 121 | 'object': 'replaceToClass', 122 | 'param': 'replaceToClass', 123 | 'body': 'page', 124 | 'html': 'page' 125 | } 126 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-mpvue-wxss", 3 | "version": "1.0.0", 4 | "description": "PostCSS plugin for wxss", 5 | "keywords": [ 6 | "postcss", 7 | "css", 8 | "postcss-plugin", 9 | "wxss" 10 | ], 11 | "author": "anchengjian ", 12 | "license": "MIT", 13 | "repository": "git@github.com:mpvue/postcss-mpvue-wxss.git", 14 | "homepage": "https://github.com/mpvue/postcss-mpvue-wxss", 15 | "dependencies": { 16 | "postcss": "^6.0.8", 17 | "postcss-selector-parser": "^2.2.3" 18 | }, 19 | "devDependencies": { 20 | "jest": "^20.0.0" 21 | }, 22 | "scripts": { 23 | "test": "jest test" 24 | }, 25 | "eslintConfig": { 26 | "env": { 27 | "jest": true 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | var postcss = require('postcss'); 2 | 3 | var plugin = require('../'); 4 | 5 | function run(input, output, opts) { 6 | return postcss([plugin(opts)]).process(input) 7 | .then(result => { 8 | expect(result.css).toEqual(output); 9 | expect(result.warnings().length).toBe(0); 10 | }); 11 | } 12 | 13 | it('test cleanComments', () => { 14 | const input = ` 15 | /* Comments */`; 16 | const output = ``; 17 | const options = {}; 18 | return run(input, output, options); 19 | }); 20 | 21 | it('test cleanSelector', () => { 22 | const input = ` 23 | * { 24 | margin: 100px 25 | } 26 | *,:after,:before{ 27 | box-sizing:inherit; 28 | } 29 | @media print { 30 | body { 31 | display: block; 32 | } 33 | }`; 34 | const output = ``; 35 | const options = {}; 36 | return run(input, output, options); 37 | }); 38 | 39 | it('test cleanSelector with options', () => { 40 | const input = ` 41 | * { 42 | margin: 100px 43 | } 44 | aaa { 45 | margin: 100px 46 | } 47 | bbb { 48 | margin: 100px 49 | } 50 | @media screen and (max-width: 300px) { 51 | body { 52 | display: block; 53 | } 54 | }`; 55 | const output = ``; 56 | const options = { 57 | cleanSelector: ['aaa', 'bbb', '*'], 58 | cleanAtRule: [{ 59 | name: 'media', 60 | params: [/screen/] 61 | }] 62 | }; 63 | return run(input, output, options); 64 | }); 65 | 66 | it('test remToRpx', () => { 67 | const input = ` 68 | .container { 69 | width: 50rem; 70 | font-size: 24.4rem 71 | }`; 72 | const output = ` 73 | .container { 74 | width: 5000rpx; 75 | font-size: 2440rpx 76 | }`; 77 | const options = {}; 78 | return run(input, output, options); 79 | }); 80 | 81 | it('test remToRpx with options', () => { 82 | const input = ` 83 | .container { 84 | width: 50rem; 85 | font-size: 24.4rem 86 | }`; 87 | const output = ` 88 | .container { 89 | width: 50rpx; 90 | font-size: 24.4rpx 91 | }`; 92 | const options = { 93 | remToRpx: 1 94 | }; 95 | return run(input, output, options); 96 | }); 97 | 98 | it('test replaceTagSelector', () => { 99 | const input = ` 100 | div { 101 | width: 50rpx; 102 | } 103 | ul li { 104 | width: 50rpx; 105 | } 106 | ul>li { 107 | width: 50rpx; 108 | } 109 | .input-box { 110 | border: 1px solid red; 111 | } 112 | view { 113 | width: 50rpx; 114 | }`; 115 | const output = ` 116 | ._div { 117 | width: 50rpx; 118 | } 119 | ._ul ._li { 120 | width: 50rpx; 121 | } 122 | ._ul>._li { 123 | width: 50rpx; 124 | } 125 | .input-box { 126 | border: 1px solid red; 127 | } 128 | view { 129 | width: 50rpx; 130 | }`; 131 | const options = {}; 132 | return run(input, output, options); 133 | }); 134 | 135 | it('test replaceTagSelector with options', () => { 136 | const input = ` 137 | aaa { 138 | width: 50rpx; 139 | } 140 | bbb { 141 | width: 60rpx; 142 | }`; 143 | const output = ` 144 | ._aaa { 145 | width: 50rpx; 146 | } 147 | ccc, .ddd { 148 | width: 60rpx; 149 | }`; 150 | const options = { 151 | replaceTagSelector: { 152 | aaa: 'replaceToClass', 153 | bbb: 'ccc, .ddd' 154 | } 155 | }; 156 | return run(input, output, options); 157 | }); 158 | 159 | it('test readme', () => { 160 | const input = ` 161 | /* 被清理 */ 162 | * { 163 | margin: 100px 164 | } 165 | 166 | /* 保持原样 */ 167 | view { 168 | width: 50rpx; 169 | } 170 | .container { 171 | width: 7.5rem; 172 | font-size: .24rem 173 | } 174 | 175 | /* Web 标签转换 */ 176 | div { 177 | width: 50rpx; 178 | } 179 | ul li { 180 | width: 50rpx; 181 | } 182 | body { 183 | width: 50rpx; 184 | }`; 185 | const output = ` 186 | view { 187 | width: 50rpx; 188 | } 189 | .container { 190 | width: 750rpx; 191 | font-size: 24rpx 192 | } 193 | ._div { 194 | width: 50rpx; 195 | } 196 | ._ul ._li { 197 | width: 50rpx; 198 | } 199 | page { 200 | width: 50rpx; 201 | }`; 202 | const options = {}; 203 | return run(input, output, options); 204 | }); 205 | --------------------------------------------------------------------------------