map-generator.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. 'use strict'
  2. let { dirname, resolve, relative, sep } = require('path')
  3. let { pathToFileURL } = require('url')
  4. let mozilla = require('source-map')
  5. class MapGenerator {
  6. constructor (stringify, root, opts) {
  7. this.stringify = stringify
  8. this.mapOpts = opts.map || {}
  9. this.root = root
  10. this.opts = opts
  11. }
  12. isMap () {
  13. if (typeof this.opts.map !== 'undefined') {
  14. return !!this.opts.map
  15. }
  16. return this.previous().length > 0
  17. }
  18. previous () {
  19. if (!this.previousMaps) {
  20. this.previousMaps = []
  21. this.root.walk(node => {
  22. if (node.source && node.source.input.map) {
  23. let map = node.source.input.map
  24. if (!this.previousMaps.includes(map)) {
  25. this.previousMaps.push(map)
  26. }
  27. }
  28. })
  29. }
  30. return this.previousMaps
  31. }
  32. isInline () {
  33. if (typeof this.mapOpts.inline !== 'undefined') {
  34. return this.mapOpts.inline
  35. }
  36. let annotation = this.mapOpts.annotation
  37. if (typeof annotation !== 'undefined' && annotation !== true) {
  38. return false
  39. }
  40. if (this.previous().length) {
  41. return this.previous().some(i => i.inline)
  42. }
  43. return true
  44. }
  45. isSourcesContent () {
  46. if (typeof this.mapOpts.sourcesContent !== 'undefined') {
  47. return this.mapOpts.sourcesContent
  48. }
  49. if (this.previous().length) {
  50. return this.previous().some(i => i.withContent())
  51. }
  52. return true
  53. }
  54. clearAnnotation () {
  55. if (this.mapOpts.annotation === false) return
  56. let node
  57. for (let i = this.root.nodes.length - 1; i >= 0; i--) {
  58. node = this.root.nodes[i]
  59. if (node.type !== 'comment') continue
  60. if (node.text.indexOf('# sourceMappingURL=') === 0) {
  61. this.root.removeChild(i)
  62. }
  63. }
  64. }
  65. setSourcesContent () {
  66. let already = {}
  67. this.root.walk(node => {
  68. if (node.source) {
  69. let from = node.source.input.from
  70. if (from && !already[from]) {
  71. already[from] = true
  72. this.map.setSourceContent(
  73. this.toUrl(this.path(from)),
  74. node.source.input.css
  75. )
  76. }
  77. }
  78. })
  79. }
  80. applyPrevMaps () {
  81. for (let prev of this.previous()) {
  82. let from = this.toUrl(this.path(prev.file))
  83. let root = prev.root || dirname(prev.file)
  84. let map
  85. if (this.mapOpts.sourcesContent === false) {
  86. map = new mozilla.SourceMapConsumer(prev.text)
  87. if (map.sourcesContent) {
  88. map.sourcesContent = map.sourcesContent.map(() => null)
  89. }
  90. } else {
  91. map = prev.consumer()
  92. }
  93. this.map.applySourceMap(map, from, this.toUrl(this.path(root)))
  94. }
  95. }
  96. isAnnotation () {
  97. if (this.isInline()) {
  98. return true
  99. }
  100. if (typeof this.mapOpts.annotation !== 'undefined') {
  101. return this.mapOpts.annotation
  102. }
  103. if (this.previous().length) {
  104. return this.previous().some(i => i.annotation)
  105. }
  106. return true
  107. }
  108. toBase64 (str) {
  109. if (Buffer) {
  110. return Buffer.from(str).toString('base64')
  111. } else {
  112. // istanbul ignore next
  113. return window.btoa(unescape(encodeURIComponent(str)))
  114. }
  115. }
  116. addAnnotation () {
  117. let content
  118. if (this.isInline()) {
  119. content =
  120. 'data:application/json;base64,' + this.toBase64(this.map.toString())
  121. } else if (typeof this.mapOpts.annotation === 'string') {
  122. content = this.mapOpts.annotation
  123. } else if (typeof this.mapOpts.annotation === 'function') {
  124. content = this.mapOpts.annotation(this.opts.to, this.root)
  125. } else {
  126. content = this.outputFile() + '.map'
  127. }
  128. let eol = '\n'
  129. if (this.css.includes('\r\n')) eol = '\r\n'
  130. this.css += eol + '/*# sourceMappingURL=' + content + ' */'
  131. }
  132. outputFile () {
  133. if (this.opts.to) {
  134. return this.path(this.opts.to)
  135. }
  136. if (this.opts.from) {
  137. return this.path(this.opts.from)
  138. }
  139. return 'to.css'
  140. }
  141. generateMap () {
  142. this.generateString()
  143. if (this.isSourcesContent()) this.setSourcesContent()
  144. if (this.previous().length > 0) this.applyPrevMaps()
  145. if (this.isAnnotation()) this.addAnnotation()
  146. if (this.isInline()) {
  147. return [this.css]
  148. }
  149. return [this.css, this.map]
  150. }
  151. path (file) {
  152. if (file.indexOf('<') === 0) return file
  153. if (/^\w+:\/\//.test(file)) return file
  154. if (this.mapOpts.absolute) return file
  155. let from = this.opts.to ? dirname(this.opts.to) : '.'
  156. if (typeof this.mapOpts.annotation === 'string') {
  157. from = dirname(resolve(from, this.mapOpts.annotation))
  158. }
  159. file = relative(from, file)
  160. return file
  161. }
  162. toUrl (path) {
  163. if (sep === '\\') {
  164. // istanbul ignore next
  165. path = path.replace(/\\/g, '/')
  166. }
  167. return encodeURI(path).replace(/[#?]/g, encodeURIComponent)
  168. }
  169. sourcePath (node) {
  170. if (this.mapOpts.from) {
  171. return this.toUrl(this.mapOpts.from)
  172. } else if (this.mapOpts.absolute) {
  173. return pathToFileURL(node.source.input.from).toString()
  174. } else {
  175. return this.toUrl(this.path(node.source.input.from))
  176. }
  177. }
  178. generateString () {
  179. this.css = ''
  180. this.map = new mozilla.SourceMapGenerator({ file: this.outputFile() })
  181. let line = 1
  182. let column = 1
  183. let noSource = '<no source>'
  184. let mapping = {
  185. source: '',
  186. generated: { line: 0, column: 0 },
  187. original: { line: 0, column: 0 }
  188. }
  189. let lines, last
  190. this.stringify(this.root, (str, node, type) => {
  191. this.css += str
  192. if (node && type !== 'end') {
  193. mapping.generated.line = line
  194. mapping.generated.column = column - 1
  195. if (node.source && node.source.start) {
  196. mapping.source = this.sourcePath(node)
  197. mapping.original.line = node.source.start.line
  198. mapping.original.column = node.source.start.column - 1
  199. this.map.addMapping(mapping)
  200. } else {
  201. mapping.source = noSource
  202. mapping.original.line = 1
  203. mapping.original.column = 0
  204. this.map.addMapping(mapping)
  205. }
  206. }
  207. lines = str.match(/\n/g)
  208. if (lines) {
  209. line += lines.length
  210. last = str.lastIndexOf('\n')
  211. column = str.length - last
  212. } else {
  213. column += str.length
  214. }
  215. if (node && type !== 'start') {
  216. let p = node.parent || { raws: {} }
  217. if (node.type !== 'decl' || node !== p.last || p.raws.semicolon) {
  218. if (node.source && node.source.end) {
  219. mapping.source = this.sourcePath(node)
  220. mapping.original.line = node.source.end.line
  221. mapping.original.column = node.source.end.column - 1
  222. mapping.generated.line = line
  223. mapping.generated.column = column - 2
  224. this.map.addMapping(mapping)
  225. } else {
  226. mapping.source = noSource
  227. mapping.original.line = 1
  228. mapping.original.column = 0
  229. mapping.generated.line = line
  230. mapping.generated.column = column - 1
  231. this.map.addMapping(mapping)
  232. }
  233. }
  234. }
  235. })
  236. }
  237. generate () {
  238. this.clearAnnotation()
  239. if (this.isMap()) {
  240. return this.generateMap()
  241. }
  242. let result = ''
  243. this.stringify(this.root, i => {
  244. result += i
  245. })
  246. return [result]
  247. }
  248. }
  249. module.exports = MapGenerator