You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

404 lines
13 KiB

5 years ago
  1. /* -*- Mode: js; js-indent-level: 2; -*- */
  2. /*
  3. * Copyright 2011 Mozilla Foundation and contributors
  4. * Licensed under the New BSD license. See LICENSE or:
  5. * http://opensource.org/licenses/BSD-3-Clause
  6. */
  7. const SourceMapGenerator = require("./source-map-generator").SourceMapGenerator;
  8. const util = require("./util");
  9. // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other
  10. // operating systems these days (capturing the result).
  11. const REGEX_NEWLINE = /(\r?\n)/;
  12. // Newline character code for charCodeAt() comparisons
  13. const NEWLINE_CODE = 10;
  14. // Private symbol for identifying `SourceNode`s when multiple versions of
  15. // the source-map library are loaded. This MUST NOT CHANGE across
  16. // versions!
  17. const isSourceNode = "$$$isSourceNode$$$";
  18. /**
  19. * SourceNodes provide a way to abstract over interpolating/concatenating
  20. * snippets of generated JavaScript source code while maintaining the line and
  21. * column information associated with the original source code.
  22. *
  23. * @param aLine The original line number.
  24. * @param aColumn The original column number.
  25. * @param aSource The original source's filename.
  26. * @param aChunks Optional. An array of strings which are snippets of
  27. * generated JS, or other SourceNodes.
  28. * @param aName The original identifier.
  29. */
  30. class SourceNode {
  31. constructor(aLine, aColumn, aSource, aChunks, aName) {
  32. this.children = [];
  33. this.sourceContents = {};
  34. this.line = aLine == null ? null : aLine;
  35. this.column = aColumn == null ? null : aColumn;
  36. this.source = aSource == null ? null : aSource;
  37. this.name = aName == null ? null : aName;
  38. this[isSourceNode] = true;
  39. if (aChunks != null) this.add(aChunks);
  40. }
  41. /**
  42. * Creates a SourceNode from generated code and a SourceMapConsumer.
  43. *
  44. * @param aGeneratedCode The generated code
  45. * @param aSourceMapConsumer The SourceMap for the generated code
  46. * @param aRelativePath Optional. The path that relative sources in the
  47. * SourceMapConsumer should be relative to.
  48. */
  49. static fromStringWithSourceMap(aGeneratedCode, aSourceMapConsumer, aRelativePath) {
  50. // The SourceNode we want to fill with the generated code
  51. // and the SourceMap
  52. const node = new SourceNode();
  53. // All even indices of this array are one line of the generated code,
  54. // while all odd indices are the newlines between two adjacent lines
  55. // (since `REGEX_NEWLINE` captures its match).
  56. // Processed fragments are accessed by calling `shiftNextLine`.
  57. const remainingLines = aGeneratedCode.split(REGEX_NEWLINE);
  58. let remainingLinesIndex = 0;
  59. const shiftNextLine = function() {
  60. const lineContents = getNextLine();
  61. // The last line of a file might not have a newline.
  62. const newLine = getNextLine() || "";
  63. return lineContents + newLine;
  64. function getNextLine() {
  65. return remainingLinesIndex < remainingLines.length ?
  66. remainingLines[remainingLinesIndex++] : undefined;
  67. }
  68. };
  69. // We need to remember the position of "remainingLines"
  70. let lastGeneratedLine = 1, lastGeneratedColumn = 0;
  71. // The generate SourceNodes we need a code range.
  72. // To extract it current and last mapping is used.
  73. // Here we store the last mapping.
  74. let lastMapping = null;
  75. let nextLine;
  76. aSourceMapConsumer.eachMapping(function(mapping) {
  77. if (lastMapping !== null) {
  78. // We add the code from "lastMapping" to "mapping":
  79. // First check if there is a new line in between.
  80. if (lastGeneratedLine < mapping.generatedLine) {
  81. // Associate first line with "lastMapping"
  82. addMappingWithCode(lastMapping, shiftNextLine());
  83. lastGeneratedLine++;
  84. lastGeneratedColumn = 0;
  85. // The remaining code is added without mapping
  86. } else {
  87. // There is no new line in between.
  88. // Associate the code between "lastGeneratedColumn" and
  89. // "mapping.generatedColumn" with "lastMapping"
  90. nextLine = remainingLines[remainingLinesIndex] || "";
  91. const code = nextLine.substr(0, mapping.generatedColumn -
  92. lastGeneratedColumn);
  93. remainingLines[remainingLinesIndex] = nextLine.substr(mapping.generatedColumn -
  94. lastGeneratedColumn);
  95. lastGeneratedColumn = mapping.generatedColumn;
  96. addMappingWithCode(lastMapping, code);
  97. // No more remaining code, continue
  98. lastMapping = mapping;
  99. return;
  100. }
  101. }
  102. // We add the generated code until the first mapping
  103. // to the SourceNode without any mapping.
  104. // Each line is added as separate string.
  105. while (lastGeneratedLine < mapping.generatedLine) {
  106. node.add(shiftNextLine());
  107. lastGeneratedLine++;
  108. }
  109. if (lastGeneratedColumn < mapping.generatedColumn) {
  110. nextLine = remainingLines[remainingLinesIndex] || "";
  111. node.add(nextLine.substr(0, mapping.generatedColumn));
  112. remainingLines[remainingLinesIndex] = nextLine.substr(mapping.generatedColumn);
  113. lastGeneratedColumn = mapping.generatedColumn;
  114. }
  115. lastMapping = mapping;
  116. }, this);
  117. // We have processed all mappings.
  118. if (remainingLinesIndex < remainingLines.length) {
  119. if (lastMapping) {
  120. // Associate the remaining code in the current line with "lastMapping"
  121. addMappingWithCode(lastMapping, shiftNextLine());
  122. }
  123. // and add the remaining lines without any mapping
  124. node.add(remainingLines.splice(remainingLinesIndex).join(""));
  125. }
  126. // Copy sourcesContent into SourceNode
  127. aSourceMapConsumer.sources.forEach(function(sourceFile) {
  128. const content = aSourceMapConsumer.sourceContentFor(sourceFile);
  129. if (content != null) {
  130. if (aRelativePath != null) {
  131. sourceFile = util.join(aRelativePath, sourceFile);
  132. }
  133. node.setSourceContent(sourceFile, content);
  134. }
  135. });
  136. return node;
  137. function addMappingWithCode(mapping, code) {
  138. if (mapping === null || mapping.source === undefined) {
  139. node.add(code);
  140. } else {
  141. const source = aRelativePath
  142. ? util.join(aRelativePath, mapping.source)
  143. : mapping.source;
  144. node.add(new SourceNode(mapping.originalLine,
  145. mapping.originalColumn,
  146. source,
  147. code,
  148. mapping.name));
  149. }
  150. }
  151. }
  152. /**
  153. * Add a chunk of generated JS to this source node.
  154. *
  155. * @param aChunk A string snippet of generated JS code, another instance of
  156. * SourceNode, or an array where each member is one of those things.
  157. */
  158. add(aChunk) {
  159. if (Array.isArray(aChunk)) {
  160. aChunk.forEach(function(chunk) {
  161. this.add(chunk);
  162. }, this);
  163. } else if (aChunk[isSourceNode] || typeof aChunk === "string") {
  164. if (aChunk) {
  165. this.children.push(aChunk);
  166. }
  167. } else {
  168. throw new TypeError(
  169. "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + aChunk
  170. );
  171. }
  172. return this;
  173. }
  174. /**
  175. * Add a chunk of generated JS to the beginning of this source node.
  176. *
  177. * @param aChunk A string snippet of generated JS code, another instance of
  178. * SourceNode, or an array where each member is one of those things.
  179. */
  180. prepend(aChunk) {
  181. if (Array.isArray(aChunk)) {
  182. for (let i = aChunk.length - 1; i >= 0; i--) {
  183. this.prepend(aChunk[i]);
  184. }
  185. } else if (aChunk[isSourceNode] || typeof aChunk === "string") {
  186. this.children.unshift(aChunk);
  187. } else {
  188. throw new TypeError(
  189. "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + aChunk
  190. );
  191. }
  192. return this;
  193. }
  194. /**
  195. * Walk over the tree of JS snippets in this node and its children. The
  196. * walking function is called once for each snippet of JS and is passed that
  197. * snippet and the its original associated source's line/column location.
  198. *
  199. * @param aFn The traversal function.
  200. */
  201. walk(aFn) {
  202. let chunk;
  203. for (let i = 0, len = this.children.length; i < len; i++) {
  204. chunk = this.children[i];
  205. if (chunk[isSourceNode]) {
  206. chunk.walk(aFn);
  207. } else if (chunk !== "") {
  208. aFn(chunk, { source: this.source,
  209. line: this.line,
  210. column: this.column,
  211. name: this.name });
  212. }
  213. }
  214. }
  215. /**
  216. * Like `String.prototype.join` except for SourceNodes. Inserts `aStr` between
  217. * each of `this.children`.
  218. *
  219. * @param aSep The separator.
  220. */
  221. join(aSep) {
  222. let newChildren;
  223. let i;
  224. const len = this.children.length;
  225. if (len > 0) {
  226. newChildren = [];
  227. for (i = 0; i < len - 1; i++) {
  228. newChildren.push(this.children[i]);
  229. newChildren.push(aSep);
  230. }
  231. newChildren.push(this.children[i]);
  232. this.children = newChildren;
  233. }
  234. return this;
  235. }
  236. /**
  237. * Call String.prototype.replace on the very right-most source snippet. Useful
  238. * for trimming whitespace from the end of a source node, etc.
  239. *
  240. * @param aPattern The pattern to replace.
  241. * @param aReplacement The thing to replace the pattern with.
  242. */
  243. replaceRight(aPattern, aReplacement) {
  244. const lastChild = this.children[this.children.length - 1];
  245. if (lastChild[isSourceNode]) {
  246. lastChild.replaceRight(aPattern, aReplacement);
  247. } else if (typeof lastChild === "string") {
  248. this.children[this.children.length - 1] = lastChild.replace(aPattern, aReplacement);
  249. } else {
  250. this.children.push("".replace(aPattern, aReplacement));
  251. }
  252. return this;
  253. }
  254. /**
  255. * Set the source content for a source file. This will be added to the SourceMapGenerator
  256. * in the sourcesContent field.
  257. *
  258. * @param aSourceFile The filename of the source file
  259. * @param aSourceContent The content of the source file
  260. */
  261. setSourceContent(aSourceFile, aSourceContent) {
  262. this.sourceContents[util.toSetString(aSourceFile)] = aSourceContent;
  263. }
  264. /**
  265. * Walk over the tree of SourceNodes. The walking function is called for each
  266. * source file content and is passed the filename and source content.
  267. *
  268. * @param aFn The traversal function.
  269. */
  270. walkSourceContents(aFn) {
  271. for (let i = 0, len = this.children.length; i < len; i++) {
  272. if (this.children[i][isSourceNode]) {
  273. this.children[i].walkSourceContents(aFn);
  274. }
  275. }
  276. const sources = Object.keys(this.sourceContents);
  277. for (let i = 0, len = sources.length; i < len; i++) {
  278. aFn(util.fromSetString(sources[i]), this.sourceContents[sources[i]]);
  279. }
  280. }
  281. /**
  282. * Return the string representation of this source node. Walks over the tree
  283. * and concatenates all the various snippets together to one string.
  284. */
  285. toString() {
  286. let str = "";
  287. this.walk(function(chunk) {
  288. str += chunk;
  289. });
  290. return str;
  291. }
  292. /**
  293. * Returns the string representation of this source node along with a source
  294. * map.
  295. */
  296. toStringWithSourceMap(aArgs) {
  297. const generated = {
  298. code: "",
  299. line: 1,
  300. column: 0
  301. };
  302. const map = new SourceMapGenerator(aArgs);
  303. let sourceMappingActive = false;
  304. let lastOriginalSource = null;
  305. let lastOriginalLine = null;
  306. let lastOriginalColumn = null;
  307. let lastOriginalName = null;
  308. this.walk(function(chunk, original) {
  309. generated.code += chunk;
  310. if (original.source !== null
  311. && original.line !== null
  312. && original.column !== null) {
  313. if (lastOriginalSource !== original.source
  314. || lastOriginalLine !== original.line
  315. || lastOriginalColumn !== original.column
  316. || lastOriginalName !== original.name) {
  317. map.addMapping({
  318. source: original.source,
  319. original: {
  320. line: original.line,
  321. column: original.column
  322. },
  323. generated: {
  324. line: generated.line,
  325. column: generated.column
  326. },
  327. name: original.name
  328. });
  329. }
  330. lastOriginalSource = original.source;
  331. lastOriginalLine = original.line;
  332. lastOriginalColumn = original.column;
  333. lastOriginalName = original.name;
  334. sourceMappingActive = true;
  335. } else if (sourceMappingActive) {
  336. map.addMapping({
  337. generated: {
  338. line: generated.line,
  339. column: generated.column
  340. }
  341. });
  342. lastOriginalSource = null;
  343. sourceMappingActive = false;
  344. }
  345. for (let idx = 0, length = chunk.length; idx < length; idx++) {
  346. if (chunk.charCodeAt(idx) === NEWLINE_CODE) {
  347. generated.line++;
  348. generated.column = 0;
  349. // Mappings end at eol
  350. if (idx + 1 === length) {
  351. lastOriginalSource = null;
  352. sourceMappingActive = false;
  353. } else if (sourceMappingActive) {
  354. map.addMapping({
  355. source: original.source,
  356. original: {
  357. line: original.line,
  358. column: original.column
  359. },
  360. generated: {
  361. line: generated.line,
  362. column: generated.column
  363. },
  364. name: original.name
  365. });
  366. }
  367. } else {
  368. generated.column++;
  369. }
  370. }
  371. });
  372. this.walkSourceContents(function(sourceFile, sourceContent) {
  373. map.setSourceContent(sourceFile, sourceContent);
  374. });
  375. return { code: generated.code, map };
  376. }
  377. }
  378. exports.SourceNode = SourceNode;