1 | import postcss from 'postcss';
|
2 | import chunk from './chunk';
|
3 | import SourceMapSource from 'webpack/lib/SourceMapSource';
|
4 | import RawSource from 'webpack/lib/RawSource';
|
5 | import {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 | */
|
12 | const 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 | */
|
19 | const 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 | */
|
28 | const 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 | */
|
45 | const 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 | */
|
67 | export 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 | }
|