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.

250 lines
8.7 KiB

4 years ago
  1. /*
  2. * MIT License http://opensource.org/licenses/MIT
  3. * Author: Ben Holloway @bholloway
  4. */
  5. 'use strict';
  6. var path = require('path'),
  7. fs = require('fs'),
  8. loaderUtils = require('loader-utils'),
  9. rework = require('rework'),
  10. visit = require('rework-visit'),
  11. convert = require('convert-source-map'),
  12. camelcase = require('camelcase'),
  13. defaults = require('lodash.defaults'),
  14. SourceMapConsumer = require('source-map').SourceMapConsumer;
  15. var findFile = require('./lib/find-file'),
  16. absoluteToRelative = require('./lib/sources-absolute-to-relative'),
  17. adjustSourceMap = require('adjust-sourcemap-loader/lib/process');
  18. var PACKAGE_NAME = require('./package.json').name;
  19. /**
  20. * A webpack loader that resolves absolute url() paths relative to their original source file.
  21. * Requires source-maps to do any meaningful work.
  22. * @param {string} content Css content
  23. * @param {object} sourceMap The source-map
  24. * @returns {string|String}
  25. */
  26. function resolveUrlLoader(content, sourceMap) {
  27. /* jshint validthis:true */
  28. // details of the file being processed
  29. var loader = this,
  30. filePath = path.dirname(loader.resourcePath);
  31. // webpack 1: prefer loader query, else options object
  32. // webpack 2: prefer loader options
  33. // webpack 3: deprecate loader.options object
  34. // webpack 4: loader.options no longer defined
  35. var options = defaults(
  36. loaderUtils.getOptions(loader),
  37. loader.options && loader.options[camelcase(PACKAGE_NAME)],
  38. {
  39. absolute : false,
  40. sourceMap : loader.sourceMap,
  41. fail : false,
  42. silent : false,
  43. keepQuery : false,
  44. attempts : 0,
  45. debug : false,
  46. root : null,
  47. includeRoot: false
  48. }
  49. );
  50. // validate root directory
  51. var resolvedRoot = (typeof options.root === 'string') && path.resolve(options.root) || undefined,
  52. isValidRoot = resolvedRoot && fs.existsSync(resolvedRoot);
  53. if (options.root && !isValidRoot) {
  54. return handleException('"root" option does not resolve to a valid path');
  55. }
  56. // loader result is cacheable
  57. loader.cacheable();
  58. // incoming source-map
  59. var sourceMapConsumer, contentWithMap, sourceRoot;
  60. if (sourceMap) {
  61. // support non-standard string encoded source-map (per less-loader)
  62. if (typeof sourceMap === 'string') {
  63. try {
  64. sourceMap = JSON.parse(sourceMap);
  65. }
  66. catch (exception) {
  67. return handleException('source-map error', 'cannot parse source-map string (from less-loader?)');
  68. }
  69. }
  70. // Note the current sourceRoot before it is removed
  71. // later when we go back to relative paths, we need to add it again
  72. sourceRoot = sourceMap.sourceRoot;
  73. // leverage adjust-sourcemap-loader's codecs to avoid having to make any assumptions about the sourcemap
  74. // historically this is a regular source of breakage
  75. var absSourceMap;
  76. try {
  77. absSourceMap = adjustSourceMap(this, {format: 'absolute'}, sourceMap);
  78. }
  79. catch (exception) {
  80. return handleException('source-map error', exception.message);
  81. }
  82. // prepare the adjusted sass source-map for later look-ups
  83. sourceMapConsumer = new SourceMapConsumer(absSourceMap);
  84. // embed source-map in css for rework-css to use
  85. contentWithMap = content + convert.fromObject(absSourceMap).toComment({multiline: true});
  86. }
  87. // absent source map
  88. else {
  89. contentWithMap = content;
  90. }
  91. // process
  92. // rework-css will throw on css syntax errors
  93. var reworked;
  94. try {
  95. reworked = rework(contentWithMap, {source: loader.resourcePath})
  96. .use(reworkPlugin)
  97. .toString({
  98. sourcemap : options.sourceMap,
  99. sourcemapAsObject: options.sourceMap
  100. });
  101. }
  102. // fail gracefully
  103. catch (exception) {
  104. return handleException('CSS error', exception);
  105. }
  106. // complete with source-map
  107. if (options.sourceMap) {
  108. // source-map sources seem to be relative to the file being processed
  109. absoluteToRelative(reworked.map.sources, path.resolve(filePath, sourceRoot || '.'));
  110. // Set source root again
  111. reworked.map.sourceRoot = sourceRoot;
  112. // need to use callback when there are multiple arguments
  113. loader.callback(null, reworked.code, reworked.map);
  114. }
  115. // complete without source-map
  116. else {
  117. return reworked;
  118. }
  119. /**
  120. * Push an error for the given exception and return the original content.
  121. * @param {string} label Summary of the error
  122. * @param {string|Error} [exception] Optional extended error details
  123. * @returns {string} The original CSS content
  124. */
  125. function handleException(label, exception) {
  126. var rest = (typeof exception === 'string') ? [exception] :
  127. (exception instanceof Error) ? [exception.message, exception.stack.split('\n')[1].trim()] :
  128. [];
  129. var message = ' resolve-url-loader cannot operate: ' + [label].concat(rest).filter(Boolean).join('\n ');
  130. if (options.fail) {
  131. loader.emitError(message);
  132. }
  133. else if (!options.silent) {
  134. loader.emitWarning(message);
  135. }
  136. return content;
  137. }
  138. /**
  139. * Plugin for css rework that follows SASS transpilation
  140. * @param {object} stylesheet AST for the CSS output from SASS
  141. */
  142. function reworkPlugin(stylesheet) {
  143. var URL_STATEMENT_REGEX = /(url\s*\()\s*(?:(['"])((?:(?!\2).)*)(\2)|([^'"](?:(?!\)).)*[^'"]))\s*(\))/g;
  144. // visit each node (selector) in the stylesheet recursively using the official utility method
  145. // each node may have multiple declarations
  146. visit(stylesheet, function visitor(declarations) {
  147. if (declarations) {
  148. declarations
  149. .forEach(eachDeclaration);
  150. }
  151. });
  152. /**
  153. * Process a declaration from the syntax tree.
  154. * @param declaration
  155. */
  156. function eachDeclaration(declaration) {
  157. var isValid = declaration.value && (declaration.value.indexOf('url') >= 0),
  158. directory;
  159. if (isValid) {
  160. // reverse the original source-map to find the original sass file
  161. var startPosApparent = declaration.position.start,
  162. startPosOriginal = sourceMapConsumer && sourceMapConsumer.originalPositionFor(startPosApparent);
  163. // we require a valid directory for the specified file
  164. directory = startPosOriginal && startPosOriginal.source && path.dirname(startPosOriginal.source);
  165. if (directory) {
  166. // allow multiple url() values in the declaration
  167. // split by url statements and process the content
  168. // additional capture groups are needed to match quotations correctly
  169. // escaped quotations are not considered
  170. declaration.value = declaration.value
  171. .split(URL_STATEMENT_REGEX)
  172. .map(eachSplitOrGroup)
  173. .join('');
  174. }
  175. // source-map present but invalid entry
  176. else if (sourceMapConsumer) {
  177. throw new Error('source-map information is not available at url() declaration');
  178. }
  179. }
  180. /**
  181. * Encode the content portion of <code>url()</code> statements.
  182. * There are 4 capture groups in the split making every 5th unmatched.
  183. * @param {string} token A single split item
  184. * @param i The index of the item in the split
  185. * @returns {string} Every 3 or 5 items is an encoded url everything else is as is
  186. */
  187. function eachSplitOrGroup(token, i) {
  188. var BACKSLASH_REGEX = /\\/g;
  189. // we can get groups as undefined under certain match circumstances
  190. var initialised = token || '';
  191. // the content of the url() statement is either in group 3 or group 5
  192. var mod = i % 7;
  193. if ((mod === 3) || (mod === 5)) {
  194. // split into uri and query/hash and then find the absolute path to the uri
  195. var split = initialised.split(/([?#])/g),
  196. uri = split[0],
  197. absolute = uri && findFile(options).absolute(directory, uri, resolvedRoot),
  198. query = options.keepQuery ? split.slice(1).join('') : '';
  199. // use the absolute path (or default to initialised)
  200. if (options.absolute) {
  201. return absolute && absolute.replace(BACKSLASH_REGEX, '/').concat(query) || initialised;
  202. }
  203. // module relative path (or default to initialised)
  204. else {
  205. var relative = absolute && path.relative(filePath, absolute),
  206. rootRelative = relative && loaderUtils.urlToRequest(relative, '~');
  207. return (rootRelative) ? rootRelative.replace(BACKSLASH_REGEX, '/').concat(query) : initialised;
  208. }
  209. }
  210. // everything else, including parentheses and quotation (where present) and media statements
  211. else {
  212. return initialised;
  213. }
  214. }
  215. }
  216. }
  217. }
  218. module.exports = resolveUrlLoader;