index.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  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. camelcase = require('camelcase'),
  10. SourceMapConsumer = require('source-map').SourceMapConsumer;
  11. var adjustSourceMap = require('adjust-sourcemap-loader/lib/process');
  12. var valueProcessor = require('./lib/value-processor');
  13. var joinFn = require('./lib/join-function');
  14. var logToTestHarness = require('./lib/log-to-test-harness');
  15. var PACKAGE_NAME = require('./package.json').name;
  16. /**
  17. * A webpack loader that resolves absolute url() paths relative to their original source file.
  18. * Requires source-maps to do any meaningful work.
  19. * @param {string} content Css content
  20. * @param {object} sourceMap The source-map
  21. * @returns {string|String}
  22. */
  23. function resolveUrlLoader(content, sourceMap) {
  24. /* jshint validthis:true */
  25. // details of the file being processed
  26. var loader = this;
  27. // a relative loader.context is a problem
  28. if (/^\./.test(loader.context)) {
  29. return handleAsError(
  30. 'webpack misconfiguration',
  31. 'loader.context is relative, expected absolute'
  32. );
  33. }
  34. // webpack 1: prefer loader query, else options object
  35. // webpack 2: prefer loader options
  36. // webpack 3: deprecate loader.options object
  37. // webpack 4: loader.options no longer defined
  38. var options = Object.assign(
  39. {
  40. sourceMap: loader.sourceMap,
  41. engine : 'postcss',
  42. silent : false,
  43. absolute : false,
  44. keepQuery: false,
  45. removeCR : false,
  46. root : false,
  47. debug : false,
  48. join : joinFn.defaultJoin
  49. },
  50. !!loader.options && loader.options[camelcase(PACKAGE_NAME)],
  51. loaderUtils.getOptions(loader)
  52. );
  53. // maybe log options for the test harness
  54. logToTestHarness(options);
  55. // defunct options
  56. if ('attempts' in options) {
  57. handleAsWarning(
  58. 'loader misconfiguration',
  59. '"attempts" option is defunct (consider "join" option if search is needed)'
  60. );
  61. }
  62. if ('includeRoot' in options) {
  63. handleAsWarning(
  64. 'loader misconfiguration',
  65. '"includeRoot" option is defunct (consider "join" option if search is needed)'
  66. );
  67. }
  68. if ('fail' in options) {
  69. handleAsWarning(
  70. 'loader misconfiguration',
  71. '"fail" option is defunct'
  72. );
  73. }
  74. // validate join option
  75. if (typeof options.join !== 'function') {
  76. return handleAsError(
  77. 'loader misconfiguration',
  78. '"join" option must be a Function'
  79. );
  80. } else if (options.join.length !== 2) {
  81. return handleAsError(
  82. 'loader misconfiguration',
  83. '"join" Function must take exactly 2 arguments (filename and options hash)'
  84. );
  85. }
  86. // validate root option
  87. if (typeof options.root === 'string') {
  88. var isValid = (options.root === '') ||
  89. (path.isAbsolute(options.root) && fs.existsSync(options.root) && fs.statSync(options.root).isDirectory());
  90. if (!isValid) {
  91. return handleAsError(
  92. 'loader misconfiguration',
  93. '"root" option must be an empty string or an absolute path to an existing directory'
  94. );
  95. }
  96. } else if (options.root !== false) {
  97. handleAsWarning(
  98. 'loader misconfiguration',
  99. '"root" option must be string where used or false where unused'
  100. );
  101. }
  102. // loader result is cacheable
  103. loader.cacheable();
  104. // incoming source-map
  105. var sourceMapConsumer, absSourceMap;
  106. if (sourceMap) {
  107. // support non-standard string encoded source-map (per less-loader)
  108. if (typeof sourceMap === 'string') {
  109. try {
  110. sourceMap = JSON.parse(sourceMap);
  111. }
  112. catch (exception) {
  113. return handleAsError(
  114. 'source-map error',
  115. 'cannot parse source-map string (from less-loader?)'
  116. );
  117. }
  118. }
  119. // leverage adjust-sourcemap-loader's codecs to avoid having to make any assumptions about the sourcemap
  120. // historically this is a regular source of breakage
  121. try {
  122. absSourceMap = adjustSourceMap(loader, {format: 'absolute'}, sourceMap);
  123. }
  124. catch (exception) {
  125. return handleAsError(
  126. 'source-map error',
  127. exception.message
  128. );
  129. }
  130. // prepare the adjusted sass source-map for later look-ups
  131. sourceMapConsumer = new SourceMapConsumer(absSourceMap);
  132. }
  133. // choose a CSS engine
  134. var enginePath = /^\w+$/.test(options.engine) && path.join(__dirname, 'lib', 'engine', options.engine + '.js');
  135. var isValidEngine = fs.existsSync(enginePath);
  136. if (!isValidEngine) {
  137. return handleAsError(
  138. 'loader misconfiguration',
  139. '"engine" option is not valid'
  140. );
  141. }
  142. // process async
  143. var callback = loader.async();
  144. Promise
  145. .resolve(require(enginePath)(loader.resourcePath, content, {
  146. outputSourceMap : !!options.sourceMap,
  147. transformDeclaration: valueProcessor(loader.resourcePath, options),
  148. absSourceMap : absSourceMap,
  149. sourceMapConsumer : sourceMapConsumer,
  150. removeCR : options.removeCR
  151. }))
  152. .catch(onFailure)
  153. .then(onSuccess);
  154. function onFailure(error) {
  155. callback(encodeError('CSS error', error));
  156. }
  157. function onSuccess(reworked) {
  158. if (reworked) {
  159. // complete with source-map
  160. // source-map sources are relative to the file being processed
  161. if (options.sourceMap) {
  162. var finalMap = adjustSourceMap(loader, {format: 'sourceRelative'}, reworked.map);
  163. callback(null, reworked.content, finalMap);
  164. }
  165. // complete without source-map
  166. else {
  167. callback(null, reworked.content);
  168. }
  169. }
  170. }
  171. /**
  172. * Push a warning for the given exception and return the original content.
  173. * @param {string} label Summary of the error
  174. * @param {string|Error} [exception] Optional extended error details
  175. * @returns {string} The original CSS content
  176. */
  177. function handleAsWarning(label, exception) {
  178. if (!options.silent) {
  179. loader.emitWarning(encodeError(label, exception));
  180. }
  181. return content;
  182. }
  183. /**
  184. * Push a warning for the given exception and return the original content.
  185. * @param {string} label Summary of the error
  186. * @param {string|Error} [exception] Optional extended error details
  187. * @returns {string} The original CSS content
  188. */
  189. function handleAsError(label, exception) {
  190. loader.emitError(encodeError(label, exception));
  191. return content;
  192. }
  193. function encodeError(label, exception) {
  194. return new Error(
  195. [
  196. PACKAGE_NAME,
  197. ': ',
  198. [label]
  199. .concat(
  200. (typeof exception === 'string') && exception ||
  201. (exception instanceof Error) && [exception.message, exception.stack.split('\n')[1].trim()] ||
  202. []
  203. )
  204. .filter(Boolean)
  205. .join('\n ')
  206. ].join('')
  207. );
  208. }
  209. }
  210. module.exports = Object.assign(resolveUrlLoader, joinFn);