source-map-generator.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  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 =
  179. this._mappings.toArray().length > 0 ? new ArraySet() : this._sources
  180. const newNames = new ArraySet()
  181. // Find mappings for the "sourceFile"
  182. this._mappings.unsortedForEach(function (mapping) {
  183. if (mapping.source === sourceFile && mapping.originalLine != null) {
  184. // Check if it can be mapped by the source map, then update the mapping.
  185. const original = aSourceMapConsumer.originalPositionFor({
  186. line: mapping.originalLine,
  187. column: mapping.originalColumn,
  188. })
  189. if (original.source != null) {
  190. // Copy mapping
  191. mapping.source = original.source
  192. if (aSourceMapPath != null) {
  193. mapping.source = util.join(aSourceMapPath, mapping.source)
  194. }
  195. if (sourceRoot != null) {
  196. mapping.source = util.relative(sourceRoot, mapping.source)
  197. }
  198. mapping.originalLine = original.line
  199. mapping.originalColumn = original.column
  200. if (original.name != null) {
  201. mapping.name = original.name
  202. }
  203. }
  204. }
  205. const source = mapping.source
  206. if (source != null && !newSources.has(source)) {
  207. newSources.add(source)
  208. }
  209. const name = mapping.name
  210. if (name != null && !newNames.has(name)) {
  211. newNames.add(name)
  212. }
  213. }, this)
  214. this._sources = newSources
  215. this._names = newNames
  216. // Copy sourcesContents of applied map.
  217. aSourceMapConsumer.sources.forEach(function (srcFile) {
  218. const content = aSourceMapConsumer.sourceContentFor(srcFile)
  219. if (content != null) {
  220. if (aSourceMapPath != null) {
  221. srcFile = util.join(aSourceMapPath, srcFile)
  222. }
  223. if (sourceRoot != null) {
  224. srcFile = util.relative(sourceRoot, srcFile)
  225. }
  226. this.setSourceContent(srcFile, content)
  227. }
  228. }, this)
  229. }
  230. /**
  231. * A mapping can have one of the three levels of data:
  232. *
  233. * 1. Just the generated position.
  234. * 2. The Generated position, original position, and original source.
  235. * 3. Generated and original position, original source, as well as a name
  236. * token.
  237. *
  238. * To maintain consistency, we validate that any new mapping being added falls
  239. * in to one of these categories.
  240. */
  241. _validateMapping(aGenerated, aOriginal, aSource, aName) {
  242. // When aOriginal is truthy but has empty values for .line and .column,
  243. // it is most likely a programmer error. In this case we throw a very
  244. // specific error message to try to guide them the right way.
  245. // For example: https://github.com/Polymer/polymer-bundler/pull/519
  246. if (
  247. aOriginal &&
  248. typeof aOriginal.line !== 'number' &&
  249. typeof aOriginal.column !== 'number'
  250. ) {
  251. throw new Error(
  252. 'original.line and original.column are not numbers -- you probably meant to omit ' +
  253. 'the original mapping entirely and only map the generated position. If so, pass ' +
  254. 'null for the original mapping instead of an object with empty or null values.'
  255. )
  256. }
  257. if (
  258. aGenerated &&
  259. 'line' in aGenerated &&
  260. 'column' in aGenerated &&
  261. aGenerated.line > 0 &&
  262. aGenerated.column >= 0 &&
  263. !aOriginal &&
  264. !aSource &&
  265. !aName
  266. ) {
  267. // Case 1.
  268. } else if (
  269. aGenerated &&
  270. 'line' in aGenerated &&
  271. 'column' in aGenerated &&
  272. aOriginal &&
  273. 'line' in aOriginal &&
  274. 'column' in aOriginal &&
  275. aGenerated.line > 0 &&
  276. aGenerated.column >= 0 &&
  277. aOriginal.line > 0 &&
  278. aOriginal.column >= 0 &&
  279. aSource
  280. ) {
  281. // Cases 2 and 3.
  282. } else {
  283. throw new Error(
  284. 'Invalid mapping: ' +
  285. JSON.stringify({
  286. generated: aGenerated,
  287. source: aSource,
  288. original: aOriginal,
  289. name: aName,
  290. })
  291. )
  292. }
  293. }
  294. /**
  295. * Serialize the accumulated mappings in to the stream of base 64 VLQs
  296. * specified by the source map format.
  297. */
  298. _serializeMappings() {
  299. let previousGeneratedColumn = 0
  300. let previousGeneratedLine = 1
  301. let previousOriginalColumn = 0
  302. let previousOriginalLine = 0
  303. let previousName = 0
  304. let previousSource = 0
  305. let result = ''
  306. let next
  307. let mapping
  308. let nameIdx
  309. let sourceIdx
  310. const mappings = this._mappings.toArray()
  311. for (let i = 0, len = mappings.length; i < len; i++) {
  312. mapping = mappings[i]
  313. next = ''
  314. if (mapping.generatedLine !== previousGeneratedLine) {
  315. previousGeneratedColumn = 0
  316. while (mapping.generatedLine !== previousGeneratedLine) {
  317. next += ';'
  318. previousGeneratedLine++
  319. }
  320. } else if (i > 0) {
  321. if (
  322. !util.compareByGeneratedPositionsInflated(mapping, mappings[i - 1])
  323. ) {
  324. continue
  325. }
  326. next += ','
  327. }
  328. next += base64VLQ.encode(
  329. mapping.generatedColumn - previousGeneratedColumn
  330. )
  331. previousGeneratedColumn = mapping.generatedColumn
  332. if (mapping.source != null) {
  333. sourceIdx = this._sources.indexOf(mapping.source)
  334. next += base64VLQ.encode(sourceIdx - previousSource)
  335. previousSource = sourceIdx
  336. // lines are stored 0-based in SourceMap spec version 3
  337. next += base64VLQ.encode(
  338. mapping.originalLine - 1 - previousOriginalLine
  339. )
  340. previousOriginalLine = mapping.originalLine - 1
  341. next += base64VLQ.encode(
  342. mapping.originalColumn - previousOriginalColumn
  343. )
  344. previousOriginalColumn = mapping.originalColumn
  345. if (mapping.name != null) {
  346. nameIdx = this._names.indexOf(mapping.name)
  347. next += base64VLQ.encode(nameIdx - previousName)
  348. previousName = nameIdx
  349. }
  350. }
  351. result += next
  352. }
  353. return result
  354. }
  355. _generateSourcesContent(aSources, aSourceRoot) {
  356. return aSources.map(function (source) {
  357. if (!this._sourcesContents) {
  358. return null
  359. }
  360. if (aSourceRoot != null) {
  361. source = util.relative(aSourceRoot, source)
  362. }
  363. const key = util.toSetString(source)
  364. return Object.prototype.hasOwnProperty.call(this._sourcesContents, key)
  365. ? this._sourcesContents[key]
  366. : null
  367. }, this)
  368. }
  369. /**
  370. * Externalize the source map.
  371. */
  372. toJSON() {
  373. const map = {
  374. version: this._version,
  375. sources: this._sources.toArray(),
  376. names: this._names.toArray(),
  377. mappings: this._serializeMappings(),
  378. }
  379. if (this._file != null) {
  380. map.file = this._file
  381. }
  382. if (this._sourceRoot != null) {
  383. map.sourceRoot = this._sourceRoot
  384. }
  385. if (this._sourcesContents) {
  386. map.sourcesContent = this._generateSourcesContent(
  387. map.sources,
  388. map.sourceRoot
  389. )
  390. }
  391. return map
  392. }
  393. /**
  394. * Render the source map being generated to a string.
  395. */
  396. toString() {
  397. return JSON.stringify(this.toJSON())
  398. }
  399. }
  400. SourceMapGenerator.prototype._version = 3
  401. exports.SourceMapGenerator = SourceMapGenerator