├── CHANGELOG.md ├── demo ├── .gitignore ├── .babelrc ├── components │ ├── common.less │ ├── Wrap.js │ ├── Wrap.less │ ├── App.js │ └── App.less ├── css-flat.config.js ├── index.js ├── index.html ├── build.sh ├── package.json └── webpack.config.js ├── index.js ├── .gitattributes ├── .gitignore ├── test ├── css-flat.config.js ├── moduleTestCases │ └── class-names │ │ ├── colors.css │ │ ├── test.png │ │ ├── expected.css │ │ └── source.css ├── moduleMinimizeTestCases │ └── keyframes-and-animation │ │ ├── expected.css │ │ └── source.css ├── moduleTest.js ├── moduleMinimizeTest.js └── helpers.js ├── .editorconfig ├── .travis.yml ├── src ├── declValueMap.js ├── error.js ├── pseudoMap.js ├── getLoaderConfig.js ├── getSelectorType.js ├── getSelectorName.js ├── declPropMap.js ├── loader.js └── processCss.js ├── LICENSE ├── package.json └── README.md /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./src/loader"); 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | yarn.lock -diff 2 | * text=auto 3 | bin/* eol=lf 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | npm-debug.log 4 | .idea 5 | -------------------------------------------------------------------------------- /test/css-flat.config.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { plugins: [] } 3 | -------------------------------------------------------------------------------- /demo/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /demo/components/common.less: -------------------------------------------------------------------------------- 1 | @red: red; 2 | .title1 { 3 | color: #666666; 4 | } -------------------------------------------------------------------------------- /test/moduleTestCases/class-names/colors.css: -------------------------------------------------------------------------------- 1 | @value blue: #0c77f8; 2 | @value red: #ff0000; 3 | @value green: #aaf200; 4 | -------------------------------------------------------------------------------- /test/moduleTestCases/class-names/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangjinzhou/css-flat-loader/HEAD/test/moduleTestCases/class-names/test.png -------------------------------------------------------------------------------- /test/moduleTestCases/class-names/expected.css: -------------------------------------------------------------------------------- 1 | ._class-1_, ._class-10_ ._bar-1_ { 2 | color: green; 3 | } 4 | .im{ 5 | color: aliceblue; 6 | } 7 | -------------------------------------------------------------------------------- /demo/css-flat.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceMap: true, 3 | plugins: [ 4 | require('precss')(), 5 | require('autoprefixer')(), 6 | ], 7 | } 8 | 9 | -------------------------------------------------------------------------------- /test/moduleTestCases/class-names/source.css: -------------------------------------------------------------------------------- 1 | .title{ 2 | background: #ffffff url("http://img.alicdn.com/tps/TB1ld1GNFXXXXXLapXXXXXXXXXX-200-200.png"); 3 | } 4 | :global(.test){ 5 | color: red; 6 | } 7 | 8 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './components/App'; 4 | 5 | ReactDOM.render( 6 | , 7 | document.body.appendChild(document.createElement('div')) 8 | ); 9 | -------------------------------------------------------------------------------- /demo/components/Wrap.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import style from './Wrap.less'; 3 | 4 | export default () => { 5 | return ( 6 |

7 | Hello World 8 |

9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | CSS Flat Demo 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 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 | -------------------------------------------------------------------------------- /demo/components/Wrap.less: -------------------------------------------------------------------------------- 1 | .test { 2 | background-color: #cccccc; 3 | border: 1px solid #DDDDDD; 4 | font-size: 51px; 5 | } 6 | .title { 7 | display: flex; 8 | font-size: 50px; 9 | margin: 0 auto; 10 | border-bottom-left-radius: 20px; 11 | } 12 | -------------------------------------------------------------------------------- /demo/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import style from './App.less'; 3 | import Wrap from './Wrap'; 4 | export default () => { 5 | return ( 6 |
7 |

8 | Hello World 9 |

10 | 11 |
12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /test/moduleMinimizeTestCases/keyframes-and-animation/expected.css: -------------------------------------------------------------------------------- 1 | @keyframes _bounce_{0%{transform:translateY(-100%);opacity:0}5%{transform:translateY(-50%);opacity:1}}@keyframes _bounce2_{0%{transform:translateY(-100%);opacity:0}50%{transform:translateY(-50%);opacity:1}} 2 | 3 | /* comment */._bounce_{animation-name:_bounce_;animation:_bounce2_ 1s ease;z-index:1442} -------------------------------------------------------------------------------- /test/moduleMinimizeTestCases/keyframes-and-animation/source.css: -------------------------------------------------------------------------------- 1 | @keyframes bounce { 2 | 0% { 3 | transform: translateY(-100%); 4 | opacity: 0; 5 | } 6 | 5% { 7 | transform: translateY(-50%); 8 | opacity: 1; 9 | } 10 | } 11 | 12 | @keyframes bounce2 { 13 | 0% { 14 | transform: translateY(-100%); 15 | opacity: 0; 16 | } 17 | 50% { 18 | transform: translateY(-50%); 19 | opacity: 1; 20 | } 21 | } 22 | 23 | /* comment */ 24 | .bounce { 25 | animation-name: bounce; 26 | animation: bounce2 1s ease; 27 | z-index: 1442; 28 | } 29 | -------------------------------------------------------------------------------- /demo/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | # number of demos 4 | num=6 5 | 6 | mkdir -p dist 7 | 8 | count=1; 9 | while [ $count -le $num ]; do 10 | cp -rf demo0$count dist 11 | echo -e "cp demo0$count succeed" 12 | pushd dist/demo0$count 13 | ../../node_modules/.bin/webpack 14 | popd 15 | echo -e "build demo0$count succeed" 16 | echo "=============" 17 | count=$((count + 1)) 18 | done 19 | 20 | ./node_modules/.bin/gh-pages -d dist 21 | echo -e "commit to gh-pages branch succeed" 22 | rm -rf dist 23 | echo -e "delete dist directory succeed" 24 | -------------------------------------------------------------------------------- /demo/components/App.less: -------------------------------------------------------------------------------- 1 | @import "common"; 2 | @grey: #CCCCCC; 3 | :global(.title){ 4 | color: red; 5 | } 6 | .title { 7 | color: black; 8 | color: red; 9 | border: #fff solid 1px; 10 | margin-top: 10px; 11 | margin-right: 20px; 12 | margin-bottom: 10px; 13 | 14 | } 15 | .title{ 16 | margin-left: 20px; 17 | } 18 | .title:hover{ 19 | color: @grey; 20 | } 21 | :global .main{ 22 | color: transparent; 23 | } 24 | .test, .a{ 25 | background-color: #DDDDDD; 26 | 27 | } 28 | @keyframes fadeOut { 29 | from { 30 | opacity: 1; 31 | } 32 | to { 33 | opacity: 0; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/moduleTest.js: -------------------------------------------------------------------------------- 1 | /*globals describe */ 2 | 3 | var test = require("./helpers").testSingleItem; 4 | 5 | var path = require("path"); 6 | var fs = require("fs"); 7 | var testCasesPath = path.join(__dirname, "moduleTestCases"); 8 | var testCases = fs.readdirSync(testCasesPath); 9 | 10 | describe("module", function() { 11 | testCases.forEach(function(name) { 12 | var source = fs.readFileSync(path.join(testCasesPath, name, "source.css"), "utf-8"); 13 | var expected = fs.readFileSync(path.join(testCasesPath, name, "expected.css"), "utf-8"); 14 | 15 | test(name, source, expected, "?module&sourceMap&localIdentName=_[local]_"); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/moduleMinimizeTest.js: -------------------------------------------------------------------------------- 1 | /*globals describe */ 2 | 3 | var test = require("./helpers").testSingleItem; 4 | 5 | var path = require("path"); 6 | var fs = require("fs"); 7 | var testCasesPath = path.join(__dirname, "moduleMinimizeTestCases"); 8 | var testCases = fs.readdirSync(testCasesPath); 9 | 10 | describe("module minimize", function() { 11 | testCases.forEach(function(name) { 12 | var source = fs.readFileSync(path.join(testCasesPath, name, "source.css"), "utf-8"); 13 | var expected = fs.readFileSync(path.join(testCasesPath, name, "expected.css"), "utf-8"); 14 | 15 | test(name, source, expected, '?' + JSON.stringify({ 16 | module: true, 17 | sourceMap: true, 18 | minimize: { 19 | discardComments: false 20 | }, 21 | localIdentName: '_[local]_' 22 | })); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | branches: 4 | only: 5 | - master 6 | matrix: 7 | fast_finish: true 8 | include: 9 | - os: linux 10 | node_js: "7" 11 | env: WEBPACK_VERSION="2.2.0" JOB_PART=lint 12 | - os: linux 13 | node_js: "6" 14 | env: WEBPACK_VERSION="2.2.0" JOB_PART=test 15 | - os: linux 16 | node_js: "4.3" 17 | env: WEBPACK_VERSION="2.2.0" JOB_PART=test 18 | - os: linux 19 | node_js: "7" 20 | env: WEBPACK_VERSION="2.2.0" JOB_PART=test 21 | - os: linux 22 | node_js: "4.3" 23 | env: WEBPACK_VERSION="1.14.0" JOB_PART=test 24 | before_install: 25 | - nvm --version 26 | - node --version 27 | before_script: 28 | - 'if [ "$WEBPACK_VERSION" ]; then yarn add webpack@^$WEBPACK_VERSION; fi' 29 | script: 30 | - yarn run travis:$JOB_PART 31 | after_success: 32 | - bash <(curl -s https://codecov.io/bash) 33 | -------------------------------------------------------------------------------- /src/declValueMap.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable quotes */ 2 | const declValueMap = { 3 | "0": "0", 4 | "absolute": "a", 5 | "auto": "a1", 6 | "block": "b", 7 | "bold": "b1", 8 | "bolder": "b2", 9 | "both": "b3", 10 | "break-all": "b4", 11 | "center": "c", 12 | "collapse": "c1", 13 | "default": "d", 14 | "ellipsis": "e", 15 | "fixed": "f", 16 | "gray": "g", 17 | "hidden": "h", 18 | "infinite": "i", 19 | "inherit": "i1", 20 | "inline": "i2", 21 | "left": "l", 22 | "middle": "m", 23 | "no-repeat": "n", 24 | "none": "n1", 25 | "normal": "n2", 26 | "nowrap": "n3", 27 | "padding": "p", 28 | "pointer": "p1", 29 | "red": "r", 30 | "relative": "r1", 31 | "right": "r2", 32 | "solid": "s", 33 | "table": "t", 34 | "tahoma": "t1", 35 | "tomato": "t2", 36 | "top": "t3", 37 | "underline": "u", 38 | "visible": "v", 39 | "white": "w", 40 | } 41 | module.exports = declValueMap 42 | -------------------------------------------------------------------------------- /src/error.js: -------------------------------------------------------------------------------- 1 | const formatCodeFrame = require('babel-code-frame') 2 | 3 | function formatMessage(message, loc, source) { 4 | let formatted = message 5 | if (loc) { 6 | formatted = formatted + ' (' + loc.line + ':' + loc.column + ')' 7 | } 8 | if (loc && source) { 9 | formatted = formatted + '\n\n' + formatCodeFrame(source, loc.line, loc.column) + '\n' 10 | } 11 | return formatted 12 | } 13 | 14 | function CSSFlatError(error) { 15 | Error.call(this) 16 | Error.captureStackTrace(this, CSSFlatError) 17 | this.name = 'Syntax Error' 18 | this.error = error.input.source 19 | const loc = error.line !== null && error.column !== null ? { line: error.line, column: error.column } : null 20 | this.message = formatMessage(error.reason, loc, error.input.source) 21 | this.hideStack = true 22 | } 23 | 24 | CSSFlatError.prototype = Object.create(Error.prototype) 25 | CSSFlatError.prototype.constructor = CSSFlatError 26 | 27 | module.exports = CSSFlatError 28 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-flat-demos", 3 | "version": "1.0.0", 4 | "description": "A collection of simple demos of CSS Flat", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --config webpack.config.js" 8 | }, 9 | "keywords": [ 10 | "CSS", 11 | "CSS Flat" 12 | ], 13 | "dependencies": { 14 | "babel-core": "^6.9.1", 15 | "babel-loader": "^6.2.4", 16 | "babel-preset-es2015": "^6.9.0", 17 | "babel-preset-react": "^6.5.0", 18 | "babel-preset-stage-0": "^6.5.0", 19 | "css-loader": "^0.23.1", 20 | "cssnano": "^3.10.0", 21 | "less": "^2.7.2", 22 | "less-loader": "^4.0.3", 23 | "optimize-css-assets-webpack-plugin": "^1.3.1", 24 | "postcss-loader": "^0.9.1", 25 | "postcss-modules-values": "^1.1.3", 26 | "react": "^15.1.0", 27 | "react-dom": "^15.1.0", 28 | "style-loader": "^0.13.1", 29 | "webpack": "^1.13.1", 30 | "webpack-dev-server": "^1.14.1" 31 | }, 32 | "devDependencies": { 33 | "css-flat-loader": "latest", 34 | "extract-text-webpack-plugin": "^1.0.1", 35 | "gh-pages": "^0.11.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/pseudoMap.js: -------------------------------------------------------------------------------- 1 | function getPseudoShort(preShort, pseudo) { 2 | return preShort + '_' + pseudo.split('(')[1].split(')')[0] 3 | } 4 | const pseudoMap = { 5 | 'link': 'l', 6 | 'visited': 'v', 7 | 'active': 'a', 8 | 'hover': 'h', 9 | 'focus': 'f', 10 | 'first-letter': 'fle', 11 | 'first-line': 'fli', 12 | 'first-child': 'fc', 13 | 'before': 'be', 14 | 'after': 'af', 15 | 'first-of-type': 'fot', 16 | 'last-of-type': 'lot', 17 | 'only-of-type': 'oot', 18 | 'only-child': 'oc', 19 | 'nth-child': (pseudo) => { 20 | return getPseudoShort('nc', pseudo) 21 | }, 22 | 'nth-last-child': (pseudo) => { 23 | return getPseudoShort('nlc', pseudo) 24 | }, 25 | 'nth-of-type': (pseudo) => { 26 | return getPseudoShort('not', pseudo) 27 | }, 28 | 'nth-last-of-type': (pseudo) => { 29 | return getPseudoShort('nlot', pseudo) 30 | }, 31 | 'last-child': 'lc', 32 | 'root': 'r', 33 | 'empty': 'e', 34 | 'target': 't', 35 | 'enabled': 'en', 36 | 'disabled': 'd', 37 | 'checked': 'c', 38 | } 39 | module.exports = pseudoMap 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright JS Foundation and other contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /demo/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 3 | var OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 4 | 5 | module.exports = { 6 | entry: __dirname + '/index.js', 7 | output: { 8 | publicPath: '/', 9 | filename: './bundle.js' 10 | }, 11 | module: { 12 | loaders: [ 13 | { 14 | test: /\.jsx?$/, 15 | exclude: /node_modules/, 16 | loader: 'babel', 17 | query: { 18 | presets: ['es2015', 'stage-0', 'react'] 19 | } 20 | }, 21 | { 22 | test: /\.less$/, 23 | loader: "style-loader!css-flat-loader!css-loader?modules&localIdentName=[path][name]---[local]---[hash:base64:5]&sourceMap!less-loader?sourceMap", 24 | //loader: ExtractTextPlugin.extract("css-flat!css?modules&localIdentName=_[local]_&sourceMap!less?sourceMap"), 25 | }, 26 | ] 27 | }, 28 | plugins: [ 29 | new ExtractTextPlugin("[name].css", { 30 | allChunks: true, 31 | }), 32 | new OptimizeCssAssetsPlugin(), 33 | ] 34 | }; 35 | -------------------------------------------------------------------------------- /src/getLoaderConfig.js: -------------------------------------------------------------------------------- 1 | const resolve = require('path').resolve 2 | 3 | const config = require('cosmiconfig') 4 | const assign = require('lodash').assign 5 | 6 | const loadPlugins = require('postcss-load-plugins/lib/plugins.js') 7 | 8 | module.exports = function cssFlatConfig(ctx, path, options) { 9 | ctx = assign({ cwd: process.cwd(), env: process.env.NODE_ENV }, ctx) 10 | path = path ? resolve(path) : process.cwd() 11 | options = assign({ rcExtensions: true }, options) 12 | if (!ctx.env) process.env.NODE_ENV = 'development' 13 | let file 14 | return config('css-flat', options) 15 | .load(path) 16 | .then(function (result) { 17 | if (!result) throw Error('No css-flat Config found in: ' + path) 18 | 19 | file = result ? result.filepath : '' 20 | 21 | return result ? result.config : {} 22 | }) 23 | .then((params) => { 24 | if (typeof params === 'function') params = params(ctx) 25 | else params = assign(params, ctx) 26 | 27 | if (!params.plugins) params.plugins = [] 28 | 29 | return assign({}, params, { 30 | plugins: loadPlugins(params), 31 | file, 32 | }) 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /src/getSelectorType.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const pseudoRegex = /:[^\s]*/ 3 | const attributeRegex = /\[[^\s]*/ 4 | module.exports = function getSelectorType(selector, localsMap) { 5 | let isGlobal = true 6 | let isClassSelector = true 7 | let type = 'normal' 8 | let selectorHalf = '' 9 | const tempSel = _.trim(selector).replace(/ |>|\+/g, ' ').replace(/\.|#/g, '') 10 | const names = tempSel.split(' ') 11 | names.forEach((name) => { 12 | if (localsMap[name.replace(/\[|:/, ' ').split(' ')[0]]) { 13 | isGlobal = false 14 | } 15 | }) 16 | if (!isGlobal && (names.length !== 1 || _.trim(selector)[0] !== '.')) { 17 | isClassSelector = false 18 | throw new Error('css flat仅允许单层类选择器:' + selector) 19 | } else { 20 | const s = _.trim(selector) 21 | const pseudoM = s.match(pseudoRegex) 22 | const attributeM = s.match(attributeRegex) 23 | if (pseudoM) { 24 | type = 'pseudo' 25 | selectorHalf = pseudoM[0] 26 | } else if (attributeM) { 27 | type = 'attribute' 28 | selectorHalf = attributeM[0] 29 | } 30 | } 31 | return { 32 | isGlobal, 33 | isClassSelector: !isGlobal && isClassSelector, 34 | type, 35 | selectorHalf, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/getSelectorName.js: -------------------------------------------------------------------------------- 1 | const pseudoMapDefault = require('./pseudoMap') 2 | const declPropMapDefault = require('./declPropMap') 3 | const declValueMapDefault = require('./declValueMap') 4 | let parentNum = 1 5 | let declPropId = 1 6 | let declValueId = 1 7 | let pseudoId = 1 8 | const parentParamsSuffixs = {} 9 | module.exports = function getSelectorName(decl, opt = {}) { 10 | const { 11 | prefix = '', 12 | parentParams = '', 13 | parentName, 14 | atRulesConfig, 15 | selectorHalf, 16 | pseudoMap = pseudoMapDefault, 17 | declPropMap = declPropMapDefault, 18 | declValueMap = declValueMapDefault, 19 | } = opt 20 | const { value: declValue, prop: declProp } = decl 21 | const name = [] 22 | const name1 = [] 23 | let declPropName = '' 24 | let declValueName = '' 25 | let pseudoName = '' 26 | declPropName = declPropMap[declProp] || declPropId++ 27 | declPropMap[declProp] = declPropName 28 | name1.push(declPropName) 29 | if (selectorHalf !== '') { 30 | pseudoName = pseudoMap[selectorHalf.slice(1).split('(')[0]] 31 | if (typeof pseudoName === 'function') { 32 | pseudoName = pseudoName(selectorHalf.slice(1)) 33 | } else if (pseudoName === undefined) { 34 | pseudoName = pseudoMap[selectorHalf] || pseudoId++ 35 | pseudoMap[selectorHalf] = pseudoName 36 | } 37 | } 38 | 39 | if (parentParams !== 'normal') { 40 | const atRulesConfigKey = ('@' + parentName + parentParams).replace(/ /g, '') 41 | const atRuleSuffix = (atRulesConfig[atRulesConfigKey] || {}).suffix 42 | parentParamsSuffixs[parentParams] = parentParamsSuffixs[parentParams] || atRuleSuffix || parentNum++ 43 | } 44 | 45 | if (parentParamsSuffixs[parentParams]) { 46 | name1.push(pseudoName, parentParamsSuffixs[parentParams]) 47 | } else if (pseudoName) { 48 | name1.push(pseudoName) 49 | } 50 | 51 | declValueName = declValueMap[declValue] || declValueId++ 52 | declValueMap[declValue] = declValueName 53 | 54 | name.push(prefix, name1.join('_'), declValueName) 55 | return name.join('-') 56 | } 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-flat-loader", 3 | "version": "0.2.1", 4 | "author": "tangjinzhou", 5 | "description": "css flat loader module for webpack", 6 | "engines": { 7 | "node": ">=0.12.0 || >=4.3.0 <5.0.0 || >=5.10" 8 | }, 9 | "files": [ 10 | "index.js", 11 | "src" 12 | ], 13 | "dependencies": { 14 | "autoprefixer": "^7.1.0", 15 | "babel-code-frame": "^6.11.0", 16 | "cosmiconfig": "^2.1.3", 17 | "css-loader": "^0.28.0", 18 | "css-selector-tokenizer": "^0.7.0", 19 | "cssnano": "^3.10.0", 20 | "eslint-config-postcss": "^2.0.2", 21 | "loader-utils": "^1.0.2", 22 | "lodash": "^4.17.4", 23 | "object-assign": "^4.1.1", 24 | "postcss": "^5.0.6", 25 | "postcss-load-plugins": "^2.3.0", 26 | "postcss-modules-extract-imports": "^1.0.0", 27 | "postcss-modules-local-by-default": "^1.0.1", 28 | "postcss-modules-scope": "^1.0.0", 29 | "postcss-modules-values": "^1.1.0", 30 | "postcss-value-parser": "^3.3.0", 31 | "precss": "^1.4.0", 32 | "source-list-map": "^0.1.7" 33 | }, 34 | "devDependencies": { 35 | "codecov": "^1.0.1", 36 | "eslint": "3.14.0", 37 | "istanbul": "^0.4.5", 38 | "mocha": "^3.2.0", 39 | "should": "^11.1.2", 40 | "standard-version": "^4.0.0" 41 | }, 42 | "scripts": { 43 | "test": "mocha", 44 | "test:cover": "npm run cover -- --report lcovonly", 45 | "lint": "eslint lib test", 46 | "travis:test": "npm run cover", 47 | "travis:lint": "npm run lint", 48 | "cover": "istanbul cover node_modules/mocha/bin/_mocha", 49 | "release": "yarn run standard-version" 50 | }, 51 | "repository": { 52 | "type": "git", 53 | "url": "git@github.com:tangjinzhou/css-flat-loader" 54 | }, 55 | "eslintConfig": { 56 | "extends": "eslint-config-postcss/es5", 57 | "env": { 58 | "jest": true 59 | }, 60 | "rules": { 61 | "comma-dangle": [ 62 | 2, 63 | "always-multiline" 64 | ], 65 | "semi": [ 66 | 2, 67 | "never" 68 | ], 69 | "no-trailing-spaces": "off", 70 | "max-len": [ 71 | 2, 72 | 180 73 | ] 74 | } 75 | }, 76 | "license": "MIT" 77 | } 78 | -------------------------------------------------------------------------------- /src/declPropMap.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable quotes */ 2 | const declProsMap = { 3 | "animation": "a", 4 | "animation-iteration-count": "a1", 5 | "background": "b", 6 | "background-clip": "b1", 7 | "background-color": "b2", 8 | "background-image": "b3", 9 | "background-position": "b4", 10 | "background-repeat": "b5", 11 | "background-size": "b6", 12 | "border": "b7", 13 | "border-bottom": "b8", 14 | "border-bottom-color": "b9", 15 | "border-collapse": "b10", 16 | "border-color": "b11", 17 | "border-left": "b12", 18 | "border-left-color": "b13", 19 | "border-radius": "b14", 20 | "border-right": "b15", 21 | "border-right-color": "b16", 22 | "border-spacing": "b17", 23 | "border-style": "b18", 24 | "border-top": "b19", 25 | "border-top-color": "b20", 26 | "border-top-left-radius": "b21", 27 | "border-top-right-radius": "b22", 28 | "border-width": "b23", 29 | "bottom": "b24", 30 | "box-shadow": "b25", 31 | "clear": "c", 32 | "color": "c1", 33 | "content": "c2", 34 | "cursor": "c3", 35 | "display": "d", 36 | "filter": "f", 37 | "float": "f1", 38 | "font": "f2", 39 | "font-family": "f3", 40 | "font-size": "f4", 41 | "font-style": "f5", 42 | "font-weight": "f6", 43 | "height": "h", 44 | "left": "l", 45 | "line-height": "l1", 46 | "list-style": "l2", 47 | "margin": "m", 48 | "margin-bottom": "m1", 49 | "margin-left": "m2", 50 | "margin-right": "m3", 51 | "margin-top": "m4", 52 | "max-height": "m5", 53 | "max-width": "m6", 54 | "min-height": "m7", 55 | "opacity": "o", 56 | "outline": "o1", 57 | "overflow": "o2", 58 | "overflow-x": "o3", 59 | "overflow-y": "o4", 60 | "padding": "p", 61 | "padding-bottom": "p1", 62 | "padding-left": "p2", 63 | "padding-right": "p3", 64 | "padding-top": "p4", 65 | "position": "p5", 66 | "right": "r", 67 | "text-align": "t", 68 | "text-decoration": "t1", 69 | "text-indent": "t2", 70 | "text-overflow": "t3", 71 | "text-shadow": "t4", 72 | "top": "t5", 73 | "transform": "t6", 74 | "transition": "t7", 75 | "vertical-align": "v", 76 | "visibility": "v1", 77 | "white-space": "w", 78 | "width": "w1", 79 | "word-break": "w2", 80 | "z-index": "z", 81 | "zoom": "z1", 82 | } 83 | module.exports = declProsMap 84 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | require("should"); 2 | var cssLoader = require("css-loader/index.js"); 3 | var vm = require("vm"); 4 | var flatLoader = require('../src/loader'); 5 | var clone = require('lodash/clone') 6 | 7 | function getEvaluated(output, modules) { 8 | try { 9 | var fn = vm.runInThisContext("(function(module, exports, require) {" + output + "})", "testcase.js"); 10 | var m = {exports: {}, id: 1}; 11 | fn(m, m.exports, function (module) { 12 | if (module.indexOf("css-base") >= 0) 13 | return require("css-loader/lib/css-base"); 14 | if (module.indexOf("-!/path/css-loader!") === 0) 15 | module = module.substr(19); 16 | if (modules && modules[module]) 17 | return modules[module]; 18 | return "{" + module + "}"; 19 | }); 20 | } catch (e) { 21 | console.error(output); // eslint-disable-line no-console 22 | throw e; 23 | } 24 | delete m.exports.toString; 25 | delete m.exports.i; 26 | return m.exports; 27 | } 28 | 29 | function runLoader(loader, input, map, addOptions, callback) { 30 | var tempCallback = function (err, output) { 31 | callback(err, output) 32 | flatLoader.call({ 33 | options: { 34 | context: "" 35 | }, 36 | callback: function () { 37 | }, 38 | async: function (res) { 39 | return () => { 40 | } 41 | }, 42 | loaders: [{request: "/path/css-loader"}], 43 | loaderIndex: 0, 44 | context: "", 45 | resource: "test.css", 46 | resourcePath: "test.css", 47 | request: "css-loader!test.css", 48 | emitError: function (message) { 49 | throw new Error(message); 50 | } 51 | }, clone(output), map); 52 | } 53 | var opt = { 54 | options: { 55 | context: "" 56 | }, 57 | callback: tempCallback, 58 | async: function () { 59 | return tempCallback; 60 | }, 61 | loaders: [{request: "/path/css-loader"}], 62 | loaderIndex: 0, 63 | context: "", 64 | resource: "test.css", 65 | resourcePath: "test.css", 66 | request: "css-loader!test.css", 67 | emitError: function (message) { 68 | throw new Error(message); 69 | } 70 | }; 71 | Object.keys(addOptions).forEach(function (key) { 72 | opt[key] = addOptions[key]; 73 | }); 74 | loader.call(opt, input, map); 75 | } 76 | 77 | exports.testSingleItem = function testSingleItem(name, input, result, query, modules) { 78 | it(name, function (done) { 79 | runLoader(cssLoader, input, undefined, { 80 | query: query 81 | }, function (err, output) { 82 | 83 | if (err) return done(err); 84 | var exports = getEvaluated(output, modules); 85 | /*Array.isArray(exports).should.be.eql(true); 86 | (exports.length).should.be.eql(1); 87 | (exports[0].length >= 3).should.be.eql(true); 88 | (exports[0][0]).should.be.eql(1); 89 | (exports[0][2]).should.be.eql(""); 90 | (exports[0][1]).should.be.eql(result);*/ 91 | done(); 92 | }); 93 | }); 94 | }; 95 | 96 | -------------------------------------------------------------------------------- /src/loader.js: -------------------------------------------------------------------------------- 1 | const loaderUtils = require('loader-utils') 2 | const processCss = require('./processCss') 3 | const CSSFlatError = require('./error') 4 | const vm = require('vm') 5 | const path = require('path') 6 | const loadConfig = require('./getLoaderConfig') 7 | const urlItemRegExpG = /___CSS_FLAT_LOADER_URL___([0-9]+)___/g 8 | const urlItemRegExp = /___CSS_FLAT_LOADER_URL___([0-9]+)___/ 9 | const urlItems = [] 10 | function getEvaluated(output, modules) { 11 | const m = { exports: {} } 12 | try { 13 | const fn = vm.runInThisContext('(function(module, exports, require) {' + output + '})', 'css-loader-output.js') 14 | fn(m, m.exports, function (module) { 15 | if (module.indexOf('css-base') >= 0) 16 | return require('css-loader/lib/css-base') 17 | if (module.indexOf('-!/path/css-loader!') === 0) 18 | module = module.substr(19) 19 | if (modules && modules[module]) 20 | return modules[module] 21 | const loaderUrl = '___CSS_FLAT_LOADER_URL___' + urlItems.length + '___' 22 | urlItems.push({ url: module }) 23 | return loaderUrl 24 | }) 25 | } catch (e) { 26 | throw e 27 | } 28 | delete m.exports.toString 29 | delete m.exports.i 30 | return m.exports 31 | } 32 | 33 | module.exports = function (input) { 34 | if (this.cacheable) this.cacheable() 35 | const callback = this.async() 36 | const loader = this 37 | const file = this.resourcePath 38 | const params = loaderUtils.getOptions(this) || {} 39 | params.plugins = params.plugins || this.options['css-flat'] 40 | 41 | let configPath 42 | 43 | // params.plugins = [] 44 | // params.sourceMap = true 45 | 46 | if (params.config) { 47 | if (path.isAbsolute(params.config)) { 48 | configPath = params.config 49 | } else { 50 | configPath = path.join(process.cwd(), params.config) 51 | } 52 | } else { 53 | configPath = path.dirname(file) 54 | } 55 | const exports = getEvaluated(input) 56 | Promise.resolve().then(function () { 57 | if ( typeof params.plugins !== 'undefined' ) { 58 | return params 59 | } else { 60 | return loadConfig({ webpack: loader }, configPath, { argv: false }) 61 | } 62 | }).then(function (config) { 63 | let inputMap = null 64 | if (config.sourceMap && exports[0][3]) { 65 | inputMap = JSON.stringify(exports[0][3]) 66 | } 67 | 68 | processCss(exports[0][1], inputMap, { 69 | from: loaderUtils.getRemainingRequest(loader), 70 | to: loaderUtils.getCurrentRequest(loader), 71 | params: config, 72 | loaderContext: loader, 73 | locals: exports.locals, 74 | }, (err, result) => { 75 | if (err) return callback(err) 76 | 77 | let cssAsString = JSON.stringify(result.source) 78 | cssAsString = cssAsString.replace(urlItemRegExpG, (item) => { 79 | const match = urlItemRegExp.exec(item) 80 | const idx = +match[1] 81 | const urlItem = urlItems[idx] 82 | const urlRequest = urlItem.url 83 | return '\" + require(' + loaderUtils.stringifyRequest(this, urlRequest) + ') + \"' 84 | }) 85 | let exportJs = JSON.stringify(result.exports) 86 | if (exportJs) { 87 | exportJs = 'exports.locals = ' + exportJs + ';' 88 | } 89 | 90 | let moduleJs 91 | if (config.sourceMap && result.map) { 92 | let map = result.map 93 | map = JSON.stringify(map) 94 | moduleJs = 'exports.push([module.id, ' + cssAsString + ', "", ' + map + ']);' 95 | } else { 96 | moduleJs = 'exports.push([module.id, ' + cssAsString + ', ""]);' 97 | } 98 | 99 | return callback(null, 'exports = module.exports = require(' + 100 | loaderUtils.stringifyRequest(loader, require.resolve('css-loader/lib/css-base')) + 101 | ')(' + params.sourceMap + ');\n' + 102 | '// imports\n' + 103 | '' + '\n\n' + 104 | '// module\n' + 105 | moduleJs + '\n\n' + 106 | '// exports\n' + 107 | exportJs) 108 | }) 109 | }).catch((err) => { 110 | console.log(err) 111 | if (err.name === 'CssSyntaxError') { 112 | callback(new CSSFlatError(err)) 113 | } else { 114 | callback(err) 115 | } 116 | }) 117 | 118 | } 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # css-flat-loader 2 | 3 | ## CSS Flat 4 | 5 | CSS Flat(CSS 扁平化)是一种模块化解决方案,基于Post CSS生态开发。 6 | 7 | 主要解决问题: 8 | 1. 达到模块化CSS的能力 9 | 2. 解决由于业务的持续迭代,导致的CSS样式文件的线性增长问题(CSS Modules尤其明显) 10 | 11 | CSS Flat 将CSS样式格式化为单条样式,开发时只需要按照正常文件书写: 12 | 13 | ```css 14 | .className { 15 | display: block; 16 | color: red; 17 | margin: 0 auto; 18 | } 19 | .className:hover { 20 | color: green; 21 | magin-top: 10px; 22 | } 23 | ``` 24 | Flat化之后: 25 | ```css 26 | .a-d-b { 27 | display: block; 28 | } 29 | .a-c-1 { 30 | color: red; 31 | } 32 | .a-m-2 { 33 | margin: 0 auto; 34 | } 35 | .a-c_h-3:hover { 36 | color: green; 37 | } 38 | .css-flat .a-mt_h-4:hover { 39 | margin-top: 10px; 40 | } 41 | ``` 42 | 当你在js文件中 import CSS Flat文件时,会export一个对象,该对象包含Flat化之后 43 | 的信息(className: newClassNames): 44 | 45 | ```js 46 | import styles from "./style.css"; // { classNames: 'a-d-b a-c-1 a-m-2 a-c_h-3 a-mt_h-4 '} 47 | // import { className } from "./style.css"; 48 | 49 | element.innerHTML = '
'; 50 | ``` 51 | 52 | ### 原则 53 | 54 | 1. 仅处理单层类选择器 55 | 56 | ```css 57 | /* 不支持 */ 58 | .main.home-main {} 59 | .main .title {} 60 | .main span {} 61 | #main {} 62 | ``` 63 | 2. 简写权重小于非简写权重 64 | 3. 媒体查询权重大于普通样式,不同条件的媒体查询权重需自行配置 65 | 66 | ### 使用方法 67 | ```bash 68 | npm install --save-dev css-flat-loader 69 | ``` 70 | 目前依赖在CSS Modules的基础上来判断是否需要Flat话,后续会独立出来,详见demo 71 | ```js 72 | { 73 | test: /\.less$/, 74 | // loader: "style-loader!css-flat-loader!css-loader?modules!less-loader", 75 | loader: ExtractTextPlugin.extract("css-flat-loader!css?modules&localIdentName=_[local]_!less") 76 | }, 77 | ``` 78 | 79 | ### stylelint设置 80 | 建议添加如下设置来对样式文件进行检测: 81 | 82 | declaration-block-no-shorthand-property-overrides: true 83 | [查看](https://stylelint.io/user-guide/rules/declaration-block-no-shorthand-property-overrides/) 84 | 85 | max-nesting-depth: [0, ignore: ["blockless-at-rules"]] 86 | [查看](https://stylelint.io/user-guide/rules/max-nesting-depth/) 87 | 88 | selector-max-class: 1 89 | [查看](https://stylelint.io/user-guide/rules/selector-max-class/) 90 | 91 | selector-max-id: 0 92 | [查看](https://stylelint.io/user-guide/rules/selector-max-id/) 93 | 94 | ### API 95 | 96 | ```js 97 | // 配置文件css-flat.config.js 98 | module.exports = { 99 | plugins: [ 100 | require('precss')(), 101 | require('autoprefixer')(), 102 | ] 103 | } 104 | ``` 105 | 注:对于px2rem, autoprefixer等推荐在css-flat.config.js的plugins中配置 106 | 107 | flat后的样式公式如下: 108 | 109 | ```text 110 | .htmlClass*n .prefix-declProp(_(pseudo)(_atRule))-declValue {} 111 | ``` 112 | 1. htmlClass 根节点类名,用来增加权重,如margin-top的权重大于margin,n为'-'的个数 113 | 2. 当atRule存在,但无伪类时,pseudo为空字符,但下划线(_)保留,避免冲突 114 | 3. 当提供的map映射无相关属性时,脚本会自动从1自增分配,所以如需自定义提供map,不要提供数字,以免冲突 115 | 116 | | 属性 | 类型 | 默认值 | 描述 | 117 | | ---------- | --- | --- | --- | 118 | |**`htmlClass`**|`{string}`|`'css-flat'`|根节点类名,请自行在html标签上添加| 119 | |**`prefix`**|`{string}`|`'a'`|类名前缀| 120 | |**`declPropMap`**|`{Object}`|见[属性映射](https://github.com/tangjinzhou/css-flat-loader/blob/master/src/declPropMap.js)|属性映射| 121 | |**`pseudoMap`**|`{Object}`|见[伪类映射](https://github.com/tangjinzhou/css-flat-loader/blob/master/src/pseudoMap.js)|伪类映射| 122 | |**`atRules`**|`{Array}`|`[]`|@规则的映射,如@media等,数组顺序代表权重| 123 | |**`declValueMap`**|`{Object}`|见[值映射](https://github.com/tangjinzhou/css-flat-loader/blob/master/src/declValueMap.js)|值映射| 124 | |**`plugins`**|`{Array}`|`[]`|插件| 125 | |**`sourceMap`**|`{Bool}`|`false`|sourceMap为true时,会保留css modules hash之后的类,但属性会改变成`--sourceMap-xxx`, 间接达到sourceMap的功能| 126 | 127 | ### 更多 128 | 对于一些大型webview APP可按照规则容器内置通用common.css, 上线时做一次diff,仅需线上加载common.css不包含的CSS, 129 | 进一步降低样式文件,提升加载速度。 130 | 131 | ## 疑惑 132 | 1. css文件减少,那html或js文件增大了 133 | 答:没错,这是肯定的,不过不知你是否注意到,当你打开浏览器控制台查看元素,发现在元素上生效的样式并没有那么多条,更多的是层层覆盖。 134 | 针对该问题做了如下优化: 135 | 1. 扁平化之前会使用cssnano进行合并 136 | 2. 扁平化之后处理prefixer 137 | 3. 规则尽可能简短 138 | 4. 经过扁平化处理后注入到js中的也仅仅是一个对象,标签上也只是个对象值的引用 139 | 140 | 2. DOM节点的操作 141 | 答:经过处理后的类名是不具有可读性的,如果你使用的是react,那么也没必要操作dom,类名的可读性不再那么重要。如果你依然有操作dom的需求,你可以在模板中自行写类名。 142 | 143 | 3. sourceMap 144 | 答:这块的确不是特别好搞,目前的方案是开启sourceMap后,会保留css-modules hash后的类名,并将该类名下的样式自定义化使其不生效(如:--sourceMap-color: red),用于定位当前节点样式源文件的位置,基本可以满足需求。 145 | 146 | 4. 简写和非简写的处理 147 | 答:优先使用cssnona进行合并,合并不了的遵循以下规则:非简写权重大于简写 148 | 如margin-top 会被处理成 .css-flat .xxx {margin-top: ...} 149 | border-top-left-radius 会被处理成 .css-flat.css-flat.css-falt .xxx {margin-top: ...} 150 | 所以你需要在html根节点上手动添加css-flat类名(可自定义) 151 | 对于媒体查询规则类似,只是权重可自定义,更加灵活 152 | 153 | 5. 不支持嵌套,只支持类 154 | 答:对,该方案的核心思想就是扁平化处理css,对于嵌套我们不推荐,也不支持,如果你的项目已经组件化处理,单个组件的模板不会太大,也不应该太大,非嵌套的类方式不管从项目的后期可维护性上,还是浏览器的渲染上,都是利大于弊的。 155 | 156 | 6. 样式合并 如style.button + style.disabled 157 | 答:推荐使用less如下方式: 158 | ```css 159 | .button{ 160 | } 161 | .button-diabled{ 162 | .button() 163 | } 164 | ``` 165 | 166 | 因扁平化处理后的样式顺序改变不应该影响最终的渲染结果,所以单个html标签上的类名不应该出现相同属性的样式,直接使用style.button + style.disabled的方式是不安全的。 167 | 细心的朋友可能已经注意到我们的规则.prefix-declProp(_(pseudo)(_atRule))-declValue {} 既有-又有_ 这两种分隔符,这里就是预留给处理相同属性的,你可以自己处理declProp(_(pseudo)(_atRule))相同的部分。 168 | 169 | ### 存在的问题 170 | 171 | 因开发中要支持样式文件的按需加载及热更新,无法过滤浏览器中已加载的样式,会很多重复的样式,在控制台中看着很不爽。上线时还需自行添加plugin处理重复样式。 172 | 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /src/processCss.js: -------------------------------------------------------------------------------- 1 | const CSSFlatError = require('./error') 2 | const postcss = require('postcss') 3 | const _ = require('lodash') 4 | const cssnano = require('cssnano') 5 | const getSelectorName = require('./getSelectorName') 6 | const getSelectorType = require('./getSelectorType') 7 | 8 | const cacheLocalRuleInfo = {} 9 | const parserPlugin = postcss.plugin('postcss-flat', (options) => { 10 | const { 11 | locals = {}, 12 | prefix = 'a', 13 | atRulesConfig, 14 | htmlClass = 'css-flat', 15 | pseudoMap, 16 | declPropMap, 17 | declValueMap, 18 | sourceMap, 19 | inputMap, 20 | } = options 21 | const genMap = sourceMap && inputMap 22 | const localsMap = _.invert(locals) 23 | const localRuleMark = {} 24 | return (css) => { 25 | const exports = {} 26 | // const globalRule = [] 27 | css.walkRules((rule) => { 28 | let parentParams = 'normal' 29 | let parentName = '' 30 | if (rule.parent.type === 'atrule') { 31 | parentName = rule.parent.name 32 | if (parentName === 'supports' || parentName === 'media') { 33 | parentParams = rule.parent.params 34 | } else { 35 | return 36 | } 37 | } 38 | 39 | rule.selector.split(',').forEach((sel) => { 40 | const selectorType = getSelectorType(sel, localsMap) 41 | const { isGlobal, isClassSelector, selectorHalf = '' } = selectorType 42 | if (isGlobal) { 43 | const globalSel = _.trim(sel) 44 | const cloneRule = rule.clone() 45 | cloneRule.selector = globalSel 46 | // globalRule.push(cloneRule) 47 | } else if (isClassSelector) { 48 | const className = sel.replace(/\.| /g, '').replace(selectorHalf, '') 49 | rule.walkDecls(function (decl) { 50 | const prop = decl.prop.replace('--sourceMap-', '') 51 | const value = decl.value 52 | const newClassName = getSelectorName(decl, { 53 | parentName, 54 | parentParams, 55 | prefix, 56 | atRulesConfig, 57 | selectorHalf, 58 | pseudoMap, 59 | declPropMap, 60 | declValueMap, 61 | }) 62 | if (!cacheLocalRuleInfo[newClassName]) { 63 | let propLen = 0 64 | let priority = '' 65 | if (prop[0] !== '-') { 66 | propLen = prop.split('-').length 67 | } 68 | for (let i = 1; i < propLen; i++) { 69 | priority += '.' + htmlClass 70 | } 71 | cacheLocalRuleInfo[newClassName] = { 72 | prop, 73 | value, 74 | newClassName, 75 | selectorHalf, // 伪类后缀 76 | priority: priority + ' ', 77 | parentParams, 78 | } 79 | } 80 | localRuleMark[parentParams] = localRuleMark[parentParams] || {} 81 | localRuleMark[parentParams][newClassName] = cacheLocalRuleInfo[newClassName] 82 | 83 | const localsKey = localsMap[className] 84 | exports[localsKey] = (exports[localsKey] || (genMap ? className : '')) + ' ' + newClassName 85 | if (genMap) { 86 | decl.prop = '--sourceMap-' + prop 87 | decl.value = value 88 | } 89 | }) 90 | } 91 | if (!genMap && !isGlobal) { 92 | rule.remove() 93 | } 94 | }) 95 | 96 | }) 97 | css.walkAtRules(/media|supports/, rule => { 98 | const atRulesConfigKey = ('@' + rule.name + rule.params).replace(/ /g, '') 99 | for (let newClassName in localRuleMark[rule.params]) { 100 | const { selectorHalf = '', priority: tempP, prop, value } = cacheLocalRuleInfo[newClassName] 101 | const atRulePriority = (atRulesConfig[atRulesConfigKey] || {}).priority || '' 102 | const priority = _.trim(atRulePriority + tempP) + ' ' 103 | rule.append(priority + '.' + newClassName + selectorHalf + '{' + prop + ':' + value + '}') 104 | } 105 | }) 106 | 107 | for (let newClassName in localRuleMark.normal) { 108 | const { selectorHalf = '', priority, prop, value } = cacheLocalRuleInfo[newClassName] 109 | const newSelector = priority + '.' + newClassName + selectorHalf 110 | css.append(newSelector + '{' + prop + ':' + value + '}') 111 | } 112 | options.exports = exports 113 | } 114 | }) 115 | 116 | module.exports = function processCss(inputSource, inputMap, options, callback) { 117 | const { 118 | minimize, 119 | atRules = [], 120 | htmlClass = 'css-flat', 121 | plugins = [], 122 | sourceMap, 123 | } = options.params || {} 124 | 125 | const atRulesConfig = {} 126 | atRules.forEach((atRule, i) => { 127 | for (let key in atRule) { 128 | const value = atRule[key] 129 | atRulesConfig[key.replace(/ /g, '')] = { 130 | suffix: value, 131 | priority: Array(i + 1).fill('.' + htmlClass).join(''), 132 | } 133 | } 134 | 135 | }) 136 | 137 | const parserOptions = _.assign({}, options.params, { 138 | atRulesConfig, 139 | locals: options.locals || {}, 140 | inputMap, 141 | }) 142 | 143 | const pipeline = postcss([ 144 | cssnano({ 145 | zindex: false, 146 | normalizeUrl: false, 147 | discardUnused: false, 148 | mergeIdents: false, 149 | autoprefixer: false, 150 | reduceTransforms: false, 151 | }), 152 | parserPlugin(parserOptions), 153 | ].concat(plugins)) 154 | 155 | if (minimize) { 156 | const minimizeOptions = _.assign({}, minimize) 157 | ;['zindex', 'normalizeUrl', 'discardUnused', 'mergeIdents', 'reduceIdents', 'autoprefixer'].forEach((name) => { 158 | if (typeof minimizeOptions[name] === 'undefined') 159 | minimizeOptions[name] = false 160 | }) 161 | pipeline.use(cssnano(minimizeOptions)) 162 | } 163 | 164 | pipeline.process(inputSource, { 165 | from: '/css-flat-loader!' + options.from, 166 | to: options.to, 167 | map: sourceMap ? { 168 | prev: inputMap, 169 | sourcesContent: true, 170 | inline: false, 171 | annotation: false, 172 | } : null, 173 | }).then(function (result) { 174 | callback(null, { 175 | source: result.css, 176 | map: result.map && result.map.toJSON(), 177 | exports: parserOptions.exports, 178 | }) 179 | }).catch((err) => { 180 | console.log(err) 181 | if (err.name === 'CssSyntaxError') { 182 | callback(new CSSFlatError(err)) 183 | } else { 184 | callback(err) 185 | } 186 | }) 187 | } 188 | 189 | 190 | --------------------------------------------------------------------------------