/* * MIT License http://opensource.org/licenses/MIT * Author: Ben Holloway @bholloway */ 'use strict'; var path = require('path'), fs = require('fs'), loaderUtils = require('loader-utils'), camelcase = require('camelcase'), SourceMapConsumer = require('source-map').SourceMapConsumer; var adjustSourceMap = require('adjust-sourcemap-loader/lib/process'); var valueProcessor = require('./lib/value-processor'); var joinFn = require('./lib/join-function'); var logToTestHarness = require('./lib/log-to-test-harness'); 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; // a relative loader.context is a problem if (/^\./.test(loader.context)) { return handleAsError( 'webpack misconfiguration', 'loader.context is relative, expected absolute' ); } // 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 = Object.assign( { sourceMap: loader.sourceMap, engine : 'postcss', silent : false, absolute : false, keepQuery: false, removeCR : false, root : false, debug : false, join : joinFn.defaultJoin }, !!loader.options && loader.options[camelcase(PACKAGE_NAME)], loaderUtils.getOptions(loader) ); // maybe log options for the test harness logToTestHarness(options); // defunct options if ('attempts' in options) { handleAsWarning( 'loader misconfiguration', '"attempts" option is defunct (consider "join" option if search is needed)' ); } if ('includeRoot' in options) { handleAsWarning( 'loader misconfiguration', '"includeRoot" option is defunct (consider "join" option if search is needed)' ); } if ('fail' in options) { handleAsWarning( 'loader misconfiguration', '"fail" option is defunct' ); } // validate join option if (typeof options.join !== 'function') { return handleAsError( 'loader misconfiguration', '"join" option must be a Function' ); } else if (options.join.length !== 2) { return handleAsError( 'loader misconfiguration', '"join" Function must take exactly 2 arguments (filename and options hash)' ); } // validate root option if (typeof options.root === 'string') { var isValid = (options.root === '') || (path.isAbsolute(options.root) && fs.existsSync(options.root) && fs.statSync(options.root).isDirectory()); if (!isValid) { return handleAsError( 'loader misconfiguration', '"root" option must be an empty string or an absolute path to an existing directory' ); } } else if (options.root !== false) { handleAsWarning( 'loader misconfiguration', '"root" option must be string where used or false where unused' ); } // loader result is cacheable loader.cacheable(); // incoming source-map var sourceMapConsumer, absSourceMap; if (sourceMap) { // support non-standard string encoded source-map (per less-loader) if (typeof sourceMap === 'string') { try { sourceMap = JSON.parse(sourceMap); } catch (exception) { return handleAsError( 'source-map error', 'cannot parse source-map string (from less-loader?)' ); } } // leverage adjust-sourcemap-loader's codecs to avoid having to make any assumptions about the sourcemap // historically this is a regular source of breakage try { absSourceMap = adjustSourceMap(loader, {format: 'absolute'}, sourceMap); } catch (exception) { return handleAsError( 'source-map error', exception.message ); } // prepare the adjusted sass source-map for later look-ups sourceMapConsumer = new SourceMapConsumer(absSourceMap); } // choose a CSS engine var enginePath = /^\w+$/.test(options.engine) && path.join(__dirname, 'lib', 'engine', options.engine + '.js'); var isValidEngine = fs.existsSync(enginePath); if (!isValidEngine) { return handleAsError( 'loader misconfiguration', '"engine" option is not valid' ); } // process async var callback = loader.async(); Promise .resolve(require(enginePath)(loader.resourcePath, content, { outputSourceMap : !!options.sourceMap, transformDeclaration: valueProcessor(loader.resourcePath, options), absSourceMap : absSourceMap, sourceMapConsumer : sourceMapConsumer, removeCR : options.removeCR })) .catch(onFailure) .then(onSuccess); function onFailure(error) { callback(encodeError('CSS error', error)); } function onSuccess(reworked) { if (reworked) { // complete with source-map // source-map sources are relative to the file being processed if (options.sourceMap) { var finalMap = adjustSourceMap(loader, {format: 'sourceRelative'}, reworked.map); callback(null, reworked.content, finalMap); } // complete without source-map else { callback(null, reworked.content); } } } /** * Push a warning 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 handleAsWarning(label, exception) { if (!options.silent) { loader.emitWarning(encodeError(label, exception)); } return content; } /** * Push a warning 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 handleAsError(label, exception) { loader.emitError(encodeError(label, exception)); return content; } function encodeError(label, exception) { return new Error( [ PACKAGE_NAME, ': ', [label] .concat( (typeof exception === 'string') && exception || (exception instanceof Error) && [exception.message, exception.stack.split('\n')[1].trim()] || [] ) .filter(Boolean) .join('\n ') ].join('') ); } } module.exports = Object.assign(resolveUrlLoader, joinFn);