237 lines
6.8 KiB
JavaScript
237 lines
6.8 KiB
JavaScript
/*
|
|
* 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);
|