Skip to content

Commit 8d5ff5a

Browse files
committed
feat: Allow css assets to be processed after concatenation.
1 parent 98272a9 commit 8d5ff5a

File tree

2 files changed

+230
-99
lines changed

2 files changed

+230
-99
lines changed

packages/webpack-plugin/src/CssAssets.ts

+228-90
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,93 @@ import { Compiler as WebpackCompiler } from "webpack";
22
import * as path from "path";
33
import * as async from "async";
44
import * as fs from "fs";
5+
import * as postcss from "postcss";
56
import { Source, RawSource, SourceMapSource, ConcatSource } from "webpack-sources";
67
import { RawSourceMap } from "source-map";
78
import * as convertSourceMap from "convert-source-map";
89
import * as debugGenerator from 'debug';
910

1011
const debug = debugGenerator("css-blocks:webpack:assets");
1112

13+
export type PostcssProcessor =
14+
Array<postcss.Plugin<any>>
15+
| ((assetPath: string) => Array<postcss.Plugin<any>>
16+
| Promise<Array<postcss.Plugin<any>>>);
17+
18+
export type GenericProcessor =
19+
(source: Source, assetPath: string) => Source | Promise<Source>;
20+
21+
export interface PostcssProcessorOption {
22+
postcss: PostcssProcessor;
23+
}
24+
25+
export interface GenericProcessorOption {
26+
processor: GenericProcessor;
27+
}
28+
29+
export type PostProcessorOption = PostcssProcessorOption | GenericProcessorOption | (PostcssProcessorOption & GenericProcessorOption);
30+
31+
function isPostcssProcessor(processor: PostProcessorOption): processor is PostcssProcessorOption {
32+
return !!(<PostcssProcessorOption>processor).postcss;
33+
}
34+
35+
function isGenericProcessor(processor: PostProcessorOption): processor is GenericProcessorOption {
36+
return !!(<GenericProcessorOption>processor).processor;
37+
}
38+
39+
export interface CssSourceOptions {
40+
/**
41+
* The name of the chunk to which the asset should belong.
42+
* If omitted, the asset won't belong to a any chunk. */
43+
chunk: string | undefined;
44+
45+
/** the source path to the css asset. */
46+
source: string | string[];
47+
48+
/**
49+
* Post-process the concatenated file with the specified postcss plugins.
50+
*/
51+
// TODO: enable
52+
// postProcess?: PostProcessorOption;
53+
}
54+
export interface ConcatenationOptions {
55+
/**
56+
* A list of assets to be concatenated.
57+
*/
58+
sources: Array<string>;
59+
60+
/**
61+
* When true, the files that are concatenated are left in the build.
62+
* Defaults to false.
63+
*/
64+
preserveSourceFiles?: boolean;
65+
66+
/**
67+
* Post-process the concatenated file with the specified postcss plugins.
68+
*
69+
* If postcss plugins are provided in conjunction with a generic processor
70+
* the postcss plugins will be ran first.
71+
*/
72+
postProcess?: PostProcessorOption;
73+
}
74+
1275
/**
1376
* Options for managing CSS assets without javascript imports.
1477
*/
1578
export interface CssAssetsOptions {
1679
/** Maps css files from a source location to a webpack asset location. */
1780
cssFiles: {
18-
[assetPath: string]: string | {
19-
/** The name of the chunk to which the asset should belong. If omitted, the asset won't belong to a any chunk. */
20-
chunk: string | undefined;
21-
/** the source path to the css asset. */
22-
source: string | string[];
23-
};
81+
[assetPath: string]: string | CssSourceOptions;
2482
};
2583
/**
2684
* Maps several webpack assets to a new concatenated asset and manages their
2785
* sourcemaps. The concatenated asset will belong to all the chunks to which
2886
* the assets belonged.
2987
*/
3088
concat: {
31-
[concatAssetPath: string]: string[];
89+
[concatAssetPath: string]: string[] | ConcatenationOptions;
3290
};
91+
3392
/**
3493
* When true, any source maps related to the assets are written out as
3594
* additional files or inline depending on the value of `inlineSourceMaps`.
@@ -42,66 +101,9 @@ export interface CssAssetsOptions {
42101
inlineSourceMaps: boolean; // defaults to false
43102
}
44103

45-
function assetAsSource(contents: string, filename: string): Source {
46-
let sourcemap: convertSourceMap.SourceMapConverter | undefined;
47-
if (/sourceMappingURL/.test(contents)) {
48-
sourcemap = convertSourceMap.fromSource(contents) ||
49-
convertSourceMap.fromMapFileComment(contents, path.dirname(filename));
50-
}
51-
if (sourcemap) {
52-
let sm: RawSourceMap = sourcemap.toObject();
53-
contents = convertSourceMap.removeComments(contents);
54-
contents = convertSourceMap.removeMapFileComments(contents);
55-
return new SourceMapSource(contents, filename, sm);
56-
} else {
57-
return new RawSource(contents);
58-
}
59-
}
60-
function assetFilesAsSource(filenames: string[], callback: (err: Error | undefined, source?: ConcatSource) => void) {
61-
let assetSource = new ConcatSource();
62-
let assetFiles = filenames.slice();
63-
let eachAssetFile = (err?: Error) => {
64-
if (err) {
65-
callback(err);
66-
} else {
67-
const nextAssetFile = assetFiles.shift();
68-
if (nextAssetFile) {
69-
processAsset(nextAssetFile, eachAssetFile);
70-
} else {
71-
callback(undefined, assetSource);
72-
}
73-
}
74-
};
75-
const firstAssetFile = assetFiles.shift();
76-
if (firstAssetFile) {
77-
processAsset(firstAssetFile, eachAssetFile);
78-
} else {
79-
callback(new Error("No asset files provided."));
80-
}
81-
function processAsset(assetPath: string, assetCallback: (err?: Error) => void) {
82-
fs.readFile(assetPath, "utf-8", (err, data) => {
83-
if (err) {
84-
assetCallback(err);
85-
} else {
86-
assetSource.add(assetAsSource(data, assetPath));
87-
assetCallback();
88-
}
89-
});
90-
}
91-
}
92-
93-
function assetFileAsSource(sourcePath: string, callback: (err: Error | undefined, source?: Source) => void) {
94-
fs.readFile(sourcePath, "utf-8", (err, contents) => {
95-
if (err) {
96-
callback(err);
97-
} else {
98-
try {
99-
callback(undefined, assetAsSource(contents, sourcePath));
100-
} catch (e) {
101-
callback(e);
102-
}
103-
}
104-
});
104+
interface SourceAndMap {
105+
source: string;
106+
map?: RawSourceMap;
105107
}
106108

107109
export class CssAssets {
@@ -165,13 +167,16 @@ export class CssAssets {
165167
debug("concatenating assets");
166168
if (!this.options.concat) return;
167169
let concatFiles = Object.keys(this.options.concat);
168-
concatFiles.forEach((concatFile) => {
170+
let postProcessResults = new Array<Promise<void>>();
171+
for (let concatFile of concatFiles) {
169172
let concatSource = new ConcatSource();
170-
let inputFiles = this.options.concat[concatFile];
173+
let concatenation = this.options.concat[concatFile];
174+
let inputFiles = Array.isArray(concatenation) ? concatenation : concatenation.sources;
175+
let concatenationOptions = Array.isArray(concatenation) ? {sources: concatenation} : concatenation;
171176
let missingFiles = inputFiles.filter(f => (!compilation.assets[f]));
172177
let chunks = new Set<any>();
173178
if (missingFiles.length === 0) {
174-
inputFiles.forEach(inputFile => {
179+
for (let inputFile of inputFiles) {
175180
let asset = compilation.assets[inputFile];
176181
concatSource.add(asset);
177182
let chunksWithInputAsset = compilation.chunks.filter((chunk: any) => (<Array<string>>chunk.files).indexOf(inputFile) >= 0);
@@ -180,16 +185,35 @@ export class CssAssets {
180185
let files: string[] = chunk.files;
181186
chunk.files = files.filter(file => file !== inputFile);
182187
});
183-
delete compilation.assets[inputFile];
184-
});
185-
compilation.assets[concatFile] = concatSource;
188+
if (!concatenationOptions.preserveSourceFiles) {
189+
delete compilation.assets[inputFile];
190+
}
191+
}
192+
if (concatenationOptions.postProcess) {
193+
postProcessResults.push(postProcess(concatenationOptions.postProcess, concatSource, concatFile).then(source => {
194+
compilation.assets[concatFile] = source;
195+
}));
196+
} else {
197+
compilation.assets[concatFile] = concatSource;
198+
}
199+
}
200+
for (let chunk of chunks) {
201+
let files: Array<string> = chunk.files;
202+
if (files.indexOf(concatFile) >= 0) continue;
203+
files.push(concatFile);
186204
}
187-
chunks.forEach(chunk => {
188-
chunk.files.push(concatFile);
205+
}
206+
if (postProcessResults.length > 0) {
207+
Promise.all(postProcessResults).then(() => {
208+
cb();
209+
}, error => {
210+
cb(error);
189211
});
190-
});
191-
cb();
212+
} else {
213+
cb();
214+
}
192215
});
216+
193217
// sourcemap output for css files
194218
// Emit all css files with sourcemaps when the `emitSourceMaps` option
195219
// is set to true (default). By default source maps are generated as a
@@ -205,18 +229,7 @@ export class CssAssets {
205229
let assetPaths = Object.keys(compilation.assets).filter(p => /\.css$/.test(p));
206230
assetPaths.forEach(assetPath => {
207231
let asset = compilation.assets[assetPath];
208-
let source, map;
209-
// sourceAndMap is supposedly more efficient when implemented.
210-
if (asset.sourceAndMap) {
211-
let sourceAndMap = asset.sourceAndMap();
212-
source = sourceAndMap.source;
213-
map = sourceAndMap.map;
214-
} else {
215-
source = asset.source();
216-
if (asset.map) {
217-
map = asset.map();
218-
}
219-
}
232+
let {source, map} = sourceAndMap(asset);
220233
if (map) {
221234
let comment;
222235
if (this.options.inlineSourceMaps) {
@@ -232,4 +245,129 @@ export class CssAssets {
232245
cb();
233246
});
234247
}
235-
}
248+
}
249+
250+
function assetAsSource(contents: string, filename: string): Source {
251+
let sourcemap: convertSourceMap.SourceMapConverter | undefined;
252+
if (/sourceMappingURL/.test(contents)) {
253+
sourcemap = convertSourceMap.fromSource(contents) ||
254+
convertSourceMap.fromMapFileComment(contents, path.dirname(filename));
255+
}
256+
if (sourcemap) {
257+
let sm: RawSourceMap = sourcemap.toObject();
258+
contents = convertSourceMap.removeComments(contents);
259+
contents = convertSourceMap.removeMapFileComments(contents);
260+
return new SourceMapSource(contents, filename, sm);
261+
} else {
262+
return new RawSource(contents);
263+
}
264+
}
265+
266+
function assetFilesAsSource(filenames: string[], callback: (err: Error | undefined, source?: ConcatSource) => void) {
267+
let assetSource = new ConcatSource();
268+
let assetFiles = filenames.slice();
269+
let eachAssetFile = (err?: Error) => {
270+
if (err) {
271+
callback(err);
272+
} else {
273+
const nextAssetFile = assetFiles.shift();
274+
if (nextAssetFile) {
275+
processAsset(nextAssetFile, eachAssetFile);
276+
} else {
277+
callback(undefined, assetSource);
278+
}
279+
}
280+
};
281+
const firstAssetFile = assetFiles.shift();
282+
if (firstAssetFile) {
283+
processAsset(firstAssetFile, eachAssetFile);
284+
} else {
285+
callback(new Error("No asset files provided."));
286+
}
287+
function processAsset(assetPath: string, assetCallback: (err?: Error) => void) {
288+
fs.readFile(assetPath, "utf-8", (err, data) => {
289+
if (err) {
290+
assetCallback(err);
291+
} else {
292+
assetSource.add(assetAsSource(data, assetPath));
293+
assetCallback();
294+
}
295+
});
296+
}
297+
}
298+
299+
function assetFileAsSource(sourcePath: string, callback: (err: Error | undefined, source?: Source) => void) {
300+
fs.readFile(sourcePath, "utf-8", (err, contents) => {
301+
if (err) {
302+
callback(err);
303+
} else {
304+
try {
305+
callback(undefined, assetAsSource(contents, sourcePath));
306+
} catch (e) {
307+
callback(e);
308+
}
309+
}
310+
});
311+
}
312+
313+
function sourceAndMap(asset: Source): SourceAndMap {
314+
// sourceAndMap is supposedly more efficient when implemented.
315+
if (asset.sourceAndMap) {
316+
return asset.sourceAndMap();
317+
} else {
318+
let source = asset.source();
319+
let map: RawSourceMap | undefined = undefined;
320+
if (asset.map) {
321+
map = asset.map();
322+
}
323+
return { source, map };
324+
}
325+
}
326+
327+
function makePostcssProcessor (
328+
plugins: PostcssProcessor
329+
): GenericProcessor {
330+
return (asset: Source, assetPath: string) => {
331+
let { source, map } = sourceAndMap(asset);
332+
let pluginsPromise: Promise<Array<postcss.Plugin<any>>>;
333+
if (typeof plugins === "function") {
334+
pluginsPromise = Promise.resolve(plugins(assetPath));
335+
} else {
336+
if (plugins.length > 0) {
337+
pluginsPromise = Promise.resolve(plugins);
338+
} else {
339+
return Promise.resolve(asset);
340+
}
341+
}
342+
return pluginsPromise.then(plugins => {
343+
let processor = postcss(plugins);
344+
let result = processor.process(source, {
345+
to: assetPath,
346+
map: { prev: map, inline: false, annotation: false }
347+
});
348+
349+
return result.then((result) => {
350+
return new SourceMapSource(result.css, assetPath, result.map.toJSON(), source, map);
351+
});
352+
});
353+
};
354+
}
355+
356+
function process(processor: GenericProcessor, asset: Source, assetPath: string) {
357+
return Promise.resolve(processor(asset, assetPath));
358+
}
359+
360+
function postProcess(option: PostProcessorOption, asset: Source, assetPath: string): Promise<Source> {
361+
let promise: Promise<Source>;
362+
if (isPostcssProcessor(option)) {
363+
promise = process(makePostcssProcessor(option.postcss), asset, assetPath);
364+
} else {
365+
promise = Promise.resolve(asset);
366+
}
367+
if (isGenericProcessor(option)) {
368+
promise = promise.then(asset => {
369+
return process(option.processor, asset, assetPath);
370+
});
371+
}
372+
return promise;
373+
}

0 commit comments

Comments
 (0)