map-generator.js 8.8 KB

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