123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292 |
- /*
- * MIT License http://opensource.org/licenses/MIT
- * Author: Ben Holloway @bholloway
- */
- 'use strict';
- const os = require('os');
- const path = require('path');
- const fs = require('fs');
- const util = require('util');
- const loaderUtils = require('loader-utils');
- const SourceMapConsumer = require('source-map').SourceMapConsumer;
- const adjustSourceMap = require('adjust-sourcemap-loader/lib/process');
- const valueProcessor = require('./lib/value-processor');
- const joinFn = require('./lib/join-function');
- const logToTestHarness = require('./lib/log-to-test-harness');
- const DEPRECATED_OPTIONS = {
- engine: [
- 'DEP_RESOLVE_URL_LOADER_OPTION_ENGINE',
- '"engine" option has been removed, postcss is the only parser used'
- ],
- 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
- const 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
- const isWebpackGte5 = 'getOptions' in loader && typeof loader.getOptions === 'function';
- // use loader.getOptions where available otherwise use loaderUtils
- const rawOptions = isWebpackGte5 ? loader.getOptions() : loaderUtils.getOptions(loader);
- const options = Object.assign(
- {
- sourceMap: loader.sourceMap,
- 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
- const 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
- const 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') {
- const 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
- let absSourceMap = null;
- let sourceMapConsumer = null;
- 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'
- );
- }
- // allow engine to throw at initialisation
- let engine = null;
- try {
- engine = require('./lib/engine/postcss');
- } catch (error) {
- return handleAsError(
- 'error initialising',
- error
- );
- }
- // process async
- const 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) {
- const 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);
|