index.js 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. /*
  2. * MIT License http://opensource.org/licenses/MIT
  3. * Author: Ben Holloway @bholloway
  4. */
  5. 'use strict';
  6. const os = require('os');
  7. const path = require('path');
  8. const fs = require('fs');
  9. const util = require('util');
  10. const loaderUtils = require('loader-utils');
  11. const SourceMapConsumer = require('source-map').SourceMapConsumer;
  12. const adjustSourceMap = require('adjust-sourcemap-loader/lib/process');
  13. const valueProcessor = require('./lib/value-processor');
  14. const joinFn = require('./lib/join-function');
  15. const logToTestHarness = require('./lib/log-to-test-harness');
  16. const DEPRECATED_OPTIONS = {
  17. engine: [
  18. 'DEP_RESOLVE_URL_LOADER_OPTION_ENGINE',
  19. '"engine" option has been removed, postcss is the only parser used'
  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. const 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. const isWebpackGte5 = 'getOptions' in loader && typeof loader.getOptions === 'function';
  62. // use loader.getOptions where available otherwise use loaderUtils
  63. const rawOptions = isWebpackGte5 ? loader.getOptions() : loaderUtils.getOptions(loader);
  64. const options = Object.assign(
  65. {
  66. sourceMap: loader.sourceMap,
  67. silent : false,
  68. removeCR : os.EOL.includes('\r'),
  69. root : false,
  70. debug : false,
  71. join : joinFn.defaultJoin
  72. },
  73. rawOptions,
  74. );
  75. // maybe log options for the test harness
  76. if (process.env.RESOLVE_URL_LOADER_TEST_HARNESS) {
  77. logToTestHarness(
  78. process[process.env.RESOLVE_URL_LOADER_TEST_HARNESS],
  79. options
  80. );
  81. }
  82. // deprecated options
  83. const deprecatedItems = Object.entries(DEPRECATED_OPTIONS).filter(([key]) => key in rawOptions);
  84. if (deprecatedItems.length) {
  85. deprecatedItems.forEach(([, value]) => handleAsDeprecated(...value));
  86. }
  87. // validate join option
  88. if (typeof options.join !== 'function') {
  89. return handleAsError(
  90. 'loader misconfiguration',
  91. '"join" option must be a Function'
  92. );
  93. } else if (options.join.length !== 2) {
  94. return handleAsError(
  95. 'loader misconfiguration',
  96. '"join" Function must take exactly 2 arguments (options, loader)'
  97. );
  98. }
  99. // validate the result of calling the join option
  100. const joinProper = options.join(options, loader);
  101. if (typeof joinProper !== 'function') {
  102. return handleAsError(
  103. 'loader misconfiguration',
  104. '"join" option must itself return a Function when it is called'
  105. );
  106. } else if (joinProper.length !== 1) {
  107. return handleAsError(
  108. 'loader misconfiguration',
  109. '"join" Function must create a function that takes exactly 1 arguments (item)'
  110. );
  111. }
  112. // validate root option
  113. if (typeof options.root === 'string') {
  114. const isValid = (options.root === '') ||
  115. (path.isAbsolute(options.root) && fs.existsSync(options.root) && fs.statSync(options.root).isDirectory());
  116. if (!isValid) {
  117. return handleAsError(
  118. 'loader misconfiguration',
  119. '"root" option must be an empty string or an absolute path to an existing directory'
  120. );
  121. }
  122. } else if (options.root !== false) {
  123. handleAsWarning(
  124. 'loader misconfiguration',
  125. '"root" option must be string where used or false where unused'
  126. );
  127. }
  128. // loader result is cacheable
  129. loader.cacheable();
  130. // incoming source-map
  131. let absSourceMap = null;
  132. let sourceMapConsumer = null;
  133. if (sourceMap) {
  134. // support non-standard string encoded source-map (per less-loader)
  135. if (typeof sourceMap === 'string') {
  136. try {
  137. sourceMap = JSON.parse(sourceMap);
  138. }
  139. catch (exception) {
  140. return handleAsError(
  141. 'source-map error',
  142. 'cannot parse source-map string (from less-loader?)'
  143. );
  144. }
  145. }
  146. // leverage adjust-sourcemap-loader's codecs to avoid having to make any assumptions about the sourcemap
  147. // historically this is a regular source of breakage
  148. try {
  149. absSourceMap = adjustSourceMap(loader, {format: 'absolute'}, sourceMap);
  150. }
  151. catch (exception) {
  152. return handleAsError(
  153. 'source-map error',
  154. exception.message
  155. );
  156. }
  157. // prepare the adjusted sass source-map for later look-ups
  158. sourceMapConsumer = new SourceMapConsumer(absSourceMap);
  159. } else {
  160. handleAsWarning(
  161. 'webpack misconfiguration',
  162. 'webpack or the upstream loader did not supply a source-map'
  163. );
  164. }
  165. // allow engine to throw at initialisation
  166. let engine = null;
  167. try {
  168. engine = require('./lib/engine/postcss');
  169. } catch (error) {
  170. return handleAsError(
  171. 'error initialising',
  172. error
  173. );
  174. }
  175. // process async
  176. const callback = loader.async();
  177. Promise
  178. .resolve(engine(loader.resourcePath, content, {
  179. outputSourceMap : !!options.sourceMap,
  180. absSourceMap : absSourceMap,
  181. sourceMapConsumer : sourceMapConsumer,
  182. removeCR : options.removeCR,
  183. transformDeclaration: valueProcessor({
  184. join : joinProper,
  185. root : options.root,
  186. directory: path.dirname(loader.resourcePath)
  187. })
  188. }))
  189. .catch(onFailure)
  190. .then(onSuccess);
  191. function onFailure(error) {
  192. callback(encodeError('error processing CSS', error));
  193. }
  194. function onSuccess(result) {
  195. if (result) {
  196. // complete with source-map
  197. // webpack4 and earlier: source-map sources are relative to the file being processed
  198. // webpack5: source-map sources are relative to the project root but without a leading slash
  199. if (options.sourceMap) {
  200. const finalMap = adjustSourceMap(loader, {
  201. format: isWebpackGte5 ? 'projectRelative' : 'sourceRelative'
  202. }, result.map);
  203. callback(null, result.content, finalMap);
  204. }
  205. // complete without source-map
  206. else {
  207. callback(null, result.content);
  208. }
  209. }
  210. }
  211. /**
  212. * Trigger a node deprecation message for the given exception and return the original content.
  213. * @param {string} code Deprecation code
  214. * @param {string} message Deprecation message
  215. * @returns {string} The original CSS content
  216. */
  217. function handleAsDeprecated(code, message) {
  218. if (!options.silent) {
  219. util.deprecate(() => undefined, message, code)();
  220. }
  221. return content;
  222. }
  223. /**
  224. * Push a warning for the given exception and return the original content.
  225. * @param {string} label Summary of the error
  226. * @param {string|Error} [exception] Optional extended error details
  227. * @returns {string} The original CSS content
  228. */
  229. function handleAsWarning(label, exception) {
  230. if (!options.silent) {
  231. loader.emitWarning(encodeError(label, exception));
  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 handleAsError(label, exception) {
  242. loader.emitError(encodeError(label, exception));
  243. return content;
  244. }
  245. function encodeError(label, exception) {
  246. return new Error(
  247. [
  248. 'resolve-url-loader',
  249. ': ',
  250. [label]
  251. .concat(
  252. (typeof exception === 'string') && exception ||
  253. Array.isArray(exception) && exception ||
  254. (exception instanceof Error) && [exception.message, exception.stack.split('\n')[1].trim()] ||
  255. []
  256. )
  257. .filter(Boolean)
  258. .join('\n ')
  259. ].join('')
  260. );
  261. }
  262. }
  263. module.exports = Object.assign(resolveUrlLoader, joinFn);