map-generator.js 7.5 KB

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