You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

305 lines
9.2 KiB

/*
* MIT License http://opensource.org/licenses/MIT
* Author: Ben Holloway @bholloway
*/
'use strict';
var os = require('os'),
path = require('path'),
fs = require('fs'),
util = require('util'),
loaderUtils = require('loader-utils'),
SourceMapConsumer = require('source-map').SourceMapConsumer;
var adjustSourceMap = require('adjust-sourcemap-loader/lib/process');
var valueProcessor = require('./lib/value-processor'),
joinFn = require('./lib/join-function'),
logToTestHarness = require('./lib/log-to-test-harness');
const DEPRECATED_OPTIONS = {
engine: [
'DEP_RESOLVE_URL_LOADER_OPTION_ENGINE',
'the "engine" option is deprecated, "postcss" engine is the default, using "rework" engine is not advised'
],
keepQuery: [
'DEP_RESOLVE_URL_LOADER_OPTION_KEEP_QUERY',
'"keepQuery" option has been removed, the query and/or hash are now always retained'
],
absolute: [
'DEP_RESOLVE_URL_LOADER_OPTION_ABSOLUTE',
'"absolute" option has been removed, consider the "join" option if absolute paths must be processed'
],
attempts: [
'DEP_RESOLVE_URL_LOADER_OPTION_ATTEMPTS',
'"attempts" option has been removed, consider the "join" option if search is needed'
],
includeRoot: [
'DEP_RESOLVE_URL_LOADER_OPTION_INCLUDE_ROOT',
'"includeRoot" option has been removed, consider the "join" option if search is needed'
],
fail: [
'DEP_RESOLVE_URL_LOADER_OPTION_FAIL',
'"fail" option has been removed'
]
};
/**
* 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'
);
}
// infer webpack version from new loader features
var isWebpackGte5 = 'getOptions' in loader && typeof loader.getOptions === 'function';
// 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 rawOptions = loaderUtils.getOptions(loader),
options = Object.assign(
{
sourceMap: loader.sourceMap,
engine : 'postcss',
silent : false,
removeCR : os.EOL.includes('\r'),
root : false,
debug : false,
join : joinFn.defaultJoin
},
rawOptions
);
// maybe log options for the test harness
if (process.env.RESOLVE_URL_LOADER_TEST_HARNESS) {
logToTestHarness(
process[process.env.RESOLVE_URL_LOADER_TEST_HARNESS],
options
);
}
// deprecated options
var deprecatedItems = Object.entries(DEPRECATED_OPTIONS).filter(([key]) => key in rawOptions);
if (deprecatedItems.length) {
deprecatedItems.forEach(([, value]) => handleAsDeprecated(...value));
}
// 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 (options, loader)'
);
}
// validate the result of calling the join option
var joinProper = options.join(options, loader);
if (typeof joinProper !== 'function') {
return handleAsError(
'loader misconfiguration',
'"join" option must itself return a Function when it is called'
);
} else if (joinProper.length !== 1) {
return handleAsError(
'loader misconfiguration',
'"join" Function must create a function that takes exactly 1 arguments (item)'
);
}
// 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);
} else {
handleAsWarning(
'webpack misconfiguration',
'webpack or the upstream loader did not supply a source-map'
);
}
// 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'
);
}
// allow engine to throw at initialisation
var engine;
try {
engine = require(enginePath);
} catch (error) {
return handleAsError(
'error initialising',
error
);
}
// process async
var callback = loader.async();
Promise
.resolve(engine(loader.resourcePath, content, {
outputSourceMap : !!options.sourceMap,
absSourceMap : absSourceMap,
sourceMapConsumer : sourceMapConsumer,
removeCR : options.removeCR,
transformDeclaration: valueProcessor({
join : joinProper,
root : options.root,
directory: path.dirname(loader.resourcePath)
})
}))
.catch(onFailure)
.then(onSuccess);
function onFailure(error) {
callback(encodeError('error processing CSS', error));
}
function onSuccess(result) {
if (result) {
// complete with source-map
// webpack4 and earlier: source-map sources are relative to the file being processed
// webpack5: source-map sources are relative to the project root but without a leading slash
if (options.sourceMap) {
var finalMap = adjustSourceMap(loader, {
format: isWebpackGte5 ? 'projectRelative' : 'sourceRelative'
}, result.map);
callback(null, result.content, finalMap);
}
// complete without source-map
else {
callback(null, result.content);
}
}
}
/**
* Trigger a node deprecation message for the given exception and return the original content.
* @param {string} code Deprecation code
* @param {string} message Deprecation message
* @returns {string} The original CSS content
*/
function handleAsDeprecated(code, message) {
if (!options.silent) {
util.deprecate(() => undefined, message, code)();
}
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 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(
[
'resolve-url-loader',
': ',
[label]
.concat(
(typeof exception === 'string') && exception ||
Array.isArray(exception) && exception ||
(exception instanceof Error) && [exception.message, exception.stack.split('\n')[1].trim()] ||
[]
)
.filter(Boolean)
.join('\n ')
].join('')
);
}
}
module.exports = Object.assign(resolveUrlLoader, joinFn);