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.

413 lines
14 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 base64VLQ = require("./base64-vlq");
  8. const util = require("./util");
  9. const ArraySet = require("./array-set").ArraySet;
  10. const MappingList = require("./mapping-list").MappingList;
  11. /**
  12. * An instance of the SourceMapGenerator represents a source map which is
  13. * being built incrementally. You may pass an object with the following
  14. * properties:
  15. *
  16. * - file: The filename of the generated source.
  17. * - sourceRoot: A root for all relative URLs in this source map.
  18. */
  19. class SourceMapGenerator {
  20. constructor(aArgs) {
  21. if (!aArgs) {
  22. aArgs = {};
  23. }
  24. this._file = util.getArg(aArgs, "file", null);
  25. this._sourceRoot = util.getArg(aArgs, "sourceRoot", null);
  26. this._skipValidation = util.getArg(aArgs, "skipValidation", false);
  27. this._sources = new ArraySet();
  28. this._names = new ArraySet();
  29. this._mappings = new MappingList();
  30. this._sourcesContents = null;
  31. }
  32. /**
  33. * Creates a new SourceMapGenerator based on a SourceMapConsumer
  34. *
  35. * @param aSourceMapConsumer The SourceMap.
  36. */
  37. static fromSourceMap(aSourceMapConsumer) {
  38. const sourceRoot = aSourceMapConsumer.sourceRoot;
  39. const generator = new SourceMapGenerator({
  40. file: aSourceMapConsumer.file,
  41. sourceRoot
  42. });
  43. aSourceMapConsumer.eachMapping(function(mapping) {
  44. const newMapping = {
  45. generated: {
  46. line: mapping.generatedLine,
  47. column: mapping.generatedColumn
  48. }
  49. };
  50. if (mapping.source != null) {
  51. newMapping.source = mapping.source;
  52. if (sourceRoot != null) {
  53. newMapping.source = util.relative(sourceRoot, newMapping.source);
  54. }
  55. newMapping.original = {
  56. line: mapping.originalLine,
  57. column: mapping.originalColumn
  58. };
  59. if (mapping.name != null) {
  60. newMapping.name = mapping.name;
  61. }
  62. }
  63. generator.addMapping(newMapping);
  64. });
  65. aSourceMapConsumer.sources.forEach(function(sourceFile) {
  66. let sourceRelative = sourceFile;
  67. if (sourceRoot !== null) {
  68. sourceRelative = util.relative(sourceRoot, sourceFile);
  69. }
  70. if (!generator._sources.has(sourceRelative)) {
  71. generator._sources.add(sourceRelative);
  72. }
  73. const content = aSourceMapConsumer.sourceContentFor(sourceFile);
  74. if (content != null) {
  75. generator.setSourceContent(sourceFile, content);
  76. }
  77. });
  78. return generator;
  79. }
  80. /**
  81. * Add a single mapping from original source line and column to the generated
  82. * source's line and column for this source map being created. The mapping
  83. * object should have the following properties:
  84. *
  85. * - generated: An object with the generated line and column positions.
  86. * - original: An object with the original line and column positions.
  87. * - source: The original source file (relative to the sourceRoot).
  88. * - name: An optional original token name for this mapping.
  89. */
  90. addMapping(aArgs) {
  91. const generated = util.getArg(aArgs, "generated");
  92. const original = util.getArg(aArgs, "original", null);
  93. let source = util.getArg(aArgs, "source", null);
  94. let name = util.getArg(aArgs, "name", null);
  95. if (!this._skipValidation) {
  96. this._validateMapping(generated, original, source, name);
  97. }
  98. if (source != null) {
  99. source = String(source);
  100. if (!this._sources.has(source)) {
  101. this._sources.add(source);
  102. }
  103. }
  104. if (name != null) {
  105. name = String(name);
  106. if (!this._names.has(name)) {
  107. this._names.add(name);
  108. }
  109. }
  110. this._mappings.add({
  111. generatedLine: generated.line,
  112. generatedColumn: generated.column,
  113. originalLine: original != null && original.line,
  114. originalColumn: original != null && original.column,
  115. source,
  116. name
  117. });
  118. }
  119. /**
  120. * Set the source content for a source file.
  121. */
  122. setSourceContent(aSourceFile, aSourceContent) {
  123. let source = aSourceFile;
  124. if (this._sourceRoot != null) {
  125. source = util.relative(this._sourceRoot, source);
  126. }
  127. if (aSourceContent != null) {
  128. // Add the source content to the _sourcesContents map.
  129. // Create a new _sourcesContents map if the property is null.
  130. if (!this._sourcesContents) {
  131. this._sourcesContents = Object.create(null);
  132. }
  133. this._sourcesContents[util.toSetString(source)] = aSourceContent;
  134. } else if (this._sourcesContents) {
  135. // Remove the source file from the _sourcesContents map.
  136. // If the _sourcesContents map is empty, set the property to null.
  137. delete this._sourcesContents[util.toSetString(source)];
  138. if (Object.keys(this._sourcesContents).length === 0) {
  139. this._sourcesContents = null;
  140. }
  141. }
  142. }
  143. /**
  144. * Applies the mappings of a sub-source-map for a specific source file to the
  145. * source map being generated. Each mapping to the supplied source file is
  146. * rewritten using the supplied source map. Note: The resolution for the
  147. * resulting mappings is the minimium of this map and the supplied map.
  148. *
  149. * @param aSourceMapConsumer The source map to be applied.
  150. * @param aSourceFile Optional. The filename of the source file.
  151. * If omitted, SourceMapConsumer's file property will be used.
  152. * @param aSourceMapPath Optional. The dirname of the path to the source map
  153. * to be applied. If relative, it is relative to the SourceMapConsumer.
  154. * This parameter is needed when the two source maps aren't in the same
  155. * directory, and the source map to be applied contains relative source
  156. * paths. If so, those relative source paths need to be rewritten
  157. * relative to the SourceMapGenerator.
  158. */
  159. applySourceMap(aSourceMapConsumer, aSourceFile, aSourceMapPath) {
  160. let sourceFile = aSourceFile;
  161. // If aSourceFile is omitted, we will use the file property of the SourceMap
  162. if (aSourceFile == null) {
  163. if (aSourceMapConsumer.file == null) {
  164. throw new Error(
  165. "SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, " +
  166. 'or the source map\'s "file" property. Both were omitted.'
  167. );
  168. }
  169. sourceFile = aSourceMapConsumer.file;
  170. }
  171. const sourceRoot = this._sourceRoot;
  172. // Make "sourceFile" relative if an absolute Url is passed.
  173. if (sourceRoot != null) {
  174. sourceFile = util.relative(sourceRoot, sourceFile);
  175. }
  176. // Applying the SourceMap can add and remove items from the sources and
  177. // the names array.
  178. const newSources = this._mappings.toArray().length > 0
  179. ? new ArraySet()
  180. : this._sources;
  181. const newNames = new ArraySet();
  182. // Find mappings for the "sourceFile"
  183. this._mappings.unsortedForEach(function(mapping) {
  184. if (mapping.source === sourceFile && mapping.originalLine != null) {
  185. // Check if it can be mapped by the source map, then update the mapping.
  186. const original = aSourceMapConsumer.originalPositionFor({
  187. line: mapping.originalLine,
  188. column: mapping.originalColumn
  189. });
  190. if (original.source != null) {
  191. // Copy mapping
  192. mapping.source = original.source;
  193. if (aSourceMapPath != null) {
  194. mapping.source = util.join(aSourceMapPath, mapping.source);
  195. }
  196. if (sourceRoot != null) {
  197. mapping.source = util.relative(sourceRoot, mapping.source);
  198. }
  199. mapping.originalLine = original.line;
  200. mapping.originalColumn = original.column;
  201. if (original.name != null) {
  202. mapping.name = original.name;
  203. }
  204. }
  205. }
  206. const source = mapping.source;
  207. if (source != null && !newSources.has(source)) {
  208. newSources.add(source);
  209. }
  210. const name = mapping.name;
  211. if (name != null && !newNames.has(name)) {
  212. newNames.add(name);
  213. }
  214. }, this);
  215. this._sources = newSources;
  216. this._names = newNames;
  217. // Copy sourcesContents of applied map.
  218. aSourceMapConsumer.sources.forEach(function(srcFile) {
  219. const content = aSourceMapConsumer.sourceContentFor(srcFile);
  220. if (content != null) {
  221. if (aSourceMapPath != null) {
  222. srcFile = util.join(aSourceMapPath, srcFile);
  223. }
  224. if (sourceRoot != null) {
  225. srcFile = util.relative(sourceRoot, srcFile);
  226. }
  227. this.setSourceContent(srcFile, content);
  228. }
  229. }, this);
  230. }
  231. /**
  232. * A mapping can have one of the three levels of data:
  233. *
  234. * 1. Just the generated position.
  235. * 2. The Generated position, original position, and original source.
  236. * 3. Generated and original position, original source, as well as a name
  237. * token.
  238. *
  239. * To maintain consistency, we validate that any new mapping being added falls
  240. * in to one of these categories.
  241. */
  242. _validateMapping(aGenerated, aOriginal, aSource, aName) {
  243. // When aOriginal is truthy but has empty values for .line and .column,
  244. // it is most likely a programmer error. In this case we throw a very
  245. // specific error message to try to guide them the right way.
  246. // For example: https://github.com/Polymer/polymer-bundler/pull/519
  247. if (aOriginal && typeof aOriginal.line !== "number" && typeof aOriginal.column !== "number") {
  248. throw new Error(
  249. "original.line and original.column are not numbers -- you probably meant to omit " +
  250. "the original mapping entirely and only map the generated position. If so, pass " +
  251. "null for the original mapping instead of an object with empty or null values."
  252. );
  253. }
  254. if (aGenerated && "line" in aGenerated && "column" in aGenerated
  255. && aGenerated.line > 0 && aGenerated.column >= 0
  256. && !aOriginal && !aSource && !aName) {
  257. // Case 1.
  258. } else if (aGenerated && "line" in aGenerated && "column" in aGenerated
  259. && aOriginal && "line" in aOriginal && "column" in aOriginal
  260. && aGenerated.line > 0 && aGenerated.column >= 0
  261. && aOriginal.line > 0 && aOriginal.column >= 0
  262. && aSource) {
  263. // Cases 2 and 3.
  264. } else {
  265. throw new Error("Invalid mapping: " + JSON.stringify({
  266. generated: aGenerated,
  267. source: aSource,
  268. original: aOriginal,
  269. name: aName
  270. }));
  271. }
  272. }
  273. /**
  274. * Serialize the accumulated mappings in to the stream of base 64 VLQs
  275. * specified by the source map format.
  276. */
  277. _serializeMappings() {
  278. let previousGeneratedColumn = 0;
  279. let previousGeneratedLine = 1;
  280. let previousOriginalColumn = 0;
  281. let previousOriginalLine = 0;
  282. let previousName = 0;
  283. let previousSource = 0;
  284. let result = "";
  285. let next;
  286. let mapping;
  287. let nameIdx;
  288. let sourceIdx;
  289. const mappings = this._mappings.toArray();
  290. for (let i = 0, len = mappings.length; i < len; i++) {
  291. mapping = mappings[i];
  292. next = "";
  293. if (mapping.generatedLine !== previousGeneratedLine) {
  294. previousGeneratedColumn = 0;
  295. while (mapping.generatedLine !== previousGeneratedLine) {
  296. next += ";";
  297. previousGeneratedLine++;
  298. }
  299. } else if (i > 0) {
  300. if (!util.compareByGeneratedPositionsInflated(mapping, mappings[i - 1])) {
  301. continue;
  302. }
  303. next += ",";
  304. }
  305. next += base64VLQ.encode(mapping.generatedColumn
  306. - previousGeneratedColumn);
  307. previousGeneratedColumn = mapping.generatedColumn;
  308. if (mapping.source != null) {
  309. sourceIdx = this._sources.indexOf(mapping.source);
  310. next += base64VLQ.encode(sourceIdx - previousSource);
  311. previousSource = sourceIdx;
  312. // lines are stored 0-based in SourceMap spec version 3
  313. next += base64VLQ.encode(mapping.originalLine - 1
  314. - previousOriginalLine);
  315. previousOriginalLine = mapping.originalLine - 1;
  316. next += base64VLQ.encode(mapping.originalColumn
  317. - previousOriginalColumn);
  318. previousOriginalColumn = mapping.originalColumn;
  319. if (mapping.name != null) {
  320. nameIdx = this._names.indexOf(mapping.name);
  321. next += base64VLQ.encode(nameIdx - previousName);
  322. previousName = nameIdx;
  323. }
  324. }
  325. result += next;
  326. }
  327. return result;
  328. }
  329. _generateSourcesContent(aSources, aSourceRoot) {
  330. return aSources.map(function(source) {
  331. if (!this._sourcesContents) {
  332. return null;
  333. }
  334. if (aSourceRoot != null) {
  335. source = util.relative(aSourceRoot, source);
  336. }
  337. const key = util.toSetString(source);
  338. return Object.prototype.hasOwnProperty.call(this._sourcesContents, key)
  339. ? this._sourcesContents[key]
  340. : null;
  341. }, this);
  342. }
  343. /**
  344. * Externalize the source map.
  345. */
  346. toJSON() {
  347. const map = {
  348. version: this._version,
  349. sources: this._sources.toArray(),
  350. names: this._names.toArray(),
  351. mappings: this._serializeMappings()
  352. };
  353. if (this._file != null) {
  354. map.file = this._file;
  355. }
  356. if (this._sourceRoot != null) {
  357. map.sourceRoot = this._sourceRoot;
  358. }
  359. if (this._sourcesContents) {
  360. map.sourcesContent = this._generateSourcesContent(map.sources, map.sourceRoot);
  361. }
  362. return map;
  363. }
  364. /**
  365. * Render the source map being generated to a string.
  366. */
  367. toString() {
  368. return JSON.stringify(this.toJSON());
  369. }
  370. }
  371. SourceMapGenerator.prototype._version = 3;
  372. exports.SourceMapGenerator = SourceMapGenerator;