source-node.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  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(
  50. aGeneratedCode,
  51. aSourceMapConsumer,
  52. aRelativePath
  53. ) {
  54. // The SourceNode we want to fill with the generated code
  55. // and the SourceMap
  56. const node = new SourceNode();
  57. // All even indices of this array are one line of the generated code,
  58. // while all odd indices are the newlines between two adjacent lines
  59. // (since `REGEX_NEWLINE` captures its match).
  60. // Processed fragments are accessed by calling `shiftNextLine`.
  61. const remainingLines = aGeneratedCode.split(REGEX_NEWLINE);
  62. let remainingLinesIndex = 0;
  63. const shiftNextLine = function () {
  64. const lineContents = getNextLine();
  65. // The last line of a file might not have a newline.
  66. const newLine = getNextLine() || "";
  67. return lineContents + newLine;
  68. function getNextLine() {
  69. return remainingLinesIndex < remainingLines.length
  70. ? remainingLines[remainingLinesIndex++]
  71. : undefined;
  72. }
  73. };
  74. // We need to remember the position of "remainingLines"
  75. let lastGeneratedLine = 1,
  76. lastGeneratedColumn = 0;
  77. // The generate SourceNodes we need a code range.
  78. // To extract it current and last mapping is used.
  79. // Here we store the last mapping.
  80. let lastMapping = null;
  81. let nextLine;
  82. aSourceMapConsumer.eachMapping(function (mapping) {
  83. if (lastMapping !== null) {
  84. // We add the code from "lastMapping" to "mapping":
  85. // First check if there is a new line in between.
  86. if (lastGeneratedLine < mapping.generatedLine) {
  87. // Associate first line with "lastMapping"
  88. addMappingWithCode(lastMapping, shiftNextLine());
  89. lastGeneratedLine++;
  90. lastGeneratedColumn = 0;
  91. // The remaining code is added without mapping
  92. } else {
  93. // There is no new line in between.
  94. // Associate the code between "lastGeneratedColumn" and
  95. // "mapping.generatedColumn" with "lastMapping"
  96. nextLine = remainingLines[remainingLinesIndex] || "";
  97. const code = nextLine.substr(
  98. 0,
  99. mapping.generatedColumn - lastGeneratedColumn
  100. );
  101. remainingLines[remainingLinesIndex] = nextLine.substr(
  102. mapping.generatedColumn - lastGeneratedColumn
  103. );
  104. lastGeneratedColumn = mapping.generatedColumn;
  105. addMappingWithCode(lastMapping, code);
  106. // No more remaining code, continue
  107. lastMapping = mapping;
  108. return;
  109. }
  110. }
  111. // We add the generated code until the first mapping
  112. // to the SourceNode without any mapping.
  113. // Each line is added as separate string.
  114. while (lastGeneratedLine < mapping.generatedLine) {
  115. node.add(shiftNextLine());
  116. lastGeneratedLine++;
  117. }
  118. if (lastGeneratedColumn < mapping.generatedColumn) {
  119. nextLine = remainingLines[remainingLinesIndex] || "";
  120. node.add(nextLine.substr(0, mapping.generatedColumn));
  121. remainingLines[remainingLinesIndex] = nextLine.substr(
  122. mapping.generatedColumn
  123. );
  124. lastGeneratedColumn = mapping.generatedColumn;
  125. }
  126. lastMapping = mapping;
  127. }, this);
  128. // We have processed all mappings.
  129. if (remainingLinesIndex < remainingLines.length) {
  130. if (lastMapping) {
  131. // Associate the remaining code in the current line with "lastMapping"
  132. addMappingWithCode(lastMapping, shiftNextLine());
  133. }
  134. // and add the remaining lines without any mapping
  135. node.add(remainingLines.splice(remainingLinesIndex).join(""));
  136. }
  137. // Copy sourcesContent into SourceNode
  138. aSourceMapConsumer.sources.forEach(function (sourceFile) {
  139. const content = aSourceMapConsumer.sourceContentFor(sourceFile);
  140. if (content != null) {
  141. if (aRelativePath != null) {
  142. sourceFile = util.join(aRelativePath, sourceFile);
  143. }
  144. node.setSourceContent(sourceFile, content);
  145. }
  146. });
  147. return node;
  148. function addMappingWithCode(mapping, code) {
  149. if (mapping === null || mapping.source === undefined) {
  150. node.add(code);
  151. } else {
  152. const source = aRelativePath
  153. ? util.join(aRelativePath, mapping.source)
  154. : mapping.source;
  155. node.add(
  156. new SourceNode(
  157. mapping.originalLine,
  158. mapping.originalColumn,
  159. source,
  160. code,
  161. mapping.name
  162. )
  163. );
  164. }
  165. }
  166. }
  167. /**
  168. * Add a chunk of generated JS to this source node.
  169. *
  170. * @param aChunk A string snippet of generated JS code, another instance of
  171. * SourceNode, or an array where each member is one of those things.
  172. */
  173. add(aChunk) {
  174. if (Array.isArray(aChunk)) {
  175. aChunk.forEach(function (chunk) {
  176. this.add(chunk);
  177. }, this);
  178. } else if (aChunk[isSourceNode] || typeof aChunk === "string") {
  179. if (aChunk) {
  180. this.children.push(aChunk);
  181. }
  182. } else {
  183. throw new TypeError(
  184. "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " +
  185. aChunk
  186. );
  187. }
  188. return this;
  189. }
  190. /**
  191. * Add a chunk of generated JS to the beginning of this source node.
  192. *
  193. * @param aChunk A string snippet of generated JS code, another instance of
  194. * SourceNode, or an array where each member is one of those things.
  195. */
  196. prepend(aChunk) {
  197. if (Array.isArray(aChunk)) {
  198. for (let i = aChunk.length - 1; i >= 0; i--) {
  199. this.prepend(aChunk[i]);
  200. }
  201. } else if (aChunk[isSourceNode] || typeof aChunk === "string") {
  202. this.children.unshift(aChunk);
  203. } else {
  204. throw new TypeError(
  205. "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " +
  206. aChunk
  207. );
  208. }
  209. return this;
  210. }
  211. /**
  212. * Walk over the tree of JS snippets in this node and its children. The
  213. * walking function is called once for each snippet of JS and is passed that
  214. * snippet and the its original associated source's line/column location.
  215. *
  216. * @param aFn The traversal function.
  217. */
  218. walk(aFn) {
  219. let chunk;
  220. for (let i = 0, len = this.children.length; i < len; i++) {
  221. chunk = this.children[i];
  222. if (chunk[isSourceNode]) {
  223. chunk.walk(aFn);
  224. } else if (chunk !== "") {
  225. aFn(chunk, {
  226. source: this.source,
  227. line: this.line,
  228. column: this.column,
  229. name: this.name,
  230. });
  231. }
  232. }
  233. }
  234. /**
  235. * Like `String.prototype.join` except for SourceNodes. Inserts `aStr` between
  236. * each of `this.children`.
  237. *
  238. * @param aSep The separator.
  239. */
  240. join(aSep) {
  241. let newChildren;
  242. let i;
  243. const len = this.children.length;
  244. if (len > 0) {
  245. newChildren = [];
  246. for (i = 0; i < len - 1; i++) {
  247. newChildren.push(this.children[i]);
  248. newChildren.push(aSep);
  249. }
  250. newChildren.push(this.children[i]);
  251. this.children = newChildren;
  252. }
  253. return this;
  254. }
  255. /**
  256. * Call String.prototype.replace on the very right-most source snippet. Useful
  257. * for trimming whitespace from the end of a source node, etc.
  258. *
  259. * @param aPattern The pattern to replace.
  260. * @param aReplacement The thing to replace the pattern with.
  261. */
  262. replaceRight(aPattern, aReplacement) {
  263. const lastChild = this.children[this.children.length - 1];
  264. if (lastChild[isSourceNode]) {
  265. lastChild.replaceRight(aPattern, aReplacement);
  266. } else if (typeof lastChild === "string") {
  267. this.children[this.children.length - 1] = lastChild.replace(
  268. aPattern,
  269. aReplacement
  270. );
  271. } else {
  272. this.children.push("".replace(aPattern, aReplacement));
  273. }
  274. return this;
  275. }
  276. /**
  277. * Set the source content for a source file. This will be added to the SourceMapGenerator
  278. * in the sourcesContent field.
  279. *
  280. * @param aSourceFile The filename of the source file
  281. * @param aSourceContent The content of the source file
  282. */
  283. setSourceContent(aSourceFile, aSourceContent) {
  284. this.sourceContents[util.toSetString(aSourceFile)] = aSourceContent;
  285. }
  286. /**
  287. * Walk over the tree of SourceNodes. The walking function is called for each
  288. * source file content and is passed the filename and source content.
  289. *
  290. * @param aFn The traversal function.
  291. */
  292. walkSourceContents(aFn) {
  293. for (let i = 0, len = this.children.length; i < len; i++) {
  294. if (this.children[i][isSourceNode]) {
  295. this.children[i].walkSourceContents(aFn);
  296. }
  297. }
  298. const sources = Object.keys(this.sourceContents);
  299. for (let i = 0, len = sources.length; i < len; i++) {
  300. aFn(util.fromSetString(sources[i]), this.sourceContents[sources[i]]);
  301. }
  302. }
  303. /**
  304. * Return the string representation of this source node. Walks over the tree
  305. * and concatenates all the various snippets together to one string.
  306. */
  307. toString() {
  308. let str = "";
  309. this.walk(function (chunk) {
  310. str += chunk;
  311. });
  312. return str;
  313. }
  314. /**
  315. * Returns the string representation of this source node along with a source
  316. * map.
  317. */
  318. toStringWithSourceMap(aArgs) {
  319. const generated = {
  320. code: "",
  321. line: 1,
  322. column: 0,
  323. };
  324. const map = new SourceMapGenerator(aArgs);
  325. let sourceMappingActive = false;
  326. let lastOriginalSource = null;
  327. let lastOriginalLine = null;
  328. let lastOriginalColumn = null;
  329. let lastOriginalName = null;
  330. this.walk(function (chunk, original) {
  331. generated.code += chunk;
  332. if (
  333. original.source !== null &&
  334. original.line !== null &&
  335. original.column !== null
  336. ) {
  337. if (
  338. lastOriginalSource !== original.source ||
  339. lastOriginalLine !== original.line ||
  340. lastOriginalColumn !== original.column ||
  341. lastOriginalName !== original.name
  342. ) {
  343. map.addMapping({
  344. source: original.source,
  345. original: {
  346. line: original.line,
  347. column: original.column,
  348. },
  349. generated: {
  350. line: generated.line,
  351. column: generated.column,
  352. },
  353. name: original.name,
  354. });
  355. }
  356. lastOriginalSource = original.source;
  357. lastOriginalLine = original.line;
  358. lastOriginalColumn = original.column;
  359. lastOriginalName = original.name;
  360. sourceMappingActive = true;
  361. } else if (sourceMappingActive) {
  362. map.addMapping({
  363. generated: {
  364. line: generated.line,
  365. column: generated.column,
  366. },
  367. });
  368. lastOriginalSource = null;
  369. sourceMappingActive = false;
  370. }
  371. for (let idx = 0, length = chunk.length; idx < length; idx++) {
  372. if (chunk.charCodeAt(idx) === NEWLINE_CODE) {
  373. generated.line++;
  374. generated.column = 0;
  375. // Mappings end at eol
  376. if (idx + 1 === length) {
  377. lastOriginalSource = null;
  378. sourceMappingActive = false;
  379. } else if (sourceMappingActive) {
  380. map.addMapping({
  381. source: original.source,
  382. original: {
  383. line: original.line,
  384. column: original.column,
  385. },
  386. generated: {
  387. line: generated.line,
  388. column: generated.column,
  389. },
  390. name: original.name,
  391. });
  392. }
  393. } else {
  394. generated.column++;
  395. }
  396. }
  397. });
  398. this.walkSourceContents(function (sourceFile, sourceContent) {
  399. map.setSourceContent(sourceFile, sourceContent);
  400. });
  401. return { code: generated.code, map };
  402. }
  403. }
  404. exports.SourceNode = SourceNode;