|
|
/* * 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);
|