source-map-worker.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. const { fetch, assert } = require("devtools/shared/DevToolsUtils");
  2. const { joinURI } = require("devtools/shared/path");
  3. const path = require("sdk/fs/path");
  4. const { SourceMapConsumer, SourceMapGenerator } = require("source-map");
  5. const { isJavaScript } = require("./source");
  6. const {
  7. originalToGeneratedId,
  8. generatedToOriginalId,
  9. isGeneratedId,
  10. isOriginalId
  11. } = require("./source-map-util");
  12. let sourceMapRequests = new Map();
  13. let sourceMapsEnabled = false;
  14. function clearSourceMaps() {
  15. sourceMapRequests.clear();
  16. }
  17. function enableSourceMaps() {
  18. sourceMapsEnabled = true;
  19. }
  20. function _resolveSourceMapURL(source) {
  21. const { url = "", sourceMapURL = "" } = source;
  22. if (path.isURL(sourceMapURL) || url == "") {
  23. // If it's already a full URL or the source doesn't have a URL,
  24. // don't resolve anything.
  25. return sourceMapURL;
  26. } else if (path.isAbsolute(sourceMapURL)) {
  27. // If it's an absolute path, it should be resolved relative to the
  28. // host of the source.
  29. const { protocol = "", host = "" } = parse(url);
  30. return `${protocol}//${host}${sourceMapURL}`;
  31. }
  32. // Otherwise, it's a relative path and should be resolved relative
  33. // to the source.
  34. return dirname(url) + "/" + sourceMapURL;
  35. }
  36. /**
  37. * Sets the source map's sourceRoot to be relative to the source map url.
  38. * @memberof utils/source-map-worker
  39. * @static
  40. */
  41. function _setSourceMapRoot(sourceMap, absSourceMapURL, source) {
  42. // No need to do this fiddling if we won't be fetching any sources over the
  43. // wire.
  44. if (sourceMap.hasContentsOfAllSources()) {
  45. return;
  46. }
  47. const base = dirname(
  48. (absSourceMapURL.indexOf("data:") === 0 && source.url) ?
  49. source.url :
  50. absSourceMapURL
  51. );
  52. if (sourceMap.sourceRoot) {
  53. sourceMap.sourceRoot = joinURI(base, sourceMap.sourceRoot);
  54. } else {
  55. sourceMap.sourceRoot = base;
  56. }
  57. return sourceMap;
  58. }
  59. function _getSourceMap(generatedSourceId)
  60. : ?Promise<SourceMapConsumer> {
  61. return sourceMapRequests.get(generatedSourceId);
  62. }
  63. async function _resolveAndFetch(generatedSource) : SourceMapConsumer {
  64. // Fetch the sourcemap over the network and create it.
  65. const sourceMapURL = _resolveSourceMapURL(generatedSource);
  66. const fetched = await fetch(
  67. sourceMapURL, { loadFromCache: false }
  68. );
  69. // Create the source map and fix it up.
  70. const map = new SourceMapConsumer(fetched.content);
  71. _setSourceMapRoot(map, sourceMapURL, generatedSource);
  72. return map;
  73. }
  74. function _fetchSourceMap(generatedSource) {
  75. const existingRequest = sourceMapRequests.get(generatedSource.id);
  76. if (existingRequest) {
  77. // If it has already been requested, return the request. Make sure
  78. // to do this even if sourcemapping is turned off, because
  79. // pretty-printing uses sourcemaps.
  80. //
  81. // An important behavior here is that if it's in the middle of
  82. // requesting it, all subsequent calls will block on the initial
  83. // request.
  84. return existingRequest;
  85. } else if (!generatedSource.sourceMapURL || !sourceMapsEnabled) {
  86. return Promise.resolve(null);
  87. }
  88. // Fire off the request, set it in the cache, and return it.
  89. // Suppress any errors and just return null (ignores bogus
  90. // sourcemaps).
  91. const req = _resolveAndFetch(generatedSource).catch(() => null);
  92. sourceMapRequests.set(generatedSource.id, req);
  93. return req;
  94. }
  95. async function getOriginalURLs(generatedSource) {
  96. const map = await _fetchSourceMap(generatedSource);
  97. return map && map.sources;
  98. }
  99. async function getGeneratedLocation(location: Location, originalSource: Source)
  100. : Promise<Location> {
  101. if (!isOriginalId(location.sourceId)) {
  102. return location;
  103. }
  104. const generatedSourceId = originalToGeneratedId(location.sourceId);
  105. const map = await _getSourceMap(generatedSourceId);
  106. if (!map) {
  107. return location;
  108. }
  109. const { line, column } = map.generatedPositionFor({
  110. source: originalSource.url,
  111. line: location.line,
  112. column: location.column == null ? 0 : location.column
  113. });
  114. return {
  115. sourceId: generatedSourceId,
  116. line: line,
  117. // Treat 0 as no column so that line breakpoints work correctly.
  118. column: column === 0 ? undefined : column
  119. };
  120. }
  121. async function getOriginalLocation(location) {
  122. if (!isGeneratedId(location.sourceId)) {
  123. return location;
  124. }
  125. const map = await _getSourceMap(location.sourceId);
  126. if (!map) {
  127. return location;
  128. }
  129. const { source: url, line, column } = map.originalPositionFor({
  130. line: location.line,
  131. column: location.column == null ? Infinity : location.column
  132. });
  133. if (url == null) {
  134. // No url means the location didn't map.
  135. return location;
  136. }
  137. return {
  138. sourceId: generatedToOriginalId(location.sourceId, url),
  139. line,
  140. column
  141. };
  142. }
  143. async function getOriginalSourceText(originalSource) {
  144. assert(isOriginalId(originalSource.id),
  145. "Source is not an original source");
  146. const generatedSourceId = originalToGeneratedId(originalSource.id);
  147. const map = await _getSourceMap(generatedSourceId);
  148. if (!map) {
  149. return null;
  150. }
  151. let text = map.sourceContentFor(originalSource.url);
  152. if (!text) {
  153. text = (await fetch(
  154. originalSource.url, { loadFromCache: false }
  155. )).content;
  156. }
  157. return {
  158. text,
  159. contentType: isJavaScript(originalSource.url || "") ?
  160. "text/javascript" :
  161. "text/plain"
  162. };
  163. }
  164. function applySourceMap(generatedId, url, code, mappings) {
  165. const generator = new SourceMapGenerator({ file: url });
  166. mappings.forEach(mapping => generator.addMapping(mapping));
  167. generator.setSourceContent(url, code);
  168. const map = SourceMapConsumer(generator.toJSON());
  169. sourceMapRequests.set(generatedId, Promise.resolve(map));
  170. }
  171. const publicInterface = {
  172. getOriginalURLs,
  173. getGeneratedLocation,
  174. getOriginalLocation,
  175. getOriginalSourceText,
  176. enableSourceMaps,
  177. applySourceMap,
  178. clearSourceMaps
  179. };
  180. self.onmessage = function(msg) {
  181. const { id, method, args } = msg.data;
  182. const response = publicInterface[method].apply(undefined, args);
  183. if (response instanceof Promise) {
  184. response.then(val => self.postMessage({ id, response: val }),
  185. err => self.postMessage({ id, error: err }));
  186. } else {
  187. self.postMessage({ id, response });
  188. }
  189. };