You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

331 lines
8.6 KiB

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