UNPKG

6.27 kBJavaScriptView Raw
1import postcss from 'postcss';
2import chunk from './chunk';
3import SourceMapSource from 'webpack/lib/SourceMapSource';
4import RawSource from 'webpack/lib/RawSource';
5import {interpolateName} from 'loader-utils';
6
7/**
8 * Detect if a file should be considered for CSS splitting.
9 * @param {String} name Name of the file.
10 * @returns {Boolean} True if to consider the file, false otherwise.
11 */
12const isCSS = (name : string) : boolean => /\.css$/.test(name);
13
14/**
15 * Remove the trailing `/` from URLs.
16 * @param {String} str The url to strip the trailing slash from.
17 * @returns {String} The stripped url.
18 */
19const strip = (str : string) : string => str.replace(/\/$/, '');
20
21/**
22 * Create a function that generates names based on some input. This uses
23 * webpack's name interpolator under the hood, but since webpack's argument
24 * list is all funny this exists just to simplify things.
25 * @param {String} input Name to be interpolated.
26 * @returns {Function} Function to do the interpolating.
27 */
28const nameInterpolator = (input) => ({file, content, index}) => {
29 const res = interpolateName({
30 context: '/',
31 resourcePath: `/${file}`,
32 }, input, {
33 content,
34 }).replace(/\[part\]/g, index + 1);
35 return res;
36};
37
38/**
39 * Normalize the `imports` argument to a function.
40 * @param {Boolean|String} input The name of the imports file, or a boolean
41 * to use the default name.
42 * @param {Boolean} preserve True if the default name should not clash.
43 * @returns {Function} Name interpolator.
44 */
45const normalizeImports = (input, preserve) => {
46 switch (typeof input) {
47 case 'string':
48 return nameInterpolator(input);
49 case 'boolean':
50 if (input) {
51 if (preserve) {
52 return nameInterpolator('[name]-split.[ext]');
53 }
54 return ({file}) => file;
55 }
56 return () => false;
57 default:
58 throw new TypeError();
59 }
60};
61
62/**
63 * Webpack plugin to split CSS assets into multiple files. This is primarily
64 * used for dealing with IE <= 9 which cannot handle more than ~4000 rules
65 * in a single stylesheet.
66 */
67export default class CSSSplitWebpackPlugin {
68 /**
69 * Create new instance of CSSSplitWebpackPlugin.
70 * @param {Number} size Maximum number of rules for a single file.
71 * @param {Boolean|String} imports Truish to generate an additional import
72 * asset. When a boolean use the default name for the asset.
73 * @param {String} filename Control the generated split file name.
74 * @param {Boolean} preserve True to keep the original unsplit file.
75 */
76 constructor({
77 size = 4000,
78 imports = false,
79 filename = '[name]-[part].[ext]',
80 preserve,
81 }) {
82 this.options = {
83 size,
84 imports: normalizeImports(imports, preserve),
85 filename: nameInterpolator(filename),
86 preserve,
87 };
88 }
89
90 /**
91 * Generate the split chunks for a given CSS file.
92 * @param {String} key Name of the file.
93 * @param {Object} asset Valid webpack Source object.
94 * @returns {Promise} Promise generating array of new files.
95 */
96 file(key : string, asset : Object) {
97 // Use source-maps when possible.
98 const input = asset.sourceAndMap ? asset.sourceAndMap() : {
99 source: asset.source(),
100 };
101 const name = (i) => this.options.filename({
102 ...asset,
103 content: input.source,
104 file: key,
105 index: i,
106 });
107 return postcss([chunk({
108 ...this.options,
109 result: (i) => {
110 return {
111 to: name(i),
112 from: key,
113 map: {
114 inline: false,
115 prev: input.map,
116 },
117 };
118 },
119 })]).process(input.source, {
120 from: key,
121 }).then((result) => {
122 return Promise.resolve({
123 file: key,
124 chunks: result.chunks.map(({css, map}, i) => {
125 return new SourceMapSource(
126 css,
127 name(i),
128 map.toString()
129 );
130 }),
131 });
132 });
133 }
134
135 /**
136 * Run the plugin against a webpack compiler instance. Roughly it walks all
137 * the chunks searching for CSS files and when it finds one that needs to be
138 * split it does so and replaces the original file in the chunk with the split
139 * ones. If the `imports` option is specified the original file is replaced
140 * with an empty CSS file importing the split files, otherwise the original
141 * file is removed entirely.
142 * @param {Object} compiler Compiler instance
143 * @returns {void}
144 */
145 apply(compiler : Object) {
146 // Only run on `this-compilation` to avoid injecting the plugin into
147 // sub-compilers as happens when using the `extract-text-webpack-plugin`.
148 compiler.plugin('this-compilation', (compilation) => {
149 const assets = compilation.assets;
150 const publicPath = strip(compilation.options.output.publicPath || './');
151 compilation.plugin('optimize-chunk-assets', (chunks, done) => {
152 const promises = chunks.map((chunk) => {
153 const input = chunk.files.filter(isCSS);
154 const items = input.map((name) => this.file(name, assets[name]));
155 return Promise.all(items).then((entries) => {
156 entries.forEach((entry) => {
157 // Skip the splitting operation for files that result in no
158 // split occuring.
159 if (entry.chunks.length === 1) {
160 return;
161 }
162 // Inject the new files into the chunk.
163 entry.chunks.forEach((file) => {
164 assets[file._name] = file;
165 chunk.files.push(file._name);
166 });
167 const content = entry.chunks.map((file) => {
168 return `@import "${publicPath}/${file._name}";`;
169 }).join('\n');
170 const imports = this.options.imports({
171 ...entry,
172 content,
173 });
174 if (!this.options.preserve) {
175 chunk.files.splice(chunk.files.indexOf(entry.file), 1);
176 delete assets[entry.file];
177 }
178 if (imports) {
179 assets[imports] = new RawSource(content);
180 chunk.files.push(imports);
181 }
182 });
183 return Promise.resolve();
184 });
185 });
186 Promise.all(promises).then(() => {
187 done();
188 }, done);
189 });
190 });
191 }
192}