├── .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 | 
6 | 
7 | 
8 | 
9 | 
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 |
--------------------------------------------------------------------------------