|
|
- /*
- * MIT License http://opensource.org/licenses/MIT
- * Author: Ben Holloway @bholloway
- */
- 'use strict';
-
- var path = require('path'),
- fs = require('fs'),
- loaderUtils = require('loader-utils'),
- rework = require('rework'),
- visit = require('rework-visit'),
- convert = require('convert-source-map'),
- camelcase = require('camelcase'),
- defaults = require('lodash.defaults'),
- SourceMapConsumer = require('source-map').SourceMapConsumer;
-
- var findFile = require('./lib/find-file'),
- absoluteToRelative = require('./lib/sources-absolute-to-relative'),
- adjustSourceMap = require('adjust-sourcemap-loader/lib/process');
-
- var PACKAGE_NAME = require('./package.json').name;
-
- /**
- * A webpack loader that resolves absolute url() paths relative to their original source file.
- * Requires source-maps to do any meaningful work.
- * @param {string} content Css content
- * @param {object} sourceMap The source-map
- * @returns {string|String}
- */
- function resolveUrlLoader(content, sourceMap) {
- /* jshint validthis:true */
-
- // details of the file being processed
- var loader = this,
- filePath = path.dirname(loader.resourcePath);
-
- // webpack 1: prefer loader query, else options object
- // webpack 2: prefer loader options
- // webpack 3: deprecate loader.options object
- // webpack 4: loader.options no longer defined
- var options = defaults(
- loaderUtils.getOptions(loader),
- loader.options && loader.options[camelcase(PACKAGE_NAME)],
- {
- absolute : false,
- sourceMap : loader.sourceMap,
- fail : false,
- silent : false,
- keepQuery : false,
- attempts : 0,
- debug : false,
- root : null,
- includeRoot: false
- }
- );
-
- // validate root directory
- var resolvedRoot = (typeof options.root === 'string') && path.resolve(options.root) || undefined,
- isValidRoot = resolvedRoot && fs.existsSync(resolvedRoot);
- if (options.root && !isValidRoot) {
- return handleException('"root" option does not resolve to a valid path');
- }
-
- // loader result is cacheable
- loader.cacheable();
-
- // incoming source-map
- var sourceMapConsumer, contentWithMap, sourceRoot;
- if (sourceMap) {
-
- // support non-standard string encoded source-map (per less-loader)
- if (typeof sourceMap === 'string') {
- try {
- sourceMap = JSON.parse(sourceMap);
- }
- catch (exception) {
- return handleException('source-map error', 'cannot parse source-map string (from less-loader?)');
- }
- }
-
- // Note the current sourceRoot before it is removed
- // later when we go back to relative paths, we need to add it again
- sourceRoot = sourceMap.sourceRoot;
-
- // leverage adjust-sourcemap-loader's codecs to avoid having to make any assumptions about the sourcemap
- // historically this is a regular source of breakage
- var absSourceMap;
- try {
- absSourceMap = adjustSourceMap(this, {format: 'absolute'}, sourceMap);
- }
- catch (exception) {
- return handleException('source-map error', exception.message);
- }
-
- // prepare the adjusted sass source-map for later look-ups
- sourceMapConsumer = new SourceMapConsumer(absSourceMap);
-
- // embed source-map in css for rework-css to use
- contentWithMap = content + convert.fromObject(absSourceMap).toComment({multiline: true});
- }
- // absent source map
- else {
- contentWithMap = content;
- }
-
- // process
- // rework-css will throw on css syntax errors
- var reworked;
- try {
- reworked = rework(contentWithMap, {source: loader.resourcePath})
- .use(reworkPlugin)
- .toString({
- sourcemap : options.sourceMap,
- sourcemapAsObject: options.sourceMap
- });
- }
- // fail gracefully
- catch (exception) {
- return handleException('CSS error', exception);
- }
-
- // complete with source-map
- if (options.sourceMap) {
-
- // source-map sources seem to be relative to the file being processed
- absoluteToRelative(reworked.map.sources, path.resolve(filePath, sourceRoot || '.'));
-
- // Set source root again
- reworked.map.sourceRoot = sourceRoot;
-
- // need to use callback when there are multiple arguments
- loader.callback(null, reworked.code, reworked.map);
- }
- // complete without source-map
- else {
- return reworked;
- }
-
- /**
- * Push an error for the given exception and return the original content.
- * @param {string} label Summary of the error
- * @param {string|Error} [exception] Optional extended error details
- * @returns {string} The original CSS content
- */
- function handleException(label, exception) {
- var rest = (typeof exception === 'string') ? [exception] :
- (exception instanceof Error) ? [exception.message, exception.stack.split('\n')[1].trim()] :
- [];
- var message = ' resolve-url-loader cannot operate: ' + [label].concat(rest).filter(Boolean).join('\n ');
- if (options.fail) {
- loader.emitError(message);
- }
- else if (!options.silent) {
- loader.emitWarning(message);
- }
- return content;
- }
-
- /**
- * Plugin for css rework that follows SASS transpilation
- * @param {object} stylesheet AST for the CSS output from SASS
- */
- function reworkPlugin(stylesheet) {
- var URL_STATEMENT_REGEX = /(url\s*\()\s*(?:(['"])((?:(?!\2).)*)(\2)|([^'"](?:(?!\)).)*[^'"]))\s*(\))/g;
-
- // visit each node (selector) in the stylesheet recursively using the official utility method
- // each node may have multiple declarations
- visit(stylesheet, function visitor(declarations) {
- if (declarations) {
- declarations
- .forEach(eachDeclaration);
- }
- });
-
- /**
- * Process a declaration from the syntax tree.
- * @param declaration
- */
- function eachDeclaration(declaration) {
- var isValid = declaration.value && (declaration.value.indexOf('url') >= 0),
- directory;
- if (isValid) {
-
- // reverse the original source-map to find the original sass file
- var startPosApparent = declaration.position.start,
- startPosOriginal = sourceMapConsumer && sourceMapConsumer.originalPositionFor(startPosApparent);
-
- // we require a valid directory for the specified file
- directory = startPosOriginal && startPosOriginal.source && path.dirname(startPosOriginal.source);
- if (directory) {
-
- // allow multiple url() values in the declaration
- // split by url statements and process the content
- // additional capture groups are needed to match quotations correctly
- // escaped quotations are not considered
- declaration.value = declaration.value
- .split(URL_STATEMENT_REGEX)
- .map(eachSplitOrGroup)
- .join('');
- }
- // source-map present but invalid entry
- else if (sourceMapConsumer) {
- throw new Error('source-map information is not available at url() declaration');
- }
- }
-
- /**
- * Encode the content portion of <code>url()</code> statements.
- * There are 4 capture groups in the split making every 5th unmatched.
- * @param {string} token A single split item
- * @param i The index of the item in the split
- * @returns {string} Every 3 or 5 items is an encoded url everything else is as is
- */
- function eachSplitOrGroup(token, i) {
- var BACKSLASH_REGEX = /\\/g;
-
- // we can get groups as undefined under certain match circumstances
- var initialised = token || '';
-
- // the content of the url() statement is either in group 3 or group 5
- var mod = i % 7;
- if ((mod === 3) || (mod === 5)) {
-
- // split into uri and query/hash and then find the absolute path to the uri
- var split = initialised.split(/([?#])/g),
- uri = split[0],
- absolute = uri && findFile(options).absolute(directory, uri, resolvedRoot),
- query = options.keepQuery ? split.slice(1).join('') : '';
-
- // use the absolute path (or default to initialised)
- if (options.absolute) {
- return absolute && absolute.replace(BACKSLASH_REGEX, '/').concat(query) || initialised;
- }
- // module relative path (or default to initialised)
- else {
- var relative = absolute && path.relative(filePath, absolute),
- rootRelative = relative && loaderUtils.urlToRequest(relative, '~');
- return (rootRelative) ? rootRelative.replace(BACKSLASH_REGEX, '/').concat(query) : initialised;
- }
- }
- // everything else, including parentheses and quotation (where present) and media statements
- else {
- return initialised;
- }
- }
- }
- }
- }
-
- module.exports = resolveUrlLoader;
|