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