index.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. /*
  2. * MIT License http://opensource.org/licenses/MIT
  3. * Author: Ben Holloway @bholloway
  4. */
  5. 'use strict';
  6. var os = require('os'),
  7. path = require('path'),
  8. fs = require('fs'),
  9. util = require('util'),
  10. loaderUtils = require('loader-utils'),
  11. SourceMapConsumer = require('source-map').SourceMapConsumer;
  12. var adjustSourceMap = require('adjust-sourcemap-loader/lib/process');
  13. var valueProcessor = require('./lib/value-processor'),
  14. joinFn = require('./lib/join-function'),
  15. logToTestHarness = require('./lib/log-to-test-harness');
  16. const DEPRECATED_OPTIONS = {
  17. engine: [
  18. 'DEP_RESOLVE_URL_LOADER_OPTION_ENGINE',
  19. 'the "engine" option is deprecated, "postcss" engine is the default, using "rework" engine is not advised'
  20. ],
  21. keepQuery: [
  22. 'DEP_RESOLVE_URL_LOADER_OPTION_KEEP_QUERY',
  23. '"keepQuery" option has been removed, the query and/or hash are now always retained'
  24. ],
  25. absolute: [
  26. 'DEP_RESOLVE_URL_LOADER_OPTION_ABSOLUTE',
  27. '"absolute" option has been removed, consider the "join" option if absolute paths must be processed'
  28. ],
  29. attempts: [
  30. 'DEP_RESOLVE_URL_LOADER_OPTION_ATTEMPTS',
  31. '"attempts" option has been removed, consider the "join" option if search is needed'
  32. ],
  33. includeRoot: [
  34. 'DEP_RESOLVE_URL_LOADER_OPTION_INCLUDE_ROOT',
  35. '"includeRoot" option has been removed, consider the "join" option if search is needed'
  36. ],
  37. fail: [
  38. 'DEP_RESOLVE_URL_LOADER_OPTION_FAIL',
  39. '"fail" option has been removed'
  40. ]
  41. };
  42. /**
  43. * A webpack loader that resolves absolute url() paths relative to their original source file.
  44. * Requires source-maps to do any meaningful work.
  45. * @param {string} content Css content
  46. * @param {object} sourceMap The source-map
  47. * @returns {string|String}
  48. */
  49. function resolveUrlLoader(content, sourceMap) {
  50. /* jshint validthis:true */
  51. // details of the file being processed
  52. var loader = this;
  53. // a relative loader.context is a problem
  54. if (/^\./.test(loader.context)) {
  55. return handleAsError(
  56. 'webpack misconfiguration',
  57. 'loader.context is relative, expected absolute'
  58. );
  59. }
  60. // infer webpack version from new loader features
  61. var isWebpackGte5 = 'getOptions' in loader && typeof loader.getOptions === 'function';
  62. // webpack 1: prefer loader query, else options object
  63. // webpack 2: prefer loader options
  64. // webpack 3: deprecate loader.options object
  65. // webpack 4: loader.options no longer defined
  66. var rawOptions = loaderUtils.getOptions(loader),
  67. options = Object.assign(
  68. {
  69. sourceMap: loader.sourceMap,
  70. engine : 'postcss',
  71. silent : false,
  72. removeCR : os.EOL.includes('\r'),
  73. root : false,
  74. debug : false,
  75. join : joinFn.defaultJoin
  76. },
  77. rawOptions
  78. );
  79. // maybe log options for the test harness
  80. if (process.env.RESOLVE_URL_LOADER_TEST_HARNESS) {
  81. logToTestHarness(
  82. process[process.env.RESOLVE_URL_LOADER_TEST_HARNESS],
  83. options
  84. );
  85. }
  86. // deprecated options
  87. var deprecatedItems = Object.entries(DEPRECATED_OPTIONS).filter(([key]) => key in rawOptions);
  88. if (deprecatedItems.length) {
  89. deprecatedItems.forEach(([, value]) => handleAsDeprecated(...value));
  90. }
  91. // validate join option
  92. if (typeof options.join !== 'function') {
  93. return handleAsError(
  94. 'loader misconfiguration',
  95. '"join" option must be a Function'
  96. );
  97. } else if (options.join.length !== 2) {
  98. return handleAsError(
  99. 'loader misconfiguration',
  100. '"join" Function must take exactly 2 arguments (options, loader)'
  101. );
  102. }
  103. // validate the result of calling the join option
  104. var joinProper = options.join(options, loader);
  105. if (typeof joinProper !== 'function') {
  106. return handleAsError(
  107. 'loader misconfiguration',
  108. '"join" option must itself return a Function when it is called'
  109. );
  110. } else if (joinProper.length !== 1) {
  111. return handleAsError(
  112. 'loader misconfiguration',
  113. '"join" Function must create a function that takes exactly 1 arguments (item)'
  114. );
  115. }
  116. // validate root option
  117. if (typeof options.root === 'string') {
  118. var isValid = (options.root === '') ||
  119. (path.isAbsolute(options.root) && fs.existsSync(options.root) && fs.statSync(options.root).isDirectory());
  120. if (!isValid) {
  121. return handleAsError(
  122. 'loader misconfiguration',
  123. '"root" option must be an empty string or an absolute path to an existing directory'
  124. );
  125. }
  126. } else if (options.root !== false) {
  127. handleAsWarning(
  128. 'loader misconfiguration',
  129. '"root" option must be string where used or false where unused'
  130. );
  131. }
  132. // loader result is cacheable
  133. loader.cacheable();
  134. // incoming source-map
  135. var sourceMapConsumer, absSourceMap;
  136. if (sourceMap) {
  137. // support non-standard string encoded source-map (per less-loader)
  138. if (typeof sourceMap === 'string') {
  139. try {
  140. sourceMap = JSON.parse(sourceMap);
  141. }
  142. catch (exception) {
  143. return handleAsError(
  144. 'source-map error',
  145. 'cannot parse source-map string (from less-loader?)'
  146. );
  147. }
  148. }
  149. // leverage adjust-sourcemap-loader's codecs to avoid having to make any assumptions about the sourcemap
  150. // historically this is a regular source of breakage
  151. try {
  152. absSourceMap = adjustSourceMap(loader, {format: 'absolute'}, sourceMap);
  153. }
  154. catch (exception) {
  155. return handleAsError(
  156. 'source-map error',
  157. exception.message
  158. );
  159. }
  160. // prepare the adjusted sass source-map for later look-ups
  161. sourceMapConsumer = new SourceMapConsumer(absSourceMap);
  162. } else {
  163. handleAsWarning(
  164. 'webpack misconfiguration',
  165. 'webpack or the upstream loader did not supply a source-map'
  166. );
  167. }
  168. // choose a CSS engine
  169. var enginePath = /^[\w-]+$/.test(options.engine) && path.join(__dirname, 'lib', 'engine', options.engine + '.js');
  170. var isValidEngine = fs.existsSync(enginePath);
  171. if (!isValidEngine) {
  172. return handleAsError(
  173. 'loader misconfiguration',
  174. '"engine" option is not valid'
  175. );
  176. }
  177. // allow engine to throw at initialisation
  178. var engine;
  179. try {
  180. engine = require(enginePath);
  181. } catch (error) {
  182. return handleAsError(
  183. 'error initialising',
  184. error
  185. );
  186. }
  187. // process async
  188. var callback = loader.async();
  189. Promise
  190. .resolve(engine(loader.resourcePath, content, {
  191. outputSourceMap : !!options.sourceMap,
  192. absSourceMap : absSourceMap,
  193. sourceMapConsumer : sourceMapConsumer,
  194. removeCR : options.removeCR,
  195. transformDeclaration: valueProcessor({
  196. join : joinProper,
  197. root : options.root,
  198. directory: path.dirname(loader.resourcePath)
  199. })
  200. }))
  201. .catch(onFailure)
  202. .then(onSuccess);
  203. function onFailure(error) {
  204. callback(encodeError('error processing CSS', error));
  205. }
  206. function onSuccess(result) {
  207. if (result) {
  208. // complete with source-map
  209. // webpack4 and earlier: source-map sources are relative to the file being processed
  210. // webpack5: source-map sources are relative to the project root but without a leading slash
  211. if (options.sourceMap) {
  212. var finalMap = adjustSourceMap(loader, {
  213. format: isWebpackGte5 ? 'projectRelative' : 'sourceRelative'
  214. }, result.map);
  215. callback(null, result.content, finalMap);
  216. }
  217. // complete without source-map
  218. else {
  219. callback(null, result.content);
  220. }
  221. }
  222. }
  223. /**
  224. * Trigger a node deprecation message for the given exception and return the original content.
  225. * @param {string} code Deprecation code
  226. * @param {string} message Deprecation message
  227. * @returns {string} The original CSS content
  228. */
  229. function handleAsDeprecated(code, message) {
  230. if (!options.silent) {
  231. util.deprecate(() => undefined, message, code)();
  232. }
  233. return content;
  234. }
  235. /**
  236. * Push a warning for the given exception and return the original content.
  237. * @param {string} label Summary of the error
  238. * @param {string|Error} [exception] Optional extended error details
  239. * @returns {string} The original CSS content
  240. */
  241. function handleAsWarning(label, exception) {
  242. if (!options.silent) {
  243. loader.emitWarning(encodeError(label, exception));
  244. }
  245. return content;
  246. }
  247. /**
  248. * Push a warning for the given exception and return the original content.
  249. * @param {string} label Summary of the error
  250. * @param {string|Error} [exception] Optional extended error details
  251. * @returns {string} The original CSS content
  252. */
  253. function handleAsError(label, exception) {
  254. loader.emitError(encodeError(label, exception));
  255. return content;
  256. }
  257. function encodeError(label, exception) {
  258. return new Error(
  259. [
  260. 'resolve-url-loader',
  261. ': ',
  262. [label]
  263. .concat(
  264. (typeof exception === 'string') && exception ||
  265. Array.isArray(exception) && exception ||
  266. (exception instanceof Error) && [exception.message, exception.stack.split('\n')[1].trim()] ||
  267. []
  268. )
  269. .filter(Boolean)
  270. .join('\n ')
  271. ].join('')
  272. );
  273. }
  274. }
  275. module.exports = Object.assign(resolveUrlLoader, joinFn);