apply-source-maps.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  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) {
  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. } else 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. } else {
  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. } else {
  102. return whenSourceMapReady(null);
  103. }
  104. }
  105. }
  106. function extractInputSourceMapFromDataUri(uri) {
  107. var dataUriMatch = matchDataUri(uri);
  108. var charset = dataUriMatch[2] ? dataUriMatch[2].split(/[=;]/)[2] : 'us-ascii';
  109. var encoding = dataUriMatch[3] ? dataUriMatch[3].split(';')[1] : 'utf8';
  110. var data = encoding == 'utf8' ? global.unescape(dataUriMatch[4]) : dataUriMatch[4];
  111. var buffer = new Buffer(data, encoding);
  112. buffer.charset = charset;
  113. return JSON.parse(buffer.toString());
  114. }
  115. function loadInputSourceMapFromRemoteUri(uri, applyContext, whenLoaded) {
  116. var isAllowed = isAllowedResource(uri, true, applyContext.inline);
  117. var isRuntimeResource = !hasProtocol(uri);
  118. if (applyContext.localOnly) {
  119. applyContext.warnings.push('Cannot fetch remote resource from "' + uri + '" as no callback given.');
  120. return whenLoaded(null);
  121. } else if (isRuntimeResource) {
  122. applyContext.warnings.push('Cannot fetch "' + uri + '" as no protocol given.');
  123. return whenLoaded(null);
  124. } else if (!isAllowed) {
  125. applyContext.warnings.push('Cannot fetch "' + uri + '" as resource is not allowed.');
  126. return whenLoaded(null);
  127. }
  128. applyContext.fetch(uri, applyContext.inlineRequest, applyContext.inlineTimeout, function (error, body) {
  129. if (error) {
  130. applyContext.warnings.push('Missing source map at "' + uri + '" - ' + error);
  131. return whenLoaded(null);
  132. }
  133. whenLoaded(body);
  134. });
  135. }
  136. function loadInputSourceMapFromLocalUri(uri, applyContext) {
  137. var isAllowed = isAllowedResource(uri, false, applyContext.inline);
  138. var sourceMap;
  139. if (!fs.existsSync(uri) || !fs.statSync(uri).isFile()) {
  140. applyContext.warnings.push('Ignoring local source map at "' + uri + '" as resource is missing.');
  141. return null;
  142. } else if (!isAllowed) {
  143. applyContext.warnings.push('Cannot fetch "' + uri + '" as resource is not allowed.');
  144. return null;
  145. }
  146. sourceMap = fs.readFileSync(uri, 'utf-8');
  147. return JSON.parse(sourceMap);
  148. }
  149. function applySourceMapRecursively(tokens, inputSourceMapTracker) {
  150. var token;
  151. var i, l;
  152. for (i = 0, l = tokens.length; i < l; i++) {
  153. token = tokens[i];
  154. switch (token[0]) {
  155. case Token.AT_RULE:
  156. applySourceMapTo(token, inputSourceMapTracker);
  157. break;
  158. case Token.AT_RULE_BLOCK:
  159. applySourceMapRecursively(token[1], inputSourceMapTracker);
  160. applySourceMapRecursively(token[2], inputSourceMapTracker);
  161. break;
  162. case Token.AT_RULE_BLOCK_SCOPE:
  163. applySourceMapTo(token, inputSourceMapTracker);
  164. break;
  165. case Token.NESTED_BLOCK:
  166. applySourceMapRecursively(token[1], inputSourceMapTracker);
  167. applySourceMapRecursively(token[2], inputSourceMapTracker);
  168. break;
  169. case Token.NESTED_BLOCK_SCOPE:
  170. applySourceMapTo(token, inputSourceMapTracker);
  171. break;
  172. case Token.COMMENT:
  173. applySourceMapTo(token, inputSourceMapTracker);
  174. break;
  175. case Token.PROPERTY:
  176. applySourceMapRecursively(token, inputSourceMapTracker);
  177. break;
  178. case Token.PROPERTY_BLOCK:
  179. applySourceMapRecursively(token[1], inputSourceMapTracker);
  180. break;
  181. case Token.PROPERTY_NAME:
  182. applySourceMapTo(token, inputSourceMapTracker);
  183. break;
  184. case Token.PROPERTY_VALUE:
  185. applySourceMapTo(token, inputSourceMapTracker);
  186. break;
  187. case Token.RULE:
  188. applySourceMapRecursively(token[1], inputSourceMapTracker);
  189. applySourceMapRecursively(token[2], inputSourceMapTracker);
  190. break;
  191. case Token.RULE_SCOPE:
  192. applySourceMapTo(token, inputSourceMapTracker);
  193. }
  194. }
  195. return tokens;
  196. }
  197. function applySourceMapTo(token, inputSourceMapTracker) {
  198. var value = token[1];
  199. var metadata = token[2];
  200. var newMetadata = [];
  201. var i, l;
  202. for (i = 0, l = metadata.length; i < l; i++) {
  203. newMetadata.push(inputSourceMapTracker.originalPositionFor(metadata[i], value.length));
  204. }
  205. token[2] = newMetadata;
  206. }
  207. module.exports = applySourceMaps;