├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── example ├── basic │ ├── index.css │ ├── index.js │ └── webpack.config.js └── less │ ├── index.html │ ├── index.js │ ├── index.less │ └── webpack.config.js ├── package-lock.json ├── package.json ├── src ├── chunk.js └── index.js └── test └── spec ├── .eslintrc ├── chunk.spec.js └── index.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "plugins": [ 5 | ["transform-adana", { 6 | "only": "src/**" 7 | }] 8 | ] 9 | } 10 | }, 11 | "plugins": [ 12 | "syntax-object-rest-spread", 13 | "transform-object-rest-spread" 14 | ], 15 | "presets": [ 16 | ["env", { 17 | "targets": { 18 | "node": 4 19 | } 20 | }], 21 | "flow" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # 2 | # EditorConfig: http://EditorConfig.org 3 | # 4 | # This files specifies some basic editor conventions for the files in this 5 | # project. Many editors support this standard, you simply need to find a plugin 6 | # for your favorite! 7 | # 8 | # For a full list of possible values consult the reference. 9 | # https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties 10 | # 11 | 12 | # Stop searching for other .editorconfig files above this folder. 13 | root = true 14 | 15 | # Pick some sane defaults for all files. 16 | [*] 17 | 18 | # UNIX line-endings are preferred. 19 | # http://adaptivepatchwork.com/2012/03/01/mind-the-end-of-your-line/ 20 | end_of_line = lf 21 | 22 | # No reason in these modern times to use anything other than UTF-8. 23 | charset = utf-8 24 | 25 | # Ensure that there's no bogus whitespace in the file. 26 | trim_trailing_whitespace = true 27 | 28 | # A little esoteric, but it's kind of a standard now. 29 | # http://stackoverflow.com/questions/729692/why-should-files-end-with-a-newline 30 | insert_final_newline = true 31 | 32 | # Pragmatism today. 33 | # http://programmers.stackexchange.com/questions/57 34 | indent_style = 2 35 | 36 | # Personal preference here. Smaller indent size means you can fit more on a line 37 | # which can be nice when there are lines with several indentations. 38 | indent_size = 2 39 | 40 | # Prefer a more conservative default line length – this allows editors with 41 | # sidebars, minimaps, etc. to show at least two documents side-by-side. 42 | # Hard wrapping by default for code is useful since many editors don't support 43 | # an elegant soft wrap; however, soft wrap is fine for things where text just 44 | # flows normally, like Markdown documents or git commit messages. Hard wrap 45 | # is also easier for line-based diffing tools to consume. 46 | # See: http://tex.stackexchange.com/questions/54140 47 | max_line_length = 80 48 | 49 | # Markdown uses trailing spaces to create line breaks. 50 | [*.md] 51 | trim_trailing_whitespace = false 52 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/**/* 2 | node_modules 3 | example 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | extends: 2 | - metalab 3 | rules: 4 | metalab/filenames/match-exported: 0 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | dist 4 | coverage 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | examples 3 | *.log 4 | coverage 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 4 5 | - 6 6 | - 8 7 | env: 8 | - USE_WEBPACK2=false 9 | - USE_WEBPACK2=true 10 | matrix: 11 | exclude: 12 | # TODO: Fix barf on peer dependencies with npm@2. 13 | - env: USE_WEBPACK2=true 14 | node_js: 4 15 | 16 | before_install: 17 | - 'if [ "${USE_WEBPACK2}" == "true" ]; then npm install --save-dev webpack@2.1.0-beta.19 extract-text-webpack-plugin@2.0.0-beta.4; fi' 18 | 19 | after_script: 20 | - npm install coveralls 21 | - cat ./coverage/coverage.json | ./node_modules/.bin/adana --format lcov | ./node_modules/coveralls/bin/coveralls.js 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # css-split-webpack-plugin 2 | 3 | Split your CSS for stupid browsers using [webpack] and [postcss]. 4 | 5 | ![build status](http://img.shields.io/travis/metalabdesign/css-split-webpack-plugin/master.svg?style=flat) 6 | ![coverage](http://img.shields.io/coveralls/metalabdesign/css-split-webpack-plugin/master.svg?style=flat) 7 | ![license](http://img.shields.io/npm/l/css-split-webpack-plugin.svg?style=flat) 8 | ![version](http://img.shields.io/npm/v/css-split-webpack-plugin.svg?style=flat) 9 | ![downloads](http://img.shields.io/npm/dm/css-split-webpack-plugin.svg?style=flat) 10 | 11 | Using [webpack] to generate your CSS is fun for some definitions of fun. Unfortunately the fun stops when you have a large app and need IE9 support because IE9 will ignore any more than ~4000 selectors in your lovely generated CSS bundle. The solution is to split your CSS bundle smartly into multiple smaller CSS files. Now _you can_.™ Supports source-maps. 12 | 13 | ## Installation 14 | 15 | ```sh 16 | npm install --save css-split-webpack-plugin 17 | ``` 18 | 19 | ## Usage 20 | 21 | Simply add an instance of `CSSSplitWebpackPlugin` to your list of plugins in your webpack configuration file _after_ `ExtractTextPlugin`. That's it! 22 | 23 | ```javascript 24 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 25 | var CSSSplitWebpackPlugin = require('../').default; 26 | 27 | module.exports = { 28 | entry: './index.js', 29 | context: __dirname, 30 | output: { 31 | path: __dirname + '/dist', 32 | publicPath: '/foo', 33 | filename: 'bundle.js', 34 | }, 35 | module: { 36 | loaders: [{ 37 | test: /\.css$/, 38 | loader: ExtractTextPlugin.extract('style-loader', 'css-loader'), 39 | }], 40 | }, 41 | plugins: [ 42 | new ExtractTextPlugin('styles.css'), 43 | new CSSSplitWebpackPlugin({size: 4000}), 44 | ], 45 | }; 46 | ``` 47 | 48 | The following configuration options are available: 49 | 50 | **size**: `default: 4000` The maximum number of CSS rules allowed in a single file. To make things work with IE this value should be somewhere around `4000`. 51 | 52 | **imports**: `default: false` If you originally built your app to only ever consider using one CSS file then this flag is for you. It creates an additional CSS file that imports all of the split files. You pass `true` to turn this feature on, or a string with the name you'd like the generated file to have. 53 | 54 | **filename**: `default: "[name]-[part].[ext]"` Control how the split files have their names generated. The default uses the parent's filename and extension, but adds in the part number. 55 | 56 | **preserve**: `default: false`. Keep the original unsplit file as well. Sometimes this is desirable if you want to target a specific browser (IE) with the split files and then serve the unsplit ones to everyone else. 57 | 58 | **defer**: `default: 'false'`. You can pass `true` here to cause this plugin to split the CSS on the `emit` phase. Sometimes this is needed if you have other plugins that operate on the CSS also in the emit phase. Unfortunately by doing this you potentially lose chunk linking and source maps. Use only when necessary. 59 | 60 | [webpack]: http://webpack.github.io/ 61 | [herp]: https://github.com/ONE001/css-file-rules-webpack-separator 62 | [postcss]: https://github.com/postcss/postcss 63 | [postcss-chunk]: https://github.com/mattfysh/postcss-chunk 64 | -------------------------------------------------------------------------------- /example/basic/index.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: green; 3 | } 4 | 5 | .bar { 6 | color: red; 7 | } 8 | 9 | .baz { 10 | color: yellow; 11 | } 12 | 13 | .qux { 14 | color: blue; 15 | } 16 | -------------------------------------------------------------------------------- /example/basic/index.js: -------------------------------------------------------------------------------- 1 | require('./index.css'); 2 | -------------------------------------------------------------------------------- /example/basic/webpack.config.js: -------------------------------------------------------------------------------- 1 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 2 | var CSSSplitWebpackPlugin = require('../../').default; 3 | 4 | module.exports = { 5 | entry: './index.js', 6 | context: __dirname, 7 | output: { 8 | path: __dirname + '/dist', 9 | publicPath: '/foo', 10 | filename: 'bundle.js', 11 | }, 12 | module: { 13 | loaders: [{ 14 | test: /\.css$/, 15 | loader: ExtractTextPlugin.extract.length !== 1 ? 16 | ExtractTextPlugin.extract('style-loader', 'css-loader') : 17 | ExtractTextPlugin.extract({ 18 | fallbackLoader: 'style-loader', 19 | loader: 'css-loader', 20 | }), 21 | }], 22 | }, 23 | devtool: 'source-map', 24 | plugins: [ 25 | new ExtractTextPlugin("styles.css"), 26 | new CSSSplitWebpackPlugin({size: 3, imports: true}), 27 | ], 28 | }; 29 | -------------------------------------------------------------------------------- /example/less/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LESS Example 6 | 7 | 8 | 9 | 10 |
foo
11 |
bar
12 |
baz
13 |
qux
14 | 15 | 16 | -------------------------------------------------------------------------------- /example/less/index.js: -------------------------------------------------------------------------------- 1 | require('./index.less'); 2 | -------------------------------------------------------------------------------- /example/less/index.less: -------------------------------------------------------------------------------- 1 | @green: green; 2 | @red: red; 3 | @yellow: yellow; 4 | @blue: blue; 5 | 6 | .foo { 7 | color: @green; 8 | } 9 | 10 | .bar { 11 | color: @red; 12 | } 13 | 14 | .baz { 15 | color: @yellow; 16 | } 17 | 18 | .qux { 19 | color: @blue; 20 | } 21 | -------------------------------------------------------------------------------- /example/less/webpack.config.js: -------------------------------------------------------------------------------- 1 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 2 | var CSSSplitWebpackPlugin = require('../../').default; 3 | 4 | module.exports = { 5 | entry: './index.js', 6 | context: __dirname, 7 | output: { 8 | path: __dirname + '/dist', 9 | publicPath: '/foo', 10 | filename: 'bundle.js', 11 | }, 12 | module: { 13 | loaders: [{ 14 | test: /\.less$/, 15 | loader: ExtractTextPlugin.extract( 16 | 'css?-url&-autoprefixer&sourceMap!less?sourceMap' 17 | ), 18 | }], 19 | }, 20 | devtool: 'source-map', 21 | plugins: [ 22 | new ExtractTextPlugin("styles.css"), 23 | new CSSSplitWebpackPlugin({size: 3}), 24 | ], 25 | }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-split-webpack-plugin", 3 | "version": "0.2.6", 4 | "author": "Izaak Schroeder ", 5 | "license": "CC0-1.0", 6 | "repository": "metalabdesign/css-split-webpack-plugin", 7 | "main": "dist/index.js", 8 | "keywords": [ 9 | "webpack-plugin", 10 | "postcss" 11 | ], 12 | "scripts": { 13 | "prepublish": "./node_modules/.bin/babel -s inline -d ./dist ./src --source-maps true", 14 | "test": "npm run lint && npm run spec", 15 | "lint": "eslint .", 16 | "spec": "NODE_ENV=test ./node_modules/.bin/_mocha --timeout 5000 -r adana-dump --compilers js:babel-core/register -R spec --recursive test/spec/**/*.spec.js" 17 | }, 18 | "devDependencies": { 19 | "adana-cli": "^0.1.1", 20 | "adana-dump": "^0.1.0", 21 | "adana-format-lcov": "^0.1.1", 22 | "babel-cli": "^6.3.17", 23 | "babel-core": "^6.3.26", 24 | "babel-plugin-syntax-object-rest-spread": "^6.13.0", 25 | "babel-plugin-transform-adana": "^0.5.10", 26 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 27 | "babel-preset-env": "^1.6.0", 28 | "babel-preset-flow": "^6.23.0", 29 | "chai": "^3.5.0", 30 | "css-loader": "^0.23.1", 31 | "eslint": "^4.6.0", 32 | "eslint-config-metalab": "^7.0.1", 33 | "extract-text-webpack-plugin": "^1.0.1", 34 | "less": "^2.7.1", 35 | "less-loader": "^2.2.3", 36 | "memory-fs": "^0.3.0", 37 | "mocha": "^2.4.5", 38 | "optimize-css-assets-webpack-plugin": "^3.2.0", 39 | "style-loader": "^0.13.1", 40 | "webpack": "^1.13.0" 41 | }, 42 | "dependencies": { 43 | "loader-utils": "^1.1.0", 44 | "postcss": "^6.0.14", 45 | "webpack-sources": "^1.0.2" 46 | }, 47 | "peerDependencies": { 48 | "webpack": ">=1" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/chunk.js: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss'; 2 | 3 | /** 4 | * Get the number of selectors for a given node. 5 | * @param {Object} node CSS node in question. 6 | * @returns {Number} Total number of selectors associated with that node. 7 | */ 8 | const getSelLength = (node) => { 9 | if (node.type === 'rule') { 10 | return node.selectors.length; 11 | } 12 | if (node.type === 'atrule' && node.nodes) { 13 | return 1 + node.nodes.reduce((memo, n) => { 14 | return memo + getSelLength(n); 15 | }, 0); 16 | } 17 | return 0; 18 | }; 19 | 20 | /** 21 | * PostCSS plugin that splits the generated result into multiple results based 22 | * on number of selectors. 23 | * @param {Number} size Maximum number of rules in a single file. 24 | * @param {Function} result Options passed to `postcss.toResult()` 25 | * @returns {Object} `postcss` plugin instance. 26 | */ 27 | export default postcss.plugin('postcss-chunk', ({ 28 | size = 4000, 29 | result: genResult = () => { 30 | return {}; 31 | }, 32 | } = {}) => { 33 | return (css, result) => { 34 | const chunks = []; 35 | let count; 36 | let chunk; 37 | 38 | // Create a new chunk that holds current result. 39 | const nextChunk = () => { 40 | count = 0; 41 | chunk = css.clone({nodes: []}); 42 | chunks.push(chunk); 43 | }; 44 | 45 | // Walk the nodes. When we overflow the selector count, then start a new 46 | // chunk. Collect the nodes into the current chunk. 47 | css.nodes.forEach((n) => { 48 | const selCount = getSelLength(n); 49 | if (!chunk || count + selCount > size) { 50 | nextChunk(); 51 | } 52 | chunk.nodes.push(n); 53 | count += selCount; 54 | }); 55 | 56 | // Output the results. 57 | result.chunks = chunks.map((c, i) => { 58 | return c.toResult(genResult(i, c)); 59 | }); 60 | }; 61 | }); 62 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss'; 2 | import chunk from './chunk'; 3 | import {SourceMapSource, RawSource} from 'webpack-sources'; 4 | import {interpolateName} from 'loader-utils'; 5 | 6 | /** 7 | * Detect if a file should be considered for CSS splitting. 8 | * @param {String} name Name of the file. 9 | * @returns {Boolean} True if to consider the file, false otherwise. 10 | */ 11 | const isCSS = (name : string) : boolean => /\.css$/.test(name); 12 | 13 | /** 14 | * Remove the trailing `/` from URLs. 15 | * @param {String} str The url to strip the trailing slash from. 16 | * @returns {String} The stripped url. 17 | */ 18 | const strip = (str : string) : string => str.replace(/\/$/, ''); 19 | 20 | /** 21 | * Create a function that generates names based on some input. This uses 22 | * webpack's name interpolator under the hood, but since webpack's argument 23 | * list is all funny this exists just to simplify things. 24 | * @param {String} input Name to be interpolated. 25 | * @returns {Function} Function to do the interpolating. 26 | */ 27 | const nameInterpolator = (input) => ({file, content, index}) => { 28 | const res = interpolateName({ 29 | context: '/', 30 | resourcePath: `/${file}`, 31 | }, input, { 32 | content, 33 | }).replace(/\[part\]/g, index + 1); 34 | return res; 35 | }; 36 | 37 | /** 38 | * Normalize the `imports` argument to a function. 39 | * @param {Boolean|String} input The name of the imports file, or a boolean 40 | * to use the default name. 41 | * @param {Boolean} preserve True if the default name should not clash. 42 | * @returns {Function} Name interpolator. 43 | */ 44 | const normalizeImports = (input, preserve) => { 45 | switch (typeof input) { 46 | case 'string': 47 | return nameInterpolator(input); 48 | case 'boolean': 49 | if (input) { 50 | if (preserve) { 51 | return nameInterpolator('[name]-split.[ext]'); 52 | } 53 | return ({file}) => file; 54 | } 55 | return () => false; 56 | default: 57 | throw new TypeError(); 58 | } 59 | }; 60 | 61 | /** 62 | * Webpack plugin to split CSS assets into multiple files. This is primarily 63 | * used for dealing with IE <= 9 which cannot handle more than ~4000 rules 64 | * in a single stylesheet. 65 | */ 66 | export default class CSSSplitWebpackPlugin { 67 | /** 68 | * Create new instance of CSSSplitWebpackPlugin. 69 | * @param {Number} size Maximum number of rules for a single file. 70 | * @param {Boolean|String} imports Truish to generate an additional import 71 | * asset. When a boolean use the default name for the asset. 72 | * @param {String} filename Control the generated split file name. 73 | * @param {Boolean} defer Defer splitting until the `emit` phase. Normally 74 | * only needed if something else in your pipeline is mangling things at 75 | * the emit phase too. 76 | * @param {Boolean} preserve True to keep the original unsplit file. 77 | */ 78 | constructor({ 79 | size = 4000, 80 | imports = false, 81 | filename = '[name]-[part].[ext]', 82 | preserve, 83 | defer = false, 84 | }) { 85 | this.options = { 86 | size, 87 | imports: normalizeImports(imports, preserve), 88 | filename: nameInterpolator(filename), 89 | preserve, 90 | defer, 91 | }; 92 | } 93 | 94 | /** 95 | * Generate the split chunks for a given CSS file. 96 | * @param {String} key Name of the file. 97 | * @param {Object} asset Valid webpack Source object. 98 | * @returns {Promise} Promise generating array of new files. 99 | */ 100 | file(key : string, asset : Object) { 101 | // Use source-maps when possible. 102 | const input = asset.sourceAndMap ? asset.sourceAndMap() : { 103 | source: asset.source(), 104 | }; 105 | const getName = (i) => this.options.filename({ 106 | ...asset, 107 | content: input.source, 108 | file: key, 109 | index: i, 110 | }); 111 | return postcss([chunk(this.options)]).process(input.source, { 112 | from: undefined, 113 | map: { 114 | prev: input.map, 115 | }, 116 | }).then((result) => { 117 | return Promise.resolve({ 118 | file: key, 119 | chunks: result.chunks.map(({css, map}, i) => { 120 | const name = getName(i); 121 | const result = map ? new SourceMapSource( 122 | css, 123 | name, 124 | map.toString() 125 | ) : new RawSource(css); 126 | result.name = name; 127 | return result; 128 | }), 129 | }); 130 | }); 131 | } 132 | 133 | chunksMapping(compilation, chunks, done) { 134 | const assets = compilation.assets; 135 | const publicPath = strip(compilation.options.output.publicPath || './'); 136 | const promises = chunks.map((chunk) => { 137 | const input = chunk.files.filter(isCSS); 138 | const items = input.map((name) => this.file(name, assets[name])); 139 | return Promise.all(items).then((entries) => { 140 | entries.forEach((entry) => { 141 | // Skip the splitting operation for files that result in no 142 | // split occuring. 143 | if (entry.chunks.length === 1) { 144 | return; 145 | } 146 | // Inject the new files into the chunk. 147 | entry.chunks.forEach((file) => { 148 | assets[file.name] = file; 149 | chunk.files.push(file.name); 150 | }); 151 | const content = entry.chunks.map((file) => { 152 | return `@import "${publicPath}/${file._name}";`; 153 | }).join('\n'); 154 | const imports = this.options.imports({ 155 | ...entry, 156 | content, 157 | }); 158 | if (!this.options.preserve) { 159 | chunk.files.splice(chunk.files.indexOf(entry.file), 1); 160 | delete assets[entry.file]; 161 | } 162 | if (imports) { 163 | assets[imports] = new RawSource(content); 164 | chunk.files.push(imports); 165 | } 166 | }); 167 | return Promise.resolve(); 168 | }); 169 | }); 170 | Promise.all(promises).then(() => { 171 | done(); 172 | }, done); 173 | } 174 | 175 | /** 176 | * Run the plugin against a webpack compiler instance. Roughly it walks all 177 | * the chunks searching for CSS files and when it finds one that needs to be 178 | * split it does so and replaces the original file in the chunk with the split 179 | * ones. If the `imports` option is specified the original file is replaced 180 | * with an empty CSS file importing the split files, otherwise the original 181 | * file is removed entirely. 182 | * @param {Object} compiler Compiler instance 183 | * @returns {void} 184 | */ 185 | apply(compiler : Object) { 186 | if (this.options.defer) { 187 | // Run on `emit` when user specifies the compiler phase 188 | // Due to the incorrect css split + optimization behavior 189 | // Expected: css split should happen after optimization 190 | compiler.plugin('emit', (compilation, done) => { 191 | return this.chunksMapping(compilation, compilation.chunks, done); 192 | }); 193 | } else { 194 | // Only run on `this-compilation` to avoid injecting the plugin into 195 | // sub-compilers as happens when using the `extract-text-webpack-plugin`. 196 | compiler.plugin('this-compilation', (compilation) => { 197 | compilation.plugin('optimize-chunk-assets', (chunks, done) => { 198 | return this.chunksMapping(compilation, chunks, done); 199 | }); 200 | }); 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /test/spec/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "mocha": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/spec/chunk.spec.js: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss'; 2 | import plugin from '../../src/chunk'; 3 | import {expect} from 'chai'; 4 | 5 | const input = ` 6 | one {} 7 | two {} 8 | three {} 9 | four {} 10 | five {} 11 | six {} 12 | seven {} 13 | eight {} 14 | nine {} 15 | ten {} 16 | eleven {} 17 | twelve {} 18 | `; 19 | 20 | const test = (input, opts) => postcss([plugin(opts)]).process(input); 21 | 22 | describe('chunk', () => { 23 | it('breaks input into chunks of max size', () => { 24 | return test(input, {size: 5}).then(({chunks}) => { 25 | expect(chunks.length).to.equal(3); 26 | expect(chunks[0].root.nodes.length).to.equal(5); 27 | expect(chunks[2].root.nodes.length).to.equal(2); 28 | }); 29 | }); 30 | 31 | it('counts multiple selectors per rule', () => { 32 | const newInput = input.replace('two', 'two-a, two-b'); 33 | return test(newInput, {size: 5}).then(({chunks}) => { 34 | expect(chunks.length).to.equal(3); 35 | expect(chunks[0].root.nodes.length).to.equal(4); 36 | expect(chunks[2].root.nodes.length).to.equal(3); 37 | }); 38 | }); 39 | 40 | it('counts at-rules as one rule', () => { 41 | const newInput = `${input} @media print { a {}, b {}, c {} }`; 42 | return test(newInput, {size: 5}).then(({chunks}) => { 43 | expect(chunks.length).to.equal(4); 44 | expect(chunks[3].root.nodes.length).to.equal(1); 45 | }); 46 | }); 47 | 48 | it('supports nested at-rules', () => { 49 | const media = '@media print { @media print { a {}, b {}, c {} } }'; 50 | const newInput = input + media; 51 | return test(newInput, {size: 5}).then(({chunks}) => { 52 | expect(chunks.length).to.equal(4); 53 | expect(chunks[3].root.nodes.length).to.equal(1); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /test/spec/index.spec.js: -------------------------------------------------------------------------------- 1 | import _webpack from 'webpack'; 2 | import ExtractTextPlugin from 'extract-text-webpack-plugin'; 3 | import OptimizeCssPlugin from 'optimize-css-assets-webpack-plugin'; 4 | import CSSSplitWebpackPlugin from '../../src'; 5 | import path from 'path'; 6 | import MemoryFileSystem from 'memory-fs'; 7 | import {expect} from 'chai'; 8 | 9 | const basic = path.join('.', 'basic', 'index.js'); 10 | const less = path.join('.', 'less', 'index.js'); 11 | 12 | const extract = ExtractTextPlugin.extract.length !== 1 ? 13 | (a, b) => ExtractTextPlugin.extract(a, b) : 14 | (fallbackLoader, loader) => loader ? ExtractTextPlugin.extract({ 15 | fallbackLoader, 16 | loader, 17 | }) : ExtractTextPlugin.extract({ 18 | loader: fallbackLoader, 19 | }); 20 | 21 | const config = (options, entry = basic, { 22 | plugins, 23 | ...extra 24 | } = {devtool: 'source-map'}) => { 25 | return { 26 | entry: path.join(__dirname, '..', '..', 'example', entry), 27 | context: path.join(__dirname, '..', '..', 'example'), 28 | output: { 29 | path: path.join(__dirname, 'dist'), 30 | publicPath: '/foo', 31 | filename: 'bundle.js', 32 | }, 33 | module: { 34 | loaders: [{ 35 | test: /\.css$/, 36 | loader: extract( 37 | 'style-loader', 38 | 'css-loader?sourceMap' 39 | ), 40 | }, { 41 | test: /\.less$/, 42 | loader: extract( 43 | 'css?-url&-autoprefixer&sourceMap!less?sourceMap' 44 | ), 45 | }], 46 | }, 47 | plugins: [ 48 | new ExtractTextPlugin('styles.css'), 49 | new CSSSplitWebpackPlugin(options), 50 | ...(plugins || []), 51 | ], 52 | ...extra, 53 | }; 54 | }; 55 | 56 | const webpack = (options, inst, extra) => { 57 | const configuration = config(options, inst, extra); 58 | const compiler = _webpack(configuration); 59 | compiler.outputFileSystem = new MemoryFileSystem(); 60 | return new Promise((resolve) => { 61 | compiler.run((err, _stats) => { 62 | expect(err).to.be.null; 63 | const stats = _stats.toJson(); 64 | const files = {}; 65 | stats.assets.forEach((asset) => { 66 | files[asset.name] = compiler.outputFileSystem.readFileSync( 67 | path.join(configuration.output.path, asset.name) 68 | ); 69 | }); 70 | resolve({stats, files}); 71 | }); 72 | }); 73 | }; 74 | 75 | describe('CSSSplitWebpackPlugin', () => { 76 | it('should split files when needed', () => 77 | webpack({size: 3, imports: true}).then(({stats}) => { 78 | expect(stats).to.not.be.null; 79 | expect(stats.assetsByChunkName).to.have.property('main') 80 | .to.contain('styles-2.css'); 81 | }) 82 | ); 83 | it('should ignore files that do not need splitting', () => 84 | webpack({size: 10, imports: true}).then(({stats}) => { 85 | expect(stats).to.not.be.null; 86 | expect(stats.assetsByChunkName).to.have.property('main') 87 | .to.not.contain('styles-2.css'); 88 | }) 89 | ); 90 | it('should generate an import file when requested', () => 91 | webpack({size: 3, imports: true}).then(({stats}) => { 92 | expect(stats).to.not.be.null; 93 | expect(stats.assetsByChunkName).to.have.property('main') 94 | .to.contain('styles.css'); 95 | }) 96 | ); 97 | it('should remove the original asset when splitting', () => 98 | webpack({size: 3, imports: false}).then(({stats}) => { 99 | expect(stats).to.not.be.null; 100 | expect(stats.assetsByChunkName).to.have.property('main') 101 | .to.not.contain('styles.css'); 102 | }) 103 | ); 104 | it('should allow customization of import name', () => 105 | webpack({size: 3, imports: 'potato.css'}).then(({stats}) => { 106 | expect(stats).to.not.be.null; 107 | expect(stats.assetsByChunkName).to.have.property('main') 108 | .to.contain('potato.css'); 109 | }) 110 | ); 111 | it('should allow preservation of the original unsplit file', () => 112 | webpack({size: 3, imports: false, preserve: true}).then(({stats}) => { 113 | expect(stats).to.not.be.null; 114 | expect(stats.assetsByChunkName).to.have.property('main') 115 | .to.contain('styles.css'); 116 | }) 117 | ); 118 | it('should give sensible names by default', () => { 119 | return webpack({size: 3, imports: true, preserve: true}).then(({stats}) => { 120 | expect(stats).to.not.be.null; 121 | expect(stats.assetsByChunkName).to.have.property('main') 122 | .to.contain('styles-split.css'); 123 | }); 124 | }); 125 | it('should handle source maps properly', () => 126 | webpack({size: 3}, less).then(({files}) => { 127 | expect(files).to.have.property('styles-1.css.map'); 128 | const map = JSON.parse(files['styles-1.css.map'].toString('utf8')); 129 | expect(map).to.have.property('version', 3); 130 | expect(map).to.have.property('sources') 131 | .to.have.property(0) 132 | .to.match(/index.less$/); 133 | }) 134 | ); 135 | it('should handle cases when there are no source maps', () => 136 | webpack({size: 3}, less, {devtool: null}).then(({files}) => { 137 | expect(files).to.not.have.property('styles-1.css.map'); 138 | }) 139 | ); 140 | it('should fail with bad imports', () => { 141 | expect(() => 142 | new CSSSplitWebpackPlugin({imports: () => {}}) 143 | ).to.throw(TypeError); 144 | }); 145 | describe('deferred emit', () => { 146 | it('should split css files when necessary', () => 147 | webpack({size: 3, defer: true}).then(({stats, files}) => { 148 | expect(stats.assetsByChunkName) 149 | .to.have.property('main') 150 | .to.contain('styles-1.css') 151 | .to.contain('styles-2.css'); 152 | expect(files).to.have.property('styles-1.css'); 153 | expect(files).to.have.property('styles-2.css'); 154 | expect(files).to.have.property('styles.css.map'); 155 | }) 156 | ); 157 | it('should ignore files that do not need splitting', () => 158 | webpack({size: 10, defer: true}).then(({stats, files}) => { 159 | expect(stats.assetsByChunkName) 160 | .to.have.property('main') 161 | .to.contain('styles.css') 162 | .to.not.contain('styles-1.css') 163 | .to.not.contain('styles-2.css'); 164 | expect(files).to.have.property('styles.css'); 165 | expect(files).to.not.have.property('styles-1.css'); 166 | expect(files).to.not.have.property('styles-2.css'); 167 | }) 168 | ); 169 | it('should handle cases when there are no source maps', () => 170 | webpack({ 171 | size: 3, 172 | defer: true, 173 | }, basic, { 174 | devtool: null, 175 | plugins: [ 176 | new OptimizeCssPlugin(), 177 | ], 178 | }).then(({stats, files}) => { 179 | expect(files).to.not.have.property('styles-1.css.map'); 180 | expect(stats.assetsByChunkName) 181 | .to.have.property('main') 182 | .to.contain('styles-1.css'); 183 | }) 184 | ); 185 | }); 186 | }); 187 | --------------------------------------------------------------------------------