├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── gulpfile.js ├── index.js ├── lib └── helpers.js ├── package.json └── test ├── helpers.js ├── integration.js ├── plugin.js └── test_helpers.js /.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 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "space-before-function-paren": [2, { "named": "never" }], 4 | "no-shadow-restricted-names": [2], 5 | "computed-property-spacing": [2], 6 | "no-empty-character-class": [2], 7 | "no-irregular-whitespace": [2], 8 | "no-unexpected-multiline": [2], 9 | "no-multiple-empty-lines": [2], 10 | "space-return-throw-case": [2], 11 | "no-constant-condition": [2], 12 | "no-extra-boolean-cast": [2], 13 | "no-inner-declarations": [2], 14 | "no-this-before-super": [2], 15 | "no-use-before-define": [2], 16 | "no-array-constructor": [2], 17 | "object-curly-spacing": [2, "always"], 18 | "no-floating-decimal": [2], 19 | "no-warning-comments": [2], 20 | "handle-callback-err": [2], 21 | "no-unneeded-ternary": [2], 22 | "operator-assignment": [2], 23 | "space-before-blocks": [2], 24 | "no-native-reassign": [2], 25 | "no-trailing-spaces": [2], 26 | "operator-linebreak": [2, "after"], 27 | "consistent-return": [2], 28 | "no-duplicate-case": [2], 29 | "no-invalid-regexp": [2], 30 | "no-negated-in-lhs": [2], 31 | "constructor-super": [2], 32 | "no-nested-ternary": [2], 33 | "no-extend-native": [2], 34 | "block-scoped-var": [2], 35 | "no-control-regex": [2], 36 | "no-sparse-arrays": [2], 37 | "no-throw-literal": [2], 38 | "no-return-assign": [2], 39 | "no-const-assign": [2], 40 | "no-class-assign": [2], 41 | "no-extra-parens": [2], 42 | "no-regex-spaces": [2], 43 | "no-implied-eval": [2], 44 | "no-useless-call": [2], 45 | "no-self-compare": [2], 46 | "no-octal-escape": [2], 47 | "no-new-wrappers": [2], 48 | "no-process-exit": [2], 49 | "no-catch-shadow": [2], 50 | "linebreak-style": [2], 51 | "space-infix-ops": [2], 52 | "space-unary-ops": [2], 53 | "no-cond-assign": [2], 54 | "no-func-assign": [2], 55 | "no-unreachable": [2], 56 | "accessor-pairs": [2], 57 | "no-empty-label": [2], 58 | "no-fallthrough": [2], 59 | "no-path-concat": [2], 60 | "no-new-require": [2], 61 | "no-spaced-func": [2], 62 | "no-unused-vars": [2], 63 | "spaced-comment": [2], 64 | "no-delete-var": [2], 65 | "comma-spacing": [2], 66 | "no-extra-semi": [2], 67 | "no-extra-bind": [2], 68 | "arrow-spacing": [2], 69 | "prefer-spread": [2], 70 | "no-new-object": [2], 71 | "no-multi-str": [2], 72 | "semi-spacing": [2], 73 | "no-lonely-if": [2], 74 | "dot-notation": [2], 75 | "dot-location": [2, "property"], 76 | "comma-dangle": [2, "never"], 77 | "no-dupe-args": [2], 78 | "no-dupe-keys": [2], 79 | "no-ex-assign": [2], 80 | "no-obj-calls": [2], 81 | "valid-typeof": [2], 82 | "default-case": [2], 83 | "no-redeclare": [2], 84 | "no-div-regex": [2], 85 | "no-sequences": [2], 86 | "no-label-var": [2], 87 | "comma-style": [2], 88 | "brace-style": [2], 89 | "no-debugger": [2], 90 | "quote-props": [2, "consistent-as-needed"], 91 | "no-iterator": [2], 92 | "no-new-func": [2], 93 | "key-spacing": [2, { "afterColon": true }], 94 | "complexity": [2], 95 | "new-parens": [2], 96 | "no-eq-null": [2], 97 | "no-bitwise": [2], 98 | "wrap-iife": [2], 99 | "no-caller": [2], 100 | "use-isnan": [2], 101 | "no-labels": [2], 102 | "no-shadow": [2], 103 | "camelcase": [2], 104 | "eol-last": [2], 105 | "no-octal": [2], 106 | "no-empty": [2], 107 | "no-alert": [2], 108 | "no-proto": [2], 109 | "no-undef": [2], 110 | "no-eval": [2], 111 | "no-with": [2], 112 | "no-void": [2], 113 | "max-len": [2, 100], 114 | "new-cap": [2], 115 | "eqeqeq": [2], 116 | "no-new": [2], 117 | "quotes": [2, "single"], 118 | "indent": [2, 4], 119 | "semi": [2, "always"], 120 | "yoda": [2, "never"] 121 | }, 122 | "env": { 123 | "mocha": true, 124 | "node": true 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | test/output 3 | npm-debug.log 4 | .idea 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - stable 5 | - "4" 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.1.2] - 2017-03-31 2 | * [#5](https://github.com/VitaliyR/postcss-esplit/issues/5) Fix splitting non-breakable at-rules 3 | 4 | ## [0.1.1] - 2017-03-31 5 | * Add *fileNameStartIndex* option 6 | 7 | ## [0.1.0] - 2016-05-18 8 | * Improved performance of processing children nodes 9 | 10 | ## [0.0.5] - 2016-05-16 11 | * Fixed insert of import nodes before @charset declaration 12 | 13 | ## [0.0.4] - 2016-05-16 14 | * Fixed bug with wrong import order 15 | * Improved selectors separation by splitting nested selectors 16 | * Refactor node moves methods 17 | * Added counter for selectors count for splitted files 18 | 19 | ## [0.0.3] - 2015-01-02 20 | * Removed IDE garbage from npm repository 21 | * Fixed bug with passing source map options to children roots 22 | 23 | ## [0.0.2] - 2015-11-20 24 | * [#3](https://github.com/VitaliyR/postcss-esplit/pull/3) Fixed scope bugs when processing a lot of files 25 | * Fixed reprocessing performance by handling which plugins already finished process and which aren't 26 | * Fixed handling plugin duplicates in processor plugins list 27 | 28 | ## [0.0.1] - 2015-11-12 29 | * Initial release 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2016 Vitaliy Ribachenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PostCSS eSplit [![Build Status][ci-img]][ci] 2 | 3 | [PostCSS] plugin for splitting css into multiple css files with sourcemaps support for preventing limitation of shinny ie9. 4 | 5 | [PostCSS]: https://github.com/postcss/postcss 6 | [ci-img]: https://travis-ci.org/VitaliyR/postcss-esplit.svg?branch=master 7 | [ci]: https://travis-ci.org/VitaliyR/postcss-esplit 8 | 9 | ```css 10 | /* Input example - style.css */ 11 | @charset "UTF-8"; 12 | 13 | .someClass { 14 | display: block; 15 | } 16 | 17 | @media (max-width: 768px) { 18 | p { 19 | color: red; 20 | } 21 | 22 | em { 23 | color: blue; 24 | } 25 | } 26 | ``` 27 | 28 | ```css 29 | /* Output example with maxSelectors = 2 */ 30 | 31 | /* style.css */ 32 | @charset "UTF-8"; 33 | @import url(style-0.css); 34 | 35 | @media (max-width: 768px) { 36 | em { 37 | color: blue; 38 | } 39 | } 40 | 41 | 42 | /* style-0.css */ 43 | .someClass { 44 | display: block; 45 | } 46 | 47 | @media (max-width: 768px) { 48 | p { 49 | color: red; 50 | } 51 | } 52 | 53 | ``` 54 | 55 | ## Install 56 | ``` 57 | npm install postcss-esplit --save 58 | ``` 59 | 60 | ## Usage 61 | 62 | ```js 63 | postcss([ require('postcss-esplit')(/*opts*/) ]) 64 | ``` 65 | 66 | See [PostCSS] docs for examples for your environment. 67 | 68 | Also, starting from version 0.0.2 there are no need to place the plugin in the end of the 69 | processor plugins list. 70 | 71 | 72 | ## Options 73 | * `maxSelectors` *{number=4000}* count of selectors exceeding which css file should be separated 74 | * `fileName` *{string=%original%-%i%}* template for retrieving name of separated files 75 | * `%original%` *{string}* name of original file 76 | * `%i%` *{number}* index of separated file 77 | * `fileNameStartIndex` *{number=0}* separated files will receive generated name starting from this index 78 | * `writeFiles` *{boolean=true}* separated files should be written to the disk 79 | * `writeSourceMaps` *{boolean=true}* source maps of separated files should be written to the disk 80 | * `writeImport` *{boolean=true}* original css source should have import declaration for separated files 81 | * `quiet` *{boolean-false}* toggling console output 82 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | 3 | var files = ['index.js', 'lib/*.js', 'test/*.js', 'gulpfile.js']; 4 | 5 | gulp.task('lint', function () { 6 | var eslint = require('gulp-eslint'); 7 | return gulp.src(files) 8 | .pipe(eslint()) 9 | .pipe(eslint.format()) 10 | .pipe(eslint.failAfterError()); 11 | }); 12 | 13 | gulp.task('test', function () { 14 | var mocha = require('gulp-mocha'); 15 | return gulp.src('test/*.js', { read: false }) 16 | .pipe(mocha()); 17 | }); 18 | 19 | gulp.task('default', ['lint', 'test']); 20 | 21 | gulp.task('watch', function () { 22 | gulp.watch(files, ['lint', 'test']); 23 | }); 24 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var chalk = require('chalk'); 4 | var mkdirp = require('mkdirp'); 5 | 6 | var postcss = require('postcss'); 7 | var MapGenerator = require('postcss/lib/map-generator'); 8 | var Result = require('postcss/lib/result'); 9 | 10 | var pkg = require('./package.json'); 11 | var helpers = require('./lib/helpers'); 12 | 13 | /** 14 | * Plugin default options 15 | * @const 16 | */ 17 | var defaults = { 18 | maxSelectors: 4000, 19 | fileName: '%original%-%i%', 20 | fileNameStartIndex: 0, 21 | writeFiles: true, 22 | writeSourceMaps: true, 23 | writeImport: true, 24 | quiet: false 25 | }; 26 | 27 | /** 28 | * At-Rules, contents should not be separated between roots 29 | * @type {Array.} 30 | */ 31 | var unbreakableAtRules = ['keyframes']; 32 | 33 | var roots; 34 | var treeSelectors; 35 | var selectors; 36 | var options; 37 | var messages; 38 | 39 | /** 40 | * Moves rules from start to, including, end nodes from root to new postcss.root 41 | * @param {Node} startNode 42 | * @param {Node} endNode 43 | * @param {Root} root 44 | * @returns {Root} 45 | */ 46 | var moveNodes = function (startNode, endNode, root) { 47 | var newRoot = postcss.root({ raws: root.raws }); 48 | var foundStart; 49 | var parentsRelations = [ 50 | [root, newRoot] 51 | ]; 52 | 53 | root.walk(function (node) { 54 | if (node.type !== 'decl' && node.type !== 'rule') return true; 55 | if (node.type === 'decl' && node.parent.type !== 'atrule') return true; 56 | if (node === startNode) foundStart = true; 57 | 58 | if (foundStart) { 59 | var newParent = helpers.find(node.parent, parentsRelations); 60 | if (!newParent) { 61 | var nodeParent = node.parent; 62 | var copyParent, prevCopyParent; 63 | 64 | // copy parents 65 | while (nodeParent.type !== 'root') { 66 | copyParent = nodeParent.clone({ 67 | nodes: [], 68 | raws: nodeParent.raws 69 | }); 70 | parentsRelations.push([nodeParent, copyParent]); 71 | 72 | prevCopyParent && copyParent.append(prevCopyParent); 73 | !newParent && (newParent = copyParent); 74 | 75 | prevCopyParent = copyParent; 76 | nodeParent = nodeParent.parent; 77 | } 78 | 79 | newRoot.append(copyParent); 80 | } 81 | 82 | // move node, remove its parents if it was the only one node here 83 | var nodeParents = helpers.parents(node); 84 | 85 | node.remove(); 86 | newParent.append(node); 87 | 88 | nodeParents.forEach(function (parent) { 89 | !parent.nodes.length && parent.remove(); 90 | }); 91 | } 92 | 93 | if (node === endNode) { 94 | return false; 95 | } 96 | }); 97 | 98 | return newRoot; 99 | }; 100 | 101 | /** 102 | * Checks if passed node is At-Rule and it exists in unbreakable at-rules list 103 | * @param {Node} node 104 | * @returns {boolean} 105 | */ 106 | var isUnbreakableAtRule = function (node) { 107 | return node.type === 'atrule' && unbreakableAtRules.indexOf(node.name) !== -1; 108 | }; 109 | 110 | /** 111 | * Splits rule selectors by provided position 112 | * 113 | * @param {Rule} node 114 | * @param {Number} position 115 | * @returns {undefined|Rule} 116 | */ 117 | var splitRule = function (node, position) { 118 | if (!position) return null; 119 | 120 | var newNode = node.clone({ 121 | selector: node.selectors.slice(position).join(',') 122 | }); 123 | node.selector = node.selectors.slice(0, position).join(','); 124 | 125 | node.parent.insertAfter(node, newNode); 126 | 127 | return node; 128 | }; 129 | 130 | /** 131 | * Walking by css nodes and if passed selectors count are below 132 | * the maxSelectors option move them to another style file. Recursive. 133 | * 134 | * @param {Root} css 135 | */ 136 | var processTree = function (css) { 137 | var startingNode, endNode, prevNode; 138 | 139 | css.walk(function (node) { 140 | if (isUnbreakableAtRule(node)) { 141 | prevNode = node.last; 142 | treeSelectors += 1; 143 | selectors += 1; 144 | return true; 145 | } 146 | if (isUnbreakableAtRule(node.parent)) { 147 | return true; 148 | } 149 | if (!node.selector) return true; 150 | 151 | !startingNode && (startingNode = node); 152 | 153 | if ((treeSelectors += node.selectors.length) > options.maxSelectors) { 154 | var selInSource = node.selectors.length - (treeSelectors - options.maxSelectors); 155 | endNode = splitRule(node, selInSource) || prevNode; 156 | 157 | selectors += selInSource ? endNode.selectors.length : 0; 158 | 159 | var newFile = moveNodes(startingNode, endNode, css); 160 | roots.push(newFile); 161 | 162 | treeSelectors = 0; 163 | processTree(css); 164 | 165 | return false; 166 | } else { 167 | selectors += node.selectors.length; 168 | } 169 | 170 | prevNode = node; 171 | }); 172 | }; 173 | 174 | /** 175 | * Log message to PostCSS 176 | * Arguments are used for filling template 177 | * 178 | * @param {string} msg 179 | */ 180 | var log = function (msg/* ...args */) { 181 | Array.prototype.slice.call(arguments, 1, arguments.length).forEach(function (data) { 182 | msg = msg.replace(/%s/, data); 183 | }, this); 184 | 185 | messages.push({ 186 | type: 'info', 187 | plugin: pkg.name, 188 | text: msg 189 | }); 190 | 191 | if (options.quiet) return; 192 | 193 | var args = [ 194 | chalk.green('>>') + ' ' + pkg.name + ': ' + msg 195 | ]; 196 | 197 | console.log.apply(this, args); 198 | }; 199 | 200 | /** 201 | * fs.writeFile which returns Promise 202 | * 203 | * @param {string} [dest] destination path 204 | * @param {string} [src] contents of the file 205 | * @param {boolean} [mkDir=false] recursive make dir to dest 206 | * @returns {Promise} 207 | */ 208 | var writePromise = function (dest, src, mkDir) { 209 | return new Promise(function (res, rej) { 210 | var writeFileHandler = function () { 211 | fs.writeFile(dest, src, function (err) { 212 | err ? rej(err) : res(); 213 | }); 214 | }; 215 | 216 | if (mkDir) { 217 | mkdirp(path.dirname(dest), function (errMkdir) { 218 | errMkdir ? rej(errMkdir) : writeFileHandler(); 219 | }); 220 | } else { 221 | writeFileHandler(); 222 | } 223 | }); 224 | }; 225 | 226 | var processRoot = function (processor, root, index, destination, result) { 227 | return new Promise(function (resolve, reject) { 228 | var filesForWrite = []; 229 | var fileNameStartIndex = Number(options.fileNameStartIndex); 230 | fileNameStartIndex = isNaN(fileNameStartIndex) ? 0 : fileNameStartIndex; 231 | var fileIndex = index + fileNameStartIndex; 232 | var fileName = helpers.getFileName(options.fileName, destination, fileIndex); 233 | 234 | var rootOpts = helpers.extend({}, result.opts, { to: fileName }); 235 | var rootResult = new Result(processor, root, rootOpts); 236 | var rootData = new MapGenerator(postcss.stringify, root, result.opts).generate(); 237 | rootResult.css = rootData[0]; 238 | rootResult.map = rootData[1]; 239 | 240 | if (!rootResult.opts.to) { 241 | if (options.writeFiles) { 242 | result.warn( 243 | 'Destination is not provided, ' + 244 | 'splitted css files would not be written' 245 | ); 246 | } 247 | 248 | return resolve(rootResult); 249 | } 250 | 251 | if (!options.writeFiles) return resolve(rootResult); 252 | 253 | filesForWrite.push( 254 | writePromise( 255 | rootResult.opts.to, rootResult.css, true 256 | ) 257 | ); 258 | 259 | if (options.writeSourceMaps && rootResult.map) { 260 | filesForWrite.push( 261 | writePromise( 262 | rootResult.opts.to + '.map', 263 | rootResult.map.toString(), 264 | true 265 | ) 266 | ); 267 | } 268 | 269 | Promise.all(filesForWrite).then(function () { 270 | resolve(rootResult); 271 | }).catch(reject); 272 | }); 273 | }; 274 | 275 | 276 | /** 277 | * Export plugin 278 | */ 279 | module.exports = postcss.plugin(pkg.name, function (opts) { 280 | opts = opts || {}; 281 | 282 | // merging default settings 283 | helpers.defaults(opts, defaults); 284 | 285 | options = opts; 286 | 287 | return function (css, result) { 288 | messages = []; 289 | 290 | // ensure plugin running in the end 291 | if (result.processor.plugins[result.processor.plugins.length - 1] !== result.lastPlugin) { 292 | // remove plugin duplicates 293 | result.processor.plugins = result.processor.plugins.filter(function (plugin) { 294 | return plugin.postcssPlugin === pkg.name ? plugin === result.lastPlugin : true; 295 | }); 296 | 297 | // adding it to the end of the plugins array 298 | result.processor.plugins.push(result.lastPlugin); 299 | 300 | return false; 301 | } 302 | 303 | roots = []; 304 | treeSelectors = selectors = 0; 305 | 306 | processTree(css); 307 | 308 | result.root = css; 309 | 310 | // for that old node.js 311 | var destination = result.opts.to ? 312 | (path.parse ? path.parse : require('path-parse'))(result.opts.to) : 313 | null; 314 | 315 | var rootsProcessing = []; 316 | 317 | roots.forEach(function (root, index) { 318 | rootsProcessing.push( 319 | processRoot(result.processor, root, index, destination, result) 320 | ); 321 | }, this); 322 | 323 | if (roots.length) { 324 | log('Divided into %s style files %s (Found %s selectors)', 325 | roots.length, 326 | result.opts.to ? 'from ' + result.opts.to : '', 327 | selectors 328 | ); 329 | } else { 330 | log('Found %s selectors, skipping %s', 331 | selectors, 332 | result.opts.to || '' 333 | ); 334 | } 335 | 336 | return new Promise(function (pluginDone, pluginFailed) { 337 | Promise.all(rootsProcessing).then(function (processedRoots) { 338 | 339 | result.roots = processedRoots; 340 | 341 | if (options.writeImport && processedRoots.length) { 342 | // find charset node 343 | var charsetRule; 344 | css.walkAtRules('charset', function (rule) { 345 | if (!charsetRule) { 346 | charsetRule = rule; 347 | return false; 348 | } 349 | }); 350 | 351 | if (charsetRule) { 352 | charsetRule.remove(); 353 | css.prepend(charsetRule); 354 | } 355 | 356 | var destinationError; 357 | 358 | for (var i = processedRoots.length - 1; i >= 0; i--) { 359 | if (processedRoots[i].opts.to) { 360 | var importNode = postcss.atRule({ 361 | name: 'import', 362 | params: 'url(' + path.basename(processedRoots[i].opts.to) + ')' 363 | }); 364 | charsetRule ? 365 | css.insertAfter(charsetRule, importNode) : 366 | css.prepend(importNode); 367 | } else { 368 | destinationError = true; 369 | } 370 | } 371 | 372 | if (destinationError) { 373 | result.warn( 374 | 'Destination is not provided, ' + 375 | '@import directive will be not written' 376 | ); 377 | } 378 | } 379 | 380 | result.messages = result.messages.concat(messages); 381 | pluginDone(); 382 | 383 | }).catch(pluginFailed); 384 | }); 385 | }; 386 | }); 387 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | 5 | /** 6 | * Extends first passed argument object with objects from other arguments 7 | * @returns {Object} first argument 8 | */ 9 | extend: function () { 10 | var obj = arguments[0]; 11 | 12 | for (var i = 1; i < arguments.length; i++) { 13 | var mergeObj = arguments[i]; 14 | for (var key in mergeObj) { 15 | if (mergeObj.hasOwnProperty(key)) { 16 | obj[key] = mergeObj[key]; 17 | } 18 | } 19 | } 20 | 21 | return obj; 22 | }, 23 | 24 | /** 25 | * Merge into first passed argument object parameters from other objects 26 | * @returns {Object} first argument 27 | */ 28 | defaults: function () { 29 | var obj = arguments[0]; 30 | 31 | for (var i = 1; i < arguments.length; i++) { 32 | var mergeObj = arguments[i]; 33 | for (var key in mergeObj) { 34 | if (mergeObj.hasOwnProperty(key) && typeof obj[key] === 'undefined') { 35 | obj[key] = mergeObj[key]; 36 | } 37 | } 38 | } 39 | 40 | return obj; 41 | }, 42 | 43 | /** 44 | * Retrieve filename for style by filling template from options 45 | * 46 | * @param {String} filename 47 | * @param {Object} [destination] object returned by path.parse 48 | * @param {number} index 49 | * @returns {string|null} 50 | */ 51 | getFileName: function (filename, destination, index) { 52 | if (!destination) return null; 53 | 54 | filename = filename 55 | .replace(/%i%/gm, index) 56 | .replace(/%original%/gm, destination.name); 57 | 58 | return destination.dir + path.sep + filename + destination.ext; 59 | }, 60 | 61 | /** 62 | * Finds obj in array of arrays where 0 el is obj-key 63 | * 64 | * @param {Object} obj 65 | * @param {Array.>}arr 66 | * @returns {*} 67 | */ 68 | find: function (obj, arr) { 69 | for (var i = 0, maxI = arr.length; i < maxI; i++) { 70 | if (arr[i][0] === obj) { 71 | return arr[i][1]; 72 | } 73 | } 74 | }, 75 | 76 | /** 77 | * Returns parents for the node 78 | * 79 | * @param {Node} node 80 | * @returns {Array.} 81 | */ 82 | parents: function (node) { 83 | var parents = []; 84 | var parent = node.parent; 85 | 86 | while (parent && parent.type !== 'root') { 87 | parents.push(parent); 88 | parent = parent.parent; 89 | } 90 | 91 | return parents; 92 | } 93 | 94 | }; 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-esplit", 3 | "version": "0.1.2", 4 | "description": "PostCSS plugin for splitting css into multiple css files with sourcemaps support for preventing limitation of shinny ie9", 5 | "keywords": [ 6 | "postcss", 7 | "css", 8 | "postcss-plugin", 9 | "ie9", 10 | "selectors-split", 11 | "css-split" 12 | ], 13 | "author": "Vitaliy Ribachenko ", 14 | "license": "MIT", 15 | "repository": "vitaliyr/postcss-esplit", 16 | "bugs": { 17 | "url": "https://github.com/vitaliyr/postcss-esplit/issues" 18 | }, 19 | "files": [ 20 | "index.js", 21 | "lib" 22 | ], 23 | "homepage": "https://github.com/vitaliyr/postcss-esplit", 24 | "dependencies": { 25 | "chalk": "^1.1.1", 26 | "mkdirp": "^0.5.1", 27 | "path-parse": "^1.0.5", 28 | "postcss": "^5.0.21" 29 | }, 30 | "devDependencies": { 31 | "chai": "^3.2.0", 32 | "csswring": "^5.0.0", 33 | "gulp": "^3.9.0", 34 | "gulp-eslint": "^1.0.0", 35 | "gulp-mocha": "^2.1.3" 36 | }, 37 | "scripts": { 38 | "test": "gulp test" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | var postcss = require('postcss'); 2 | var chai = require('chai'); 3 | var expect = chai.expect; 4 | 5 | var path = require('path'); 6 | var testHelpers = require('./test_helpers'); 7 | 8 | var helpers = require('../lib/helpers'); 9 | 10 | 11 | describe('helpers', function () { 12 | 13 | it('Defaults should extend objects', function (done) { 14 | var a = {}; 15 | var b = { param: true }; 16 | 17 | var c = helpers.defaults(a, b); 18 | 19 | expect(c).to.eql(a); 20 | expect(c.param).to.eql(b.param); 21 | 22 | done(); 23 | }); 24 | 25 | it('Defaults should not rewrite already defined parameters in source', function (done) { 26 | var a = { param: true }; 27 | var b = { param: false }; 28 | 29 | var c = helpers.defaults(a, b); 30 | 31 | expect(c.param).to.eql(true); 32 | 33 | done(); 34 | }); 35 | 36 | it('Extend should extend objects for first argument', function (done) { 37 | var a = {}; 38 | var b = {}; 39 | var c = { something: false }; 40 | 41 | var d = helpers.extend(a, b, c); 42 | 43 | expect(d).to.eql(a); 44 | expect(d).to.not.eql(b); 45 | 46 | done(); 47 | }); 48 | 49 | it('Extend should not deeply extend objects', function (done) { 50 | var a = {}; 51 | var b = { 52 | something: { 53 | test: true 54 | } 55 | }; 56 | 57 | helpers.extend(a, b); 58 | 59 | expect(a.something).to.be.eql(b.something); 60 | 61 | helpers.extend(a, { 62 | something: { 63 | another: false 64 | } 65 | }); 66 | 67 | expect(a.something.test).to.eql(undefined); 68 | expect(a.something.another).to.eql(false); 69 | 70 | done(); 71 | }); 72 | 73 | it('getFileName should return proper filename', function (done) { 74 | var destination = path.parse(testHelpers.testPath); 75 | var fileName; 76 | 77 | fileName = helpers.getFileName('test', destination, 0); 78 | expect(fileName).to.be.eql(testHelpers.testPath); 79 | 80 | fileName = helpers.getFileName('test-%i%', destination, 11); 81 | expect(fileName).to.be.eql(testHelpers.testDir + '/test-11.css'); 82 | 83 | fileName = helpers.getFileName('%original%test%i%', destination, 1); 84 | expect(fileName).to.be.eql(testHelpers.testDir + '/testtest1.css'); 85 | 86 | done(); 87 | }); 88 | 89 | it('find should find object in array', function (done) { 90 | var obj00 = { iam: '00' }; 91 | var obj01 = { iam: '01' }; 92 | var obj10 = { iam: '10' }; 93 | var obj11 = { iam: '11' }; 94 | var obj20 = { iam: '20' }; 95 | var obj21 = { iam: '21' }; 96 | var arr = [ 97 | [obj00, obj01], 98 | [obj10, obj11], 99 | [obj20, obj21] 100 | ]; 101 | var obj; 102 | 103 | obj = helpers.find(obj20, arr); 104 | expect(obj).to.be.eql(obj21); 105 | 106 | obj = helpers.find(obj10, arr); 107 | expect(obj).to.be.eql(obj11); 108 | 109 | obj = helpers.find(obj21, arr); 110 | expect(obj).to.be.not.ok; 111 | 112 | done(); 113 | }); 114 | 115 | it('parents should return all parents', function (done) { 116 | var root = postcss.root(); 117 | var node = postcss.parse( 118 | '@media (max-heigth: 10px){ a{} @media (max-width: 10px) { b{} } }' 119 | ); 120 | root.append(node); 121 | var parents; 122 | 123 | parents = helpers.parents(root); 124 | expect(parents).to.have.length(0); 125 | 126 | parents = helpers.parents(root.nodes[0]); 127 | expect(parents).to.have.length(0); 128 | 129 | parents = helpers.parents(root.nodes[0].nodes[1].nodes[0]); 130 | expect(parents).to.have.length(2); 131 | expect(parents[1]).to.be.eql(root.nodes[0]); 132 | 133 | done(); 134 | }); 135 | 136 | }); 137 | -------------------------------------------------------------------------------- /test/integration.js: -------------------------------------------------------------------------------- 1 | var postcss = require('postcss'); 2 | var chai = require('chai'); 3 | var expect = chai.expect; 4 | 5 | var testHelpers = require('./test_helpers'); 6 | 7 | var csswring = require('csswring'); 8 | var plugin = require('../'); 9 | 10 | describe('Integration', function () { 11 | 12 | it('Plugin should run after other plugins', function (done) { 13 | var opts = { 14 | writeFiles: false, 15 | writeImport: false, 16 | maxSelectors: 1 17 | }; 18 | var input = 'a{ color: red; }\nb{ font-weight : 700 }\nc{}'; 19 | 20 | postcss([plugin(opts), csswring()]).process(input, { to: testHelpers.testPath }) 21 | .then(function (result) { 22 | expect(result.warnings()).to.be.empty; 23 | expect(result.roots).to.have.length(1); // because csswring will remove c{} 24 | expect(result.roots[0].css).to.eql('a{color:red}'); 25 | expect(result.css).to.eql('b{font-weight:700}'); 26 | done(); 27 | }).catch(done); 28 | }); 29 | 30 | }); 31 | -------------------------------------------------------------------------------- /test/plugin.js: -------------------------------------------------------------------------------- 1 | /* eslint max-len: [2, 160] */ 2 | 3 | var chai = require('chai'); 4 | var expect = chai.expect; 5 | 6 | var fs = require('fs'); 7 | var path = require('path'); 8 | var pkg = require('../package.json'); 9 | 10 | var testHelpers = require('./test_helpers'); 11 | 12 | var test = testHelpers.test; 13 | var testOwnSuite = testHelpers.testOwnSuite; 14 | 15 | /** 16 | * For temporary outputting css files 17 | * @const 18 | * @type {string} 19 | */ 20 | 21 | describe(pkg.name, function () { 22 | 23 | testHelpers.clearTestDir(); 24 | 25 | it('There are no splitted files even if writeFiles is true', function (done) { 26 | var source = 'a{}'; 27 | 28 | testOwnSuite(source, { writeFiles: false }, done, function (result) { 29 | expect(result.css).to.eql(source); 30 | expect(result.warnings()).to.be.empty; 31 | expect(result.roots).to.be.empty; 32 | }); 33 | }); 34 | 35 | it('Not write files if writeFiles is false', function (done) { 36 | var source = 'a{}b{}c{}'; 37 | 38 | testOwnSuite(source, { maxSelectors: 1, writeFiles: false }, done, function (result) { 39 | expect(result.warnings()).to.have.length(1); 40 | expect(result.roots).to.have.length(2); 41 | 42 | if (fs.existsSync(testHelpers.testDir)) { 43 | expect(fs.readdirSync(testHelpers.testDir)).to.have.length(0); 44 | } 45 | }); 46 | }); 47 | 48 | it('Fail if there are splitted files and writeFiles is true but opts.to is empty', function (done) { 49 | var source = 'a{} b{}'; 50 | 51 | testOwnSuite(source, { maxSelectors: 1 }, done, function (result) { 52 | expect(result.warnings()).to.be.not.empty; 53 | expect(result.roots).to.be.not.empty; 54 | }); 55 | }); 56 | 57 | it('Not split file with less count of selectors than maxSelectors', function (done) { 58 | test('a{ }', 'a{ }', [], { writeFiles: false }, done); 59 | }); 60 | 61 | it('Split file correctly without warnings by providing maxSelectors', function (done) { 62 | test('a{} b{}', 'b{}', ['a {}'], { maxSelectors: 1, writeFiles: false, writeImport: false }, done); 63 | }); 64 | 65 | it('Split file to 3 correctly without warning by providing maxSelectors', function (done) { 66 | test( 67 | 'a{} b{} c{}', 68 | 'c{}', 69 | ['a {}', 'b {}'], 70 | { maxSelectors: 1, writeFiles: false, writeImport: false }, 71 | done 72 | ); 73 | }); 74 | 75 | it('Split file to 3 with proper import order', function (done) { 76 | var source = 'x{} y{} z{}'; 77 | 78 | testOwnSuite(source, { maxSelectors: 1, writeFiles: true }, done, function (result) { 79 | expect(result.warnings()).to.be.empty; 80 | expect(result.roots.length).to.eql(2); 81 | 82 | var urlRegExp = /url\(|\)/gm; 83 | 84 | var importNode = result.root.nodes[0].params.replace(urlRegExp, ''); 85 | var importNode2 = result.root.nodes[1].params.replace(urlRegExp, ''); 86 | 87 | expect(path.basename(result.roots[0].opts.to)).to.eql(importNode); 88 | expect(path.basename(result.roots[1].opts.to)).to.eql(importNode2); 89 | 90 | expect(result.roots[0].css).to.eql('x{}'); 91 | expect(result.roots[1].css).to.eql('y{}'); 92 | 93 | expect(fs.readdirSync(testHelpers.testDir)).to.have.length(2); 94 | expect(fs.readFileSync(testHelpers.testDir + path.sep + 'test-0.css', 'utf-8')).to.eql('x{}'); 95 | expect(fs.readFileSync(testHelpers.testDir + path.sep + 'test-1.css', 'utf-8')).to.eql('y{}'); 96 | }, { 97 | to: testHelpers.testPath 98 | }); 99 | }); 100 | 101 | it('Split file and write correct names', function (done) { 102 | var source = 'a{} b{} c{}'; 103 | 104 | testOwnSuite(source, { maxSelectors: 1, writeFiles: true, fileName: '%original%-00%i%' }, done, function (result) { 105 | expect(result.warnings()).to.be.empty; 106 | expect(result.roots.length).to.eql(2); 107 | 108 | var urlRegExp = /url\(|\)/gm; 109 | 110 | var importNode = result.root.nodes[0].params.replace(urlRegExp, ''); 111 | var importNode2 = result.root.nodes[1].params.replace(urlRegExp, ''); 112 | 113 | expect(path.basename(result.roots[0].opts.to)).to.eql(importNode); 114 | expect(path.basename(result.roots[1].opts.to)).to.eql(importNode2); 115 | 116 | expect(result.roots[0].css).to.eql('a{}'); 117 | expect(result.roots[1].css).to.eql('b{}'); 118 | 119 | expect(fs.readFileSync(testHelpers.testDir + path.sep + 'test-000.css', 'utf-8')).to.eql('a{}'); 120 | expect(fs.readFileSync(testHelpers.testDir + path.sep + 'test-001.css', 'utf-8')).to.eql('b{}'); 121 | }, { 122 | to: testHelpers.testPath 123 | }); 124 | }); 125 | 126 | it('Split file and write correct names, starting from 10 index', function (done) { 127 | var source = 'q{} w{} e{}'; 128 | 129 | testOwnSuite(source, 130 | { maxSelectors: 1, writeFiles: true, fileName: '%original%-%i%', fileNameStartIndex: 10 }, done, 131 | function (result) { 132 | expect(result.warnings()).to.be.empty; 133 | expect(result.roots.length).to.eql(2); 134 | 135 | var urlRegExp = /url\(|\)/gm; 136 | 137 | var importNode = result.root.nodes[0].params.replace(urlRegExp, ''); 138 | var importNode2 = result.root.nodes[1].params.replace(urlRegExp, ''); 139 | 140 | expect(path.basename(result.roots[0].opts.to)).to.eql(importNode); 141 | expect(path.basename(result.roots[1].opts.to)).to.eql(importNode2); 142 | 143 | expect(result.roots[0].css).to.eql('q{}'); 144 | expect(result.roots[1].css).to.eql('w{}'); 145 | 146 | expect(fs.readFileSync(testHelpers.testDir + path.sep + 'test-10.css', 'utf-8')).to.eql('q{}'); 147 | expect(fs.readFileSync(testHelpers.testDir + path.sep + 'test-11.css', 'utf-8')).to.eql('w{}'); 148 | }, { 149 | to: testHelpers.testPath 150 | }); 151 | }); 152 | 153 | it('Split file correctly with media queries', function (done) { 154 | test( 155 | 'a{} @media (max-width: 0px) { a{} b{} } c {}', 156 | '@media (max-width: 0px) { b{} } c {}', 157 | [ 158 | 'a{} @media (max-width: 0px) { a {} }' 159 | ], 160 | { maxSelectors: 2, writeFiles: false, writeImport: false }, 161 | done 162 | ); 163 | }); 164 | 165 | it('Split file correctly with unbreakable at-rules', function (done) { 166 | test( 167 | 'a{} @keyframes animation {0% {background:red;}100% {background:blue;} } b{}', 168 | 'b{}', 169 | [ 170 | 'a{} @keyframes animation {0% {background:red;}100% {background:blue;} }' 171 | ], 172 | { maxSelectors: 2, writeFiles: false, writeImport: false }, 173 | done 174 | ); 175 | }); 176 | 177 | it('Split file correctly with multiple unbreakable at-rules', function (done) { 178 | test( 179 | 'a{} @keyframes animation {0% {background:red;}100% {background:blue;}} b{} c{} @keyframes animation2 {100% {background:blue;}}', 180 | 'b{} c{} @keyframes animation2 {100% {background:blue;}}', 181 | [ 182 | 'a{} @keyframes animation {0% {background:red;}100% {background:blue;}}' 183 | ], 184 | { maxSelectors: 2, writeFiles: false, writeImport: false }, 185 | done 186 | ); 187 | }); 188 | 189 | it('Split file with empty media queries', function (done) { 190 | test( 191 | '@media (max-width: 0px) { a{} b{} }', 192 | '@media (max-width: 0px) { b{} }', 193 | [ 194 | '@media (max-width: 0px) { a{} }' 195 | ], 196 | { maxSelectors: 1, writeFiles: false, writeImport: false }, 197 | done 198 | ); 199 | }); 200 | 201 | it('Split file with nested atrules', function (done) { 202 | test( 203 | '@media (max-width: 0px) { a{} @media (height: 0px) { b {} } c{} }', 204 | '@media (max-width: 0px) { c{} }', 205 | [ 206 | '@media (max-width: 0px) { a{} }', 207 | '@media (max-width: 0px) { @media (height: 0px) { b{} } }' 208 | ], 209 | { maxSelectors: 1, writeFiles: false, writeImport: false }, 210 | done 211 | ); 212 | }); 213 | 214 | it('Split file correctly with nested (wrong) media queries', function (done) { 215 | test( 216 | '@media (max-width: 0px) { a{} @media (max-height: 0px) { b {} } } c {}', 217 | 'c {}', 218 | [ 219 | '@media (max-width: 0px) { a{} }', 220 | '@media (max-width: 0px) { @media (max-height: 0px) { b{} } }' 221 | ], 222 | { maxSelectors: 1, writeFiles: false, writeImport: false }, 223 | done 224 | ); 225 | }); 226 | 227 | it('Split file correctly with few selectors in rule', function (done) { 228 | test( 229 | 'a,b,c {}', 230 | 'c {}', 231 | [ 232 | 'a{}', 233 | 'b{}' 234 | ], 235 | { maxSelectors: 1, writeFiles: false, writeImport: false }, 236 | done 237 | ); 238 | }); 239 | 240 | it('Split file correctly with few selectors in rule with data', function (done) { 241 | test( 242 | 'a,b,c { font-weight: bold; color: red }', 243 | 'c { font-weight: bold; color: red }', 244 | [ 245 | 'a{ font-weight: bold; color: red }', 246 | 'b{ font-weight: bold; color: red }' 247 | ], 248 | { maxSelectors: 1, writeFiles: false, writeImport: false }, 249 | done 250 | ); 251 | }); 252 | 253 | it('Should split correctly with moving @font-face', function (done) { 254 | test( 255 | 'a{} @font-face { font-family:proxima_nova_rgregular; src:url(font.eot); } b{} c{}', 256 | 'c {}', 257 | [ 258 | 'a{} @font-face { font-family:proxima_nova_rgregular; src:url(font.eot); } b{}' 259 | ], 260 | { maxSelectors: 2, writeFiles: false, writeImport: false }, 261 | done 262 | ); 263 | }); 264 | 265 | it('Should split correctly with moving @font-face and splitting big selector', function (done) { 266 | test( 267 | 'a,b{} c{} @font-face { font-family:proxima_nova_rgregular; src:url(font.eot); } d,e{} f{}', 268 | 'e{} f{}', 269 | [ 270 | 'a,b{}', 271 | 'c{} @font-face { font-family:proxima_nova_rgregular; src:url(font.eot); } d{}' 272 | ], 273 | { maxSelectors: 2, writeFiles: false, writeImport: false }, 274 | done 275 | ); 276 | }); 277 | 278 | it('Should insert imports after the @charset if any', function (done) { 279 | var source = '@charset "UTF-8";a{}b{}'; 280 | 281 | testOwnSuite(source, { maxSelectors: 1, writeFiles: false }, done, function (result) { 282 | expect(result.warnings()).to.be.empty; 283 | expect(result.roots.length).to.eql(1); 284 | 285 | expect(testHelpers.clearCss(result.css)).to.eql(testHelpers.clearCss('@charset "UTF-8";@import url(test-0.css);b{}')); 286 | expect(result.roots[0].css).to.eql('a{}'); 287 | }, { 288 | to: testHelpers.testPath 289 | }); 290 | }); 291 | 292 | it('Should insert imports after the first @charset if there are lot of them', function (done) { 293 | var source = 'a{}b{}@charset "UTF-8";c{}@charset "KOI8-R";d{}'; 294 | 295 | testOwnSuite(source, { maxSelectors: 1, writeFiles: false }, done, function (result) { 296 | expect(result.warnings()).to.be.empty; 297 | expect(result.roots.length).to.eql(3); 298 | 299 | expect(result.css).to.eql('@charset "UTF-8";@import url(test-0.css);@import url(test-1.css);@import url(test-2.css);@charset "KOI8-R";d{}'); 300 | expect(result.roots[0].css).to.eql('a{}'); 301 | expect(result.roots[1].css).to.eql('b{}'); 302 | expect(result.roots[2].css).to.eql('c{}'); 303 | }, { 304 | to: testHelpers.testPath 305 | }); 306 | }); 307 | 308 | it('Should insert imports in the beginning of the css if no charset is provided', function (done) { 309 | var source = 'a{}b{}c{}d{}'; 310 | 311 | testOwnSuite(source, { maxSelectors: 1, writeFiles: false }, done, function (result) { 312 | expect(result.warnings()).to.be.empty; 313 | expect(result.roots.length).to.eql(3); 314 | 315 | expect(testHelpers.clearCss(result.css)).to.eql( 316 | testHelpers.clearCss('@import url(test-0.css);@import url(test-1.css);@import url(test-2.css);d{}') 317 | ); 318 | expect(result.roots[0].css).to.eql('a{}'); 319 | expect(result.roots[1].css).to.eql('b{}'); 320 | expect(result.roots[2].css).to.eql('c{}'); 321 | }, { 322 | to: testHelpers.testPath 323 | }); 324 | }); 325 | 326 | it('Should move charset to the beginning of the css if it is located in the deep of css', function (done) { 327 | var source = 'a{}b{}c{}d{}@charset "UTF-8";'; 328 | 329 | testOwnSuite(source, { maxSelectors: 1, writeFiles: false }, done, function (result) { 330 | expect(result.warnings()).to.be.empty; 331 | expect(result.roots.length).to.eql(3); 332 | 333 | expect(testHelpers.clearCss(result.css)).to.eql( 334 | testHelpers.clearCss( 335 | '@charset "UTF-8";@import url(test-0.css);@import url(test-1.css);@import url(test-2.css);d{}' 336 | ) 337 | ); 338 | expect(result.roots[0].css).to.eql('a{}'); 339 | expect(result.roots[1].css).to.eql('b{}'); 340 | expect(result.roots[2].css).to.eql('c{}'); 341 | }, { 342 | to: testHelpers.testPath 343 | }); 344 | }); 345 | 346 | it('New roots should copy raws from original root', function (done) { 347 | var source = 'a { color:red; } b {color: white }\n'; 348 | 349 | testOwnSuite(source, { maxSelectors: 1, writeFiles: false, writeImport: false }, done, function (result) { 350 | expect(result.warnings()).to.be.empty; 351 | expect(result.roots.length).to.eql(1); 352 | 353 | expect(result.css).to.eql('b {color: white }\n'); 354 | expect(result.roots[0].css).to.eql('a { color:red; }\n'); 355 | }, { 356 | to: testHelpers.testPath 357 | }); 358 | }); 359 | 360 | }); 361 | -------------------------------------------------------------------------------- /test/test_helpers.js: -------------------------------------------------------------------------------- 1 | var postcss = require('postcss'); 2 | var chai = require('chai'); 3 | var expect = chai.expect; 4 | var AssertionError = chai.AssertionError; 5 | var plugin = require('../'); 6 | 7 | var fs = require('fs'); 8 | var path = require('path'); 9 | 10 | var spaceRegexp = / |\n/gmi; 11 | var testPath = 'test/output/test.css'; 12 | var testDir = 'test/output'; 13 | 14 | /** 15 | * Clears passed css from spaces and line breaks 16 | * @param {string} css 17 | * @returns {string} 18 | */ 19 | var clearCss = function (css) { 20 | return css.replace(spaceRegexp, ''); 21 | }; 22 | 23 | var testOwnSuite = function (input, opts, done, cb, processOpts) { 24 | return postcss([plugin(opts)]).process(input, processOpts).then(function (result) { 25 | try { 26 | cb(result); 27 | done(); 28 | } catch (e) { 29 | done(e); 30 | } 31 | }).catch(function (error) { 32 | done( 33 | error.actual instanceof Array ? 34 | new AssertionError(error.actual[0].text, {}) : 35 | error 36 | ); 37 | }); 38 | }; 39 | 40 | var test = function (input, output, splittedFiles, opts, done) { 41 | var args = [input, opts, done]; 42 | 43 | args.push(function (result) { 44 | expect(clearCss(result.css)).to.eql(clearCss(output)); 45 | expect(result.warnings()).to.be.empty; 46 | 47 | if (splittedFiles.length) { 48 | expect(result.roots.length).to.be.eql(splittedFiles.length); 49 | result.roots.forEach(function (root, index) { 50 | expect(clearCss(root.css)).to.eql(clearCss(splittedFiles[index])); 51 | }); 52 | } 53 | }); 54 | 55 | testOwnSuite.apply(this, args); 56 | }; 57 | 58 | var clearTestDir = function () { 59 | if (fs.existsSync(testDir)) { 60 | fs.readdirSync(testDir).forEach(function (file) { 61 | fs.unlink(testDir + path.sep + file); 62 | }); 63 | } 64 | }; 65 | 66 | /** 67 | * Exports 68 | */ 69 | module.exports = { 70 | testPath: testPath, 71 | testDir: testDir, 72 | 73 | clearCss: clearCss, 74 | clearTestDir: clearTestDir, 75 | test: test, 76 | testOwnSuite: testOwnSuite 77 | }; 78 | --------------------------------------------------------------------------------