previous-map.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. 'use strict'
  2. let { existsSync, readFileSync, realpathSync } = require('fs')
  3. let { dirname, isAbsolute, join, relative, sep } = require('path')
  4. let { SourceMapConsumer, SourceMapGenerator } = require('source-map-js')
  5. function realPath(path) {
  6. try {
  7. return realpathSync(path)
  8. } catch {
  9. // Missing or dangling: keep the literal path. The existsSync() check below
  10. // still gates the read, and a path that does not exist cannot escape.
  11. return path
  12. }
  13. }
  14. function fromBase64(str) {
  15. if (Buffer) {
  16. return Buffer.from(str, 'base64').toString()
  17. } else {
  18. /* c8 ignore next 2 */
  19. return window.atob(str)
  20. }
  21. }
  22. class PreviousMap {
  23. constructor(css, opts) {
  24. if (opts.map === false) return
  25. if (opts.unsafeMap) this.unsafeMap = true
  26. this.loadAnnotation(css)
  27. this.inline = this.startWith(this.annotation, 'data:')
  28. let prev = opts.map ? opts.map.prev : undefined
  29. let text = this.loadMap(opts.from, prev)
  30. if (!this.mapFile && opts.from) {
  31. this.mapFile = opts.from
  32. }
  33. if (this.mapFile) this.root = dirname(this.mapFile)
  34. if (text) this.text = text
  35. }
  36. consumer() {
  37. if (!this.consumerCache) {
  38. this.consumerCache = new SourceMapConsumer(this.json || this.text)
  39. }
  40. return this.consumerCache
  41. }
  42. decodeInline(text) {
  43. let baseCharsetUri = /^data:application\/json;charset=utf-?8;base64,/
  44. let baseUri = /^data:application\/json;base64,/
  45. let charsetUri = /^data:application\/json;charset=utf-?8,/
  46. let uri = /^data:application\/json,/
  47. let uriMatch = text.match(charsetUri) || text.match(uri)
  48. if (uriMatch) {
  49. return decodeURIComponent(text.substr(uriMatch[0].length))
  50. }
  51. let baseUriMatch = text.match(baseCharsetUri) || text.match(baseUri)
  52. if (baseUriMatch) {
  53. return fromBase64(text.substr(baseUriMatch[0].length))
  54. }
  55. let encoding = text.slice('data:application/json;'.length)
  56. encoding = encoding.slice(0, encoding.indexOf(','))
  57. throw new Error('Unsupported source map encoding ' + encoding)
  58. }
  59. getAnnotationURL(sourceMapString) {
  60. return sourceMapString.replace(/^\/\*\s*# sourceMappingURL=/, '').trim()
  61. }
  62. isMap(map) {
  63. if (typeof map !== 'object') return false
  64. return (
  65. typeof map.mappings === 'string' ||
  66. typeof map._mappings === 'string' ||
  67. Array.isArray(map.sections)
  68. )
  69. }
  70. loadAnnotation(css) {
  71. let comments = css.match(/\/\*\s*# sourceMappingURL=/g)
  72. if (!comments) return
  73. // sourceMappingURLs from comments, strings, etc.
  74. let start = css.lastIndexOf(comments.pop())
  75. let end = css.indexOf('*/', start)
  76. if (start > -1 && end > -1) {
  77. // Locate the last sourceMappingURL to avoid pickin
  78. this.annotation = this.getAnnotationURL(css.substring(start, end))
  79. }
  80. }
  81. loadFile(path, cssFile, trusted) {
  82. if (!trusted && !this.unsafeMap) {
  83. if (!/\.map$/i.test(path)) return undefined
  84. if (!cssFile) return undefined
  85. // Compare *resolved* paths: relative() is textual, so without this a
  86. // symlink at or below the CSS file's directory points the map outside it.
  87. let rel = relative(realPath(dirname(cssFile)), realPath(path))
  88. if (rel === '..' || rel.startsWith('..' + sep) || isAbsolute(rel)) {
  89. return undefined
  90. }
  91. }
  92. this.root = dirname(path)
  93. if (existsSync(path)) {
  94. this.mapFile = path
  95. return readFileSync(path, 'utf-8').toString().trim()
  96. }
  97. }
  98. loadMap(file, prev) {
  99. if (prev === false) return false
  100. if (prev) {
  101. if (typeof prev === 'string') {
  102. return prev
  103. } else if (typeof prev === 'function') {
  104. let prevPath = prev(file)
  105. if (prevPath) {
  106. let map = this.loadFile(prevPath, file, true)
  107. if (!map) {
  108. throw new Error(
  109. 'Unable to load previous source map: ' + prevPath.toString()
  110. )
  111. }
  112. return map
  113. }
  114. } else if (prev instanceof SourceMapConsumer) {
  115. return SourceMapGenerator.fromSourceMap(prev).toString()
  116. } else if (prev instanceof SourceMapGenerator) {
  117. return prev.toString()
  118. } else if (this.isMap(prev)) {
  119. return JSON.stringify(prev)
  120. } else {
  121. throw new Error(
  122. 'Unsupported previous source map format: ' + prev.toString()
  123. )
  124. }
  125. } else if (this.inline) {
  126. return this.decodeInline(this.annotation)
  127. } else if (this.annotation) {
  128. let map = this.annotation
  129. if (file) map = join(dirname(file), map)
  130. let unknown = this.loadFile(map, file, false)
  131. if (unknown) {
  132. try {
  133. /* c8 ignore next 4 */
  134. this.json = JSON.parse(unknown.replace(/^\)]}'[^\n]*\n/, ''))
  135. } catch {
  136. return undefined
  137. }
  138. }
  139. return unknown
  140. }
  141. }
  142. startWith(string, start) {
  143. if (!string) return false
  144. return string.substr(0, start.length) === start
  145. }
  146. withContent() {
  147. return !!(
  148. this.consumer().sourcesContent &&
  149. this.consumer().sourcesContent.length > 0
  150. )
  151. }
  152. }
  153. module.exports = PreviousMap
  154. PreviousMap.default = PreviousMap