processCss.js 7.43 KB
/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Tobias Koppers @sokra
*/
var formatCodeFrame = require("babel-code-frame");
var Tokenizer = require("css-selector-tokenizer");
var postcss = require("postcss");
var loaderUtils = require("loader-utils");
var assign = require("object-assign");
var getLocalIdent = require("./getLocalIdent");

var icssUtils = require('icss-utils');
var localByDefault = require("postcss-modules-local-by-default");
var extractImports = require("postcss-modules-extract-imports");
var modulesScope = require("postcss-modules-scope");
var modulesValues = require("postcss-modules-values");
var valueParser = require('postcss-value-parser');

var parserPlugin = postcss.plugin("css-loader-parser", function(options) {
	return function(css) {
		var imports = {};
		var exports = {};
		var importItems = [];
		var urlItems = [];

		function replaceImportsInString(str) {
			if(options.import) {
				var tokens = valueParser(str);
				tokens.walk(function (node) {
					if (node.type !== 'word') {
						return;
					}
					var token = node.value;
					var importIndex = imports["$" + token];
					if(typeof importIndex === "number") {
						node.value = "___CSS_LOADER_IMPORT___" + importIndex + "___";
					}
				})
				return tokens.toString();
			}
			return str;
		}

		if(options.import) {
			css.walkAtRules(/^import$/i, function(rule) {
				var values = Tokenizer.parseValues(rule.params);
				var url = values.nodes[0].nodes[0];
				if(url && url.type === "url") {
					url = url.url;
				} else if(url && url.type === "string") {
					url = url.value;
				} else throw rule.error("Unexpected format " + rule.params);
				if (!url.replace(/\s/g, '').length) {
					return;
				}
				values.nodes[0].nodes.shift();
				var mediaQuery = Tokenizer.stringifyValues(values);

				if(loaderUtils.isUrlRequest(url, options.root)) {
					url = loaderUtils.urlToRequest(url, options.root);
				}

				importItems.push({
					url: url,
					mediaQuery: mediaQuery
				});
				rule.remove();
			});
		}

		var icss = icssUtils.extractICSS(css);
		exports = icss.icssExports;
		Object.keys(icss.icssImports).forEach(function(key) {
			var url = loaderUtils.parseString(key);
			Object.keys(icss.icssImports[key]).forEach(function(prop) {
				imports["$" + prop] = importItems.length;
				importItems.push({
					url: url,
					export: icss.icssImports[key][prop]
				});
			})
		});

		Object.keys(exports).forEach(function(exportName) {
			exports[exportName] = replaceImportsInString(exports[exportName]);
		});

		function isAlias(url) {
			// Handle alias starting by / and root disabled
			return url !== options.resolve(url)
		}

		function processNode(item) {
			switch (item.type) {
				case "value":
					item.nodes.forEach(processNode);
					break;
				case "nested-item":
					item.nodes.forEach(processNode);
					break;
				case "item":
					var importIndex = imports["$" + item.name];
					if (typeof importIndex === "number") {
						item.name = "___CSS_LOADER_IMPORT___" + importIndex + "___";
					}
					break;
				case "url":
					if (options.url && item.url.replace(/\s/g, '').length && !/^#/.test(item.url) && (isAlias(item.url) || loaderUtils.isUrlRequest(item.url, options.root))) {
						// Strip quotes, they will be re-added if the module needs them
						item.stringType = "";
						delete item.innerSpacingBefore;
						delete item.innerSpacingAfter;
						var url = item.url;
						item.url = "___CSS_LOADER_URL___" + urlItems.length + "___";
						urlItems.push({
							url: url
						});
					}
					break;
			}
		}

		css.walkDecls(function(decl) {
			var values = Tokenizer.parseValues(decl.value);
			values.nodes.forEach(function(value) {
				value.nodes.forEach(processNode);
			});
			decl.value = Tokenizer.stringifyValues(values);
		});
		css.walkAtRules(function(atrule) {
			if(typeof atrule.params === "string") {
				atrule.params = replaceImportsInString(atrule.params);
			}
		});

		options.importItems = importItems;
		options.urlItems = urlItems;
		options.exports = exports;
	};
});

module.exports = function processCss(inputSource, inputMap, options, callback) {
	var query = options.query;
	var root = query.root && query.root.length > 0 ? query.root.replace(/\/$/, "") : query.root;
	var context = query.context;
	var localIdentName = query.localIdentName || "[hash:base64]";
	var localIdentRegExp = query.localIdentRegExp;
	var forceMinimize = query.minimize;
	var minimize = typeof forceMinimize !== "undefined" ? !!forceMinimize : options.minimize;

	var customGetLocalIdent = query.getLocalIdent || getLocalIdent;

	var parserOptions = {
		root: root,
		mode: options.mode,
		url: query.url !== false,
		import: query.import !== false,
		resolve: options.resolve
	};

	var pipeline = postcss([
		modulesValues,
		localByDefault({
			mode: options.mode,
			rewriteUrl: function(global, url) {
				if(parserOptions.url){
                    url = url.trim();

					if(!url.replace(/\s/g, '').length || !loaderUtils.isUrlRequest(url, root)) {
						return url;
					}
					if(global) {
						return loaderUtils.urlToRequest(url, root);
					}
				}
				return url;
			}
		}),
		extractImports(),
		modulesScope({
			generateScopedName: function generateScopedName (exportName) {
				return customGetLocalIdent(options.loaderContext, localIdentName, exportName, {
					regExp: localIdentRegExp,
					hashPrefix: query.hashPrefix || "",
					context: context
				});
			}
		}),
		parserPlugin(parserOptions)
	]);

	if(minimize) {
		var cssnano = require("cssnano");
		var minimizeOptions = assign({}, query.minimize);
		["zindex", "normalizeUrl", "discardUnused", "mergeIdents", "reduceIdents", "autoprefixer"].forEach(function(name) {
			if(typeof minimizeOptions[name] === "undefined")
				minimizeOptions[name] = false;
		});
		pipeline.use(cssnano(minimizeOptions));
	}

	pipeline.process(inputSource, {
		// we need a prefix to avoid path rewriting of PostCSS
		from: "/css-loader!" + options.from,
		to: options.to,
		map: options.sourceMap ? {
			prev: inputMap,
			sourcesContent: true,
			inline: false,
			annotation: false
		} : null
	}).then(function(result) {
		callback(null, {
			source: result.css,
			map: result.map && result.map.toJSON(),
			exports: parserOptions.exports,
			importItems: parserOptions.importItems,
			importItemRegExpG: /___CSS_LOADER_IMPORT___([0-9]+)___/g,
			importItemRegExp: /___CSS_LOADER_IMPORT___([0-9]+)___/,
			urlItems: parserOptions.urlItems,
			urlItemRegExpG: /___CSS_LOADER_URL___([0-9]+)___/g,
			urlItemRegExp: /___CSS_LOADER_URL___([0-9]+)___/
		});
	}).catch(function(err) {
		if (err.name === 'CssSyntaxError') {
			var wrappedError = new CSSLoaderError(
				'Syntax Error',
				err.reason,
				err.line != null && err.column != null
					? {line: err.line, column: err.column}
					: null,
				err.input.source
			);
			callback(wrappedError);
		} else {
			callback(err);
		}
	});
};

function formatMessage(message, loc, source) {
	var formatted = message;
	if (loc) {
		formatted = formatted
			+ ' (' + loc.line + ':' + loc.column + ')';
	}
	if (loc && source) {
		formatted = formatted
			+ '\n\n' + formatCodeFrame(source, loc.line, loc.column) + '\n';
	}
	return formatted;
}

function CSSLoaderError(name, message, loc, source, error) {
	Error.call(this);
	Error.captureStackTrace(this, CSSLoaderError);
	this.name = name;
	this.error = error;
	this.message = formatMessage(message, loc, source);
	this.hideStack = true;
}

CSSLoaderError.prototype = Object.create(Error.prototype);
CSSLoaderError.prototype.constructor = CSSLoaderError;