generate-sourcemap.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. "use strict";
  2. const os = require("os");
  3. const convertSourceMap = require("convert-source-map");
  4. const SourceMapConsumer = require("source-map").SourceMapConsumer;
  5. const SourceMapGenerator = require("source-map").SourceMapGenerator;
  6. const stableSort = require("stable");
  7. function SourceMapper(src, nodePositions, fragments, inFile, sourceRoot) {
  8. this.generator = new SourceMapGenerator({ sourceRoot: sourceRoot });
  9. this.src = src;
  10. // stableSort does not mutate input array so no need to copy it
  11. this.nodePositions = stableSort(nodePositions, compareLoc);
  12. this.fragments = stableSort(fragments, function(a, b) { return a.start - b.start });
  13. this.inFile = inFile || "source.js";
  14. this.generator.setSourceContent(this.inFile, src);
  15. }
  16. SourceMapper.prototype.calculateMappings = function() {
  17. const self = this;
  18. // These offsets represent the difference in coordinates between a node in the source
  19. // and the corresponding position in the output.
  20. let lineOffset = 0;
  21. let columnOffset = 0;
  22. // Since the column position resets to zero after each newline, we have to keep track
  23. // of the current line that columnOffset refers to in order to know whether to reset it
  24. let currentLine = 0;
  25. let frag = 0;
  26. let pos = 0;
  27. while (pos < self.nodePositions.length) {
  28. while (frag < self.fragments.length &&
  29. compareLoc(self.fragments[frag].loc.start, self.nodePositions[pos]) < 1) {
  30. const fragmentLines = self.fragments[frag].str.split("\n");
  31. const addedNewlines = fragmentLines.length - 1;
  32. const replacedLines = self.fragments[frag].loc.end.line - self.fragments[frag].loc.start.line;
  33. const replacedColumns = self.fragments[frag].loc.end.column - self.fragments[frag].loc.start.column;
  34. // If there were any lines added by the fragment string, the line offset should increase;
  35. // If there were any lines removed by the fragment replacement then the line offset should decrease
  36. lineOffset = lineOffset + addedNewlines - replacedLines;
  37. // The column position needs to reset after each newline. So if the fragment added any
  38. // newlines then the column offset is the difference between the column of the last line of
  39. // the fragment, and the column of the end of the replaced section of the source.
  40. // Otherwise we increment or decrement the column offset just like how the line offset works.
  41. // Note that "replacedColumns" might be negative in some cases (if the beginning of the source
  42. // was further right than the end due to a newline); the math still works out.
  43. columnOffset = fragmentLines.length > 1 ?
  44. fragmentLines[fragmentLines.length - 1].length - self.fragments[frag].loc.end.column :
  45. columnOffset + self.fragments[frag].str.length - replacedColumns;
  46. currentLine = self.fragments[frag].loc.end.line;
  47. // Skip creating mappings for any source nodes that were replaced by this fragment (and are thus
  48. // no longer a part of the output)
  49. while (pos < self.nodePositions.length &&
  50. compareLoc(self.fragments[frag].loc.end, self.nodePositions[pos]) > 0) {
  51. ++pos;
  52. }
  53. ++frag;
  54. }
  55. if (pos < self.nodePositions.length) {
  56. if (currentLine < self.nodePositions[pos].line)
  57. columnOffset = 0;
  58. self.addMapping(self.nodePositions[pos], {
  59. line: self.nodePositions[pos].line + lineOffset,
  60. column: self.nodePositions[pos].column + columnOffset
  61. });
  62. ++pos;
  63. }
  64. }
  65. }
  66. SourceMapper.prototype.addMapping = function(input, output) {
  67. this.generator.addMapping({
  68. source: this.inFile,
  69. original: input,
  70. generated: output
  71. });
  72. }
  73. SourceMapper.prototype.applySourceMap = function (consumer) {
  74. this.generator.applySourceMap(consumer);
  75. }
  76. SourceMapper.prototype.generate = function () {
  77. return this.generator.toString();
  78. }
  79. function compareLoc(a, b) {
  80. return (a.line - b.line) || (a.column - b.column);
  81. }
  82. module.exports = function generateSourcemap(result, src, nodePositions, fragments, mapOpts) {
  83. const existingMap = convertSourceMap.fromSource(src);
  84. src = convertSourceMap.removeMapFileComments(src);
  85. const mapper = new SourceMapper(src, nodePositions, fragments, mapOpts.inFile, mapOpts.sourceRoot);
  86. mapper.calculateMappings();
  87. if (mapOpts.inline) {
  88. if (existingMap)
  89. mapper.applySourceMap(new SourceMapConsumer(existingMap.toObject()));
  90. result.src = convertSourceMap.removeMapFileComments(result.src) +
  91. os.EOL +
  92. convertSourceMap.fromJSON(mapper.generate()).toComment();
  93. } else {
  94. result.map = mapper.generate();
  95. }
  96. }