input-source-map-tracker.js 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. var SourceMapConsumer = require('source-map').SourceMapConsumer;
  2. var fs = require('fs');
  3. var path = require('path');
  4. var http = require('http');
  5. var https = require('https');
  6. var url = require('url');
  7. var override = require('../utils/object.js').override;
  8. var MAP_MARKER = /\/\*# sourceMappingURL=(\S+) \*\//;
  9. var REMOTE_RESOURCE = /^(https?:)?\/\//;
  10. var DATA_URI = /^data:(\S*?)?(;charset=[^;]+)?(;[^,]+?)?,(.+)/;
  11. var unescape = global.unescape;
  12. function InputSourceMapStore(outerContext) {
  13. this.options = outerContext.options;
  14. this.errors = outerContext.errors;
  15. this.warnings = outerContext.warnings;
  16. this.sourceTracker = outerContext.sourceTracker;
  17. this.timeout = this.options.inliner.timeout;
  18. this.requestOptions = this.options.inliner.request;
  19. this.localOnly = outerContext.localOnly;
  20. this.relativeTo = outerContext.options.target || process.cwd();
  21. this.maps = {};
  22. this.sourcesContent = {};
  23. }
  24. function fromString(self, _, whenDone) {
  25. self.trackLoaded(undefined, undefined, self.options.sourceMap);
  26. return whenDone();
  27. }
  28. function fromSource(self, data, whenDone, context) {
  29. var nextAt = 0;
  30. function proceedToNext() {
  31. context.cursor += nextAt + 1;
  32. fromSource(self, data, whenDone, context);
  33. }
  34. while (context.cursor < data.length) {
  35. var fragment = data.substring(context.cursor);
  36. var markerStartMatch = self.sourceTracker.nextStart(fragment) || { index: -1 };
  37. var markerEndMatch = self.sourceTracker.nextEnd(fragment) || { index: -1 };
  38. var mapMatch = MAP_MARKER.exec(fragment) || { index: -1 };
  39. var sourceMapFile = mapMatch[1];
  40. nextAt = data.length;
  41. if (markerStartMatch.index > -1)
  42. nextAt = markerStartMatch.index;
  43. if (markerEndMatch.index > -1 && markerEndMatch.index < nextAt)
  44. nextAt = markerEndMatch.index;
  45. if (mapMatch.index > -1 && mapMatch.index < nextAt)
  46. nextAt = mapMatch.index;
  47. if (nextAt == data.length)
  48. break;
  49. if (nextAt == markerStartMatch.index) {
  50. context.files.push(markerStartMatch.filename);
  51. } else if (nextAt == markerEndMatch.index) {
  52. context.files.pop();
  53. } else if (nextAt == mapMatch.index) {
  54. var isRemote = /^https?:\/\//.test(sourceMapFile) || /^\/\//.test(sourceMapFile);
  55. var isDataUri = DATA_URI.test(sourceMapFile);
  56. if (isRemote) {
  57. return fetchMapFile(self, sourceMapFile, context, proceedToNext);
  58. } else {
  59. var sourceFile = context.files[context.files.length - 1];
  60. var sourceMapPath, sourceMapData;
  61. var sourceDir = sourceFile ? path.dirname(sourceFile) : self.options.relativeTo;
  62. if (isDataUri) {
  63. // source map's path is the same as the source file it comes from
  64. sourceMapPath = path.resolve(self.options.root, sourceFile || '');
  65. sourceMapData = fromDataUri(sourceMapFile);
  66. } else {
  67. sourceMapPath = path.resolve(self.options.root, path.join(sourceDir || '', sourceMapFile));
  68. sourceMapData = fs.readFileSync(sourceMapPath, 'utf-8');
  69. }
  70. self.trackLoaded(sourceFile || undefined, sourceMapPath, sourceMapData);
  71. }
  72. }
  73. context.cursor += nextAt + 1;
  74. }
  75. return whenDone();
  76. }
  77. function fromDataUri(uriString) {
  78. var match = DATA_URI.exec(uriString);
  79. var charset = match[2] ? match[2].split(/[=;]/)[2] : 'us-ascii';
  80. var encoding = match[3] ? match[3].split(';')[1] : 'utf8';
  81. var data = encoding == 'utf8' ? unescape(match[4]) : match[4];
  82. var buffer = new Buffer(data, encoding);
  83. buffer.charset = charset;
  84. return buffer.toString();
  85. }
  86. function fetchMapFile(self, sourceUrl, context, done) {
  87. fetch(self, sourceUrl, function (data) {
  88. self.trackLoaded(context.files[context.files.length - 1] || undefined, sourceUrl, data);
  89. done();
  90. }, function (message) {
  91. context.errors.push('Broken source map at "' + sourceUrl + '" - ' + message);
  92. return done();
  93. });
  94. }
  95. function fetch(self, path, onSuccess, onFailure) {
  96. var protocol = path.indexOf('https') === 0 ? https : http;
  97. var requestOptions = override(url.parse(path), self.requestOptions);
  98. var errorHandled = false;
  99. protocol
  100. .get(requestOptions, function (res) {
  101. if (res.statusCode < 200 || res.statusCode > 299)
  102. return onFailure(res.statusCode);
  103. var chunks = [];
  104. res.on('data', function (chunk) {
  105. chunks.push(chunk.toString());
  106. });
  107. res.on('end', function () {
  108. onSuccess(chunks.join(''));
  109. });
  110. })
  111. .on('error', function (res) {
  112. if (errorHandled)
  113. return;
  114. onFailure(res.message);
  115. errorHandled = true;
  116. })
  117. .on('timeout', function () {
  118. if (errorHandled)
  119. return;
  120. onFailure('timeout');
  121. errorHandled = true;
  122. })
  123. .setTimeout(self.timeout);
  124. }
  125. function originalPositionIn(trackedSource, line, column, token, allowNFallbacks) {
  126. var originalPosition;
  127. var maxRange = token.length;
  128. var position = {
  129. line: line,
  130. column: column + maxRange
  131. };
  132. while (maxRange-- > 0) {
  133. position.column--;
  134. originalPosition = trackedSource.data.originalPositionFor(position);
  135. if (originalPosition)
  136. break;
  137. }
  138. if (originalPosition.line === null && line > 1 && allowNFallbacks > 0)
  139. return originalPositionIn(trackedSource, line - 1, column, token, allowNFallbacks - 1);
  140. if (trackedSource.path && originalPosition.source) {
  141. originalPosition.source = REMOTE_RESOURCE.test(trackedSource.path) ?
  142. url.resolve(trackedSource.path, originalPosition.source) :
  143. path.join(trackedSource.path, originalPosition.source);
  144. originalPosition.sourceResolved = true;
  145. }
  146. return originalPosition;
  147. }
  148. function trackContentSources(self, sourceFile) {
  149. var consumer = self.maps[sourceFile].data;
  150. var isRemote = REMOTE_RESOURCE.test(sourceFile);
  151. var sourcesMapping = {};
  152. consumer.sources.forEach(function (file, index) {
  153. var uniquePath = isRemote ?
  154. url.resolve(path.dirname(sourceFile), file) :
  155. path.relative(self.relativeTo, path.resolve(path.dirname(sourceFile || '.'), file));
  156. sourcesMapping[uniquePath] = consumer.sourcesContent && consumer.sourcesContent[index];
  157. });
  158. self.sourcesContent[sourceFile] = sourcesMapping;
  159. }
  160. function _resolveSources(self, remaining, whenDone) {
  161. function processNext() {
  162. return _resolveSources(self, remaining, whenDone);
  163. }
  164. if (remaining.length === 0)
  165. return whenDone();
  166. var current = remaining.shift();
  167. var sourceFile = current[0];
  168. var originalFile = current[1];
  169. var isRemote = REMOTE_RESOURCE.test(sourceFile);
  170. if (isRemote && self.localOnly) {
  171. self.warnings.push('No callback given to `#minify` method, cannot fetch a remote file from "' + originalFile + '"');
  172. return processNext();
  173. }
  174. if (isRemote) {
  175. fetch(self, originalFile, function (data) {
  176. self.sourcesContent[sourceFile][originalFile] = data;
  177. processNext();
  178. }, function (message) {
  179. self.warnings.push('Broken original source file at "' + originalFile + '" - ' + message);
  180. processNext();
  181. });
  182. } else {
  183. var fullPath = path.join(self.options.root, originalFile);
  184. if (fs.existsSync(fullPath))
  185. self.sourcesContent[sourceFile][originalFile] = fs.readFileSync(fullPath, 'utf-8');
  186. else
  187. self.warnings.push('Missing original source file at "' + fullPath + '".');
  188. return processNext();
  189. }
  190. }
  191. InputSourceMapStore.prototype.track = function (data, whenDone) {
  192. return typeof this.options.sourceMap == 'string' ?
  193. fromString(this, data, whenDone) :
  194. fromSource(this, data, whenDone, { files: [], cursor: 0, errors: this.errors });
  195. };
  196. InputSourceMapStore.prototype.trackLoaded = function (sourcePath, mapPath, mapData) {
  197. var relativeTo = this.options.explicitTarget ? this.options.target : this.options.root;
  198. var isRemote = REMOTE_RESOURCE.test(sourcePath);
  199. if (mapPath) {
  200. mapPath = isRemote ?
  201. path.dirname(mapPath) :
  202. path.dirname(path.relative(relativeTo, mapPath));
  203. }
  204. this.maps[sourcePath] = {
  205. path: mapPath,
  206. data: new SourceMapConsumer(mapData)
  207. };
  208. trackContentSources(this, sourcePath);
  209. };
  210. InputSourceMapStore.prototype.isTracking = function (source) {
  211. return !!this.maps[source];
  212. };
  213. InputSourceMapStore.prototype.originalPositionFor = function (sourceInfo, token, allowNFallbacks) {
  214. return originalPositionIn(this.maps[sourceInfo.source], sourceInfo.line, sourceInfo.column, token, allowNFallbacks);
  215. };
  216. InputSourceMapStore.prototype.sourcesContentFor = function (contextSource) {
  217. return this.sourcesContent[contextSource];
  218. };
  219. InputSourceMapStore.prototype.resolveSources = function (whenDone) {
  220. var toResolve = [];
  221. for (var sourceFile in this.sourcesContent) {
  222. var contents = this.sourcesContent[sourceFile];
  223. for (var originalFile in contents) {
  224. if (!contents[originalFile])
  225. toResolve.push([sourceFile, originalFile]);
  226. }
  227. }
  228. return _resolveSources(this, toResolve, whenDone);
  229. };
  230. module.exports = InputSourceMapStore;