apply-source-maps.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. var fs = require('fs');
  2. var path = require('path');
  3. var isAllowedResource = require('./is-allowed-resource');
  4. var matchDataUri = require('./match-data-uri');
  5. var rebaseLocalMap = require('./rebase-local-map');
  6. var rebaseRemoteMap = require('./rebase-remote-map');
  7. var Token = require('../tokenizer/token');
  8. var hasProtocol = require('../utils/has-protocol');
  9. var isDataUriResource = require('../utils/is-data-uri-resource');
  10. var isRemoteResource = require('../utils/is-remote-resource');
  11. var MAP_MARKER_PATTERN = /^\/\*# sourceMappingURL=(\S+) \*\/$/;
  12. function applySourceMaps(tokens, context, callback) {
  13. var applyContext = {
  14. callback: callback,
  15. fetch: context.options.fetch,
  16. index: 0,
  17. inline: context.options.inline,
  18. inlineRequest: context.options.inlineRequest,
  19. inlineTimeout: context.options.inlineTimeout,
  20. inputSourceMapTracker: context.inputSourceMapTracker,
  21. localOnly: context.localOnly,
  22. processedTokens: [],
  23. rebaseTo: context.options.rebaseTo,
  24. sourceTokens: tokens,
  25. warnings: context.warnings
  26. };
  27. return context.options.sourceMap && tokens.length > 0
  28. ? doApplySourceMaps(applyContext)
  29. : callback(tokens);
  30. }
  31. function doApplySourceMaps(applyContext) {
  32. var singleSourceTokens = [];
  33. var lastSource = findTokenSource(applyContext.sourceTokens[0]);
  34. var source;
  35. var token;
  36. var l;
  37. for (l = applyContext.sourceTokens.length; applyContext.index < l; applyContext.index++) {
  38. token = applyContext.sourceTokens[applyContext.index];
  39. source = findTokenSource(token);
  40. if (source != lastSource) {
  41. singleSourceTokens = [];
  42. lastSource = source;
  43. }
  44. singleSourceTokens.push(token);
  45. applyContext.processedTokens.push(token);
  46. if (token[0] == Token.COMMENT && MAP_MARKER_PATTERN.test(token[1])) {
  47. return fetchAndApplySourceMap(token[1], source, singleSourceTokens, applyContext);
  48. }
  49. }
  50. return applyContext.callback(applyContext.processedTokens);
  51. }
  52. function findTokenSource(token) {
  53. var scope;
  54. var metadata;
  55. if (token[0] == Token.AT_RULE || token[0] == Token.COMMENT || token[0] == Token.RAW) {
  56. metadata = token[2][0];
  57. } else {
  58. scope = token[1][0];
  59. metadata = scope[2][0];
  60. }
  61. return metadata[2];
  62. }
  63. function fetchAndApplySourceMap(sourceMapComment, source, singleSourceTokens, applyContext) {
  64. return extractInputSourceMapFrom(sourceMapComment, applyContext, function(inputSourceMap) {
  65. if (inputSourceMap) {
  66. applyContext.inputSourceMapTracker.track(source, inputSourceMap);
  67. applySourceMapRecursively(singleSourceTokens, applyContext.inputSourceMapTracker);
  68. }
  69. applyContext.index++;
  70. return doApplySourceMaps(applyContext);
  71. });
  72. }
  73. function extractInputSourceMapFrom(sourceMapComment, applyContext, whenSourceMapReady) {
  74. var uri = MAP_MARKER_PATTERN.exec(sourceMapComment)[1];
  75. var absoluteUri;
  76. var sourceMap;
  77. var rebasedMap;
  78. if (isDataUriResource(uri)) {
  79. sourceMap = extractInputSourceMapFromDataUri(uri);
  80. return whenSourceMapReady(sourceMap);
  81. } if (isRemoteResource(uri)) {
  82. return loadInputSourceMapFromRemoteUri(uri, applyContext, function(sourceMap) {
  83. var parsedMap;
  84. if (sourceMap) {
  85. parsedMap = JSON.parse(sourceMap);
  86. rebasedMap = rebaseRemoteMap(parsedMap, uri);
  87. whenSourceMapReady(rebasedMap);
  88. } else {
  89. whenSourceMapReady(null);
  90. }
  91. });
  92. }
  93. // at this point `uri` is already rebased, see lib/reader/rebase.js#rebaseSourceMapComment
  94. // it is rebased to be consistent with rebasing other URIs
  95. // however here we need to resolve it back to read it from disk
  96. absoluteUri = path.resolve(applyContext.rebaseTo, uri);
  97. sourceMap = loadInputSourceMapFromLocalUri(absoluteUri, applyContext);
  98. if (sourceMap) {
  99. rebasedMap = rebaseLocalMap(sourceMap, absoluteUri, applyContext.rebaseTo);
  100. return whenSourceMapReady(rebasedMap);
  101. }
  102. return whenSourceMapReady(null);
  103. }
  104. function extractInputSourceMapFromDataUri(uri) {
  105. var dataUriMatch = matchDataUri(uri);
  106. var charset = dataUriMatch[2] ? dataUriMatch[2].split(/[=;]/)[2] : 'us-ascii';
  107. var encoding = dataUriMatch[3] ? dataUriMatch[3].split(';')[1] : 'utf8';
  108. var data = encoding == 'utf8' ? global.unescape(dataUriMatch[4]) : dataUriMatch[4];
  109. var buffer = Buffer.from(data, encoding);
  110. buffer.charset = charset;
  111. return JSON.parse(buffer.toString());
  112. }
  113. function loadInputSourceMapFromRemoteUri(uri, applyContext, whenLoaded) {
  114. var isAllowed = isAllowedResource(uri, true, applyContext.inline);
  115. var isRuntimeResource = !hasProtocol(uri);
  116. if (applyContext.localOnly) {
  117. applyContext.warnings.push('Cannot fetch remote resource from "' + uri + '" as no callback given.');
  118. return whenLoaded(null);
  119. } if (isRuntimeResource) {
  120. applyContext.warnings.push('Cannot fetch "' + uri + '" as no protocol given.');
  121. return whenLoaded(null);
  122. } if (!isAllowed) {
  123. applyContext.warnings.push('Cannot fetch "' + uri + '" as resource is not allowed.');
  124. return whenLoaded(null);
  125. }
  126. applyContext.fetch(uri, applyContext.inlineRequest, applyContext.inlineTimeout, function(error, body) {
  127. if (error) {
  128. applyContext.warnings.push('Missing source map at "' + uri + '" - ' + error);
  129. return whenLoaded(null);
  130. }
  131. whenLoaded(body);
  132. });
  133. }
  134. function loadInputSourceMapFromLocalUri(uri, applyContext) {
  135. var isAllowed = isAllowedResource(uri, false, applyContext.inline);
  136. var sourceMap;
  137. if (!fs.existsSync(uri) || !fs.statSync(uri).isFile()) {
  138. applyContext.warnings.push('Ignoring local source map at "' + uri + '" as resource is missing.');
  139. return null;
  140. } if (!isAllowed) {
  141. applyContext.warnings.push('Cannot fetch "' + uri + '" as resource is not allowed.');
  142. return null;
  143. } if (!fs.statSync(uri).size) {
  144. applyContext.warnings.push('Cannot fetch "' + uri + '" as resource is empty.');
  145. return null;
  146. }
  147. sourceMap = fs.readFileSync(uri, 'utf-8');
  148. return JSON.parse(sourceMap);
  149. }
  150. function applySourceMapRecursively(tokens, inputSourceMapTracker) {
  151. var token;
  152. var i, l;
  153. for (i = 0, l = tokens.length; i < l; i++) {
  154. token = tokens[i];
  155. switch (token[0]) {
  156. case Token.AT_RULE:
  157. applySourceMapTo(token, inputSourceMapTracker);
  158. break;
  159. case Token.AT_RULE_BLOCK:
  160. applySourceMapRecursively(token[1], inputSourceMapTracker);
  161. applySourceMapRecursively(token[2], inputSourceMapTracker);
  162. break;
  163. case Token.AT_RULE_BLOCK_SCOPE:
  164. applySourceMapTo(token, inputSourceMapTracker);
  165. break;
  166. case Token.NESTED_BLOCK:
  167. applySourceMapRecursively(token[1], inputSourceMapTracker);
  168. applySourceMapRecursively(token[2], inputSourceMapTracker);
  169. break;
  170. case Token.NESTED_BLOCK_SCOPE:
  171. applySourceMapTo(token, inputSourceMapTracker);
  172. break;
  173. case Token.COMMENT:
  174. applySourceMapTo(token, inputSourceMapTracker);
  175. break;
  176. case Token.PROPERTY:
  177. applySourceMapRecursively(token, inputSourceMapTracker);
  178. break;
  179. case Token.PROPERTY_BLOCK:
  180. applySourceMapRecursively(token[1], inputSourceMapTracker);
  181. break;
  182. case Token.PROPERTY_NAME:
  183. applySourceMapTo(token, inputSourceMapTracker);
  184. break;
  185. case Token.PROPERTY_VALUE:
  186. applySourceMapTo(token, inputSourceMapTracker);
  187. break;
  188. case Token.RULE:
  189. applySourceMapRecursively(token[1], inputSourceMapTracker);
  190. applySourceMapRecursively(token[2], inputSourceMapTracker);
  191. break;
  192. case Token.RULE_SCOPE:
  193. applySourceMapTo(token, inputSourceMapTracker);
  194. }
  195. }
  196. return tokens;
  197. }
  198. function applySourceMapTo(token, inputSourceMapTracker) {
  199. var value = token[1];
  200. var metadata = token[2];
  201. var newMetadata = [];
  202. var i, l;
  203. for (i = 0, l = metadata.length; i < l; i++) {
  204. newMetadata.push(inputSourceMapTracker.originalPositionFor(metadata[i], value.length));
  205. }
  206. token[2] = newMetadata;
  207. }
  208. module.exports = applySourceMaps;