scoped.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import { PluginCreator, Rule, AtRule } from 'postcss'
  2. import selectorParser from 'postcss-selector-parser'
  3. const animationNameRE = /^(-\w+-)?animation-name$/
  4. const animationRE = /^(-\w+-)?animation$/
  5. const scopedPlugin: PluginCreator<string> = (id = '') => {
  6. const keyframes = Object.create(null)
  7. const shortId = id.replace(/^data-v-/, '')
  8. return {
  9. postcssPlugin: 'vue-sfc-scoped',
  10. Rule(rule) {
  11. processRule(id, rule)
  12. },
  13. AtRule(node) {
  14. if (
  15. /-?keyframes$/.test(node.name) &&
  16. !node.params.endsWith(`-${shortId}`)
  17. ) {
  18. // register keyframes
  19. keyframes[node.params] = node.params = node.params + '-' + shortId
  20. }
  21. },
  22. OnceExit(root) {
  23. if (Object.keys(keyframes).length) {
  24. // If keyframes are found in this <style>, find and rewrite animation names
  25. // in declarations.
  26. // Caveat: this only works for keyframes and animation rules in the same
  27. // <style> element.
  28. // individual animation-name declaration
  29. root.walkDecls(decl => {
  30. if (animationNameRE.test(decl.prop)) {
  31. decl.value = decl.value
  32. .split(',')
  33. .map(v => keyframes[v.trim()] || v.trim())
  34. .join(',')
  35. }
  36. // shorthand
  37. if (animationRE.test(decl.prop)) {
  38. decl.value = decl.value
  39. .split(',')
  40. .map(v => {
  41. const vals = v.trim().split(/\s+/)
  42. const i = vals.findIndex(val => keyframes[val])
  43. if (i !== -1) {
  44. vals.splice(i, 1, keyframes[vals[i]])
  45. return vals.join(' ')
  46. } else {
  47. return v
  48. }
  49. })
  50. .join(',')
  51. }
  52. })
  53. }
  54. }
  55. }
  56. }
  57. const processedRules = new WeakSet<Rule>()
  58. function processRule(id: string, rule: Rule) {
  59. if (
  60. processedRules.has(rule) ||
  61. (rule.parent &&
  62. rule.parent.type === 'atrule' &&
  63. /-?keyframes$/.test((rule.parent as AtRule).name))
  64. ) {
  65. return
  66. }
  67. processedRules.add(rule)
  68. rule.selector = selectorParser(selectorRoot => {
  69. selectorRoot.each(selector => {
  70. rewriteSelector(id, selector, selectorRoot)
  71. })
  72. }).processSync(rule.selector)
  73. }
  74. function rewriteSelector(
  75. id: string,
  76. selector: selectorParser.Selector,
  77. selectorRoot: selectorParser.Root
  78. ) {
  79. let node: selectorParser.Node | null = null
  80. let shouldInject = true
  81. // find the last child node to insert attribute selector
  82. selector.each(n => {
  83. // DEPRECATED ">>>" and "/deep/" combinator
  84. if (
  85. n.type === 'combinator' &&
  86. (n.value === '>>>' || n.value === '/deep/')
  87. ) {
  88. n.value = ' '
  89. n.spaces.before = n.spaces.after = ''
  90. // warn(
  91. // `the >>> and /deep/ combinators have been deprecated. ` +
  92. // `Use :deep() instead.`
  93. // )
  94. return false
  95. }
  96. if (n.type === 'pseudo') {
  97. const { value } = n
  98. // deep: inject [id] attribute at the node before the ::v-deep
  99. // combinator.
  100. if (value === ':deep' || value === '::v-deep') {
  101. if (n.nodes.length) {
  102. // .foo ::v-deep(.bar) -> .foo[xxxxxxx] .bar
  103. // replace the current node with ::v-deep's inner selector
  104. let last: selectorParser.Selector['nodes'][0] = n
  105. n.nodes[0].each(ss => {
  106. selector.insertAfter(last, ss)
  107. last = ss
  108. })
  109. // insert a space combinator before if it doesn't already have one
  110. const prev = selector.at(selector.index(n) - 1)
  111. if (!prev || !isSpaceCombinator(prev)) {
  112. selector.insertAfter(
  113. n,
  114. selectorParser.combinator({
  115. value: ' '
  116. })
  117. )
  118. }
  119. selector.removeChild(n)
  120. } else {
  121. // DEPRECATED usage in v3
  122. // .foo ::v-deep .bar -> .foo[xxxxxxx] .bar
  123. // warn(
  124. // `::v-deep usage as a combinator has ` +
  125. // `been deprecated. Use :deep(<inner-selector>) instead.`
  126. // )
  127. const prev = selector.at(selector.index(n) - 1)
  128. if (prev && isSpaceCombinator(prev)) {
  129. selector.removeChild(prev)
  130. }
  131. selector.removeChild(n)
  132. }
  133. return false
  134. }
  135. // !!! Vue 2 does not have :slotted support
  136. // ::v-slotted(.foo) -> .foo[xxxxxxx-s]
  137. // if (value === ':slotted' || value === '::v-slotted') {
  138. // rewriteSelector(id, n.nodes[0], selectorRoot, true /* slotted */)
  139. // let last: selectorParser.Selector['nodes'][0] = n
  140. // n.nodes[0].each(ss => {
  141. // selector.insertAfter(last, ss)
  142. // last = ss
  143. // })
  144. // // selector.insertAfter(n, n.nodes[0])
  145. // selector.removeChild(n)
  146. // // since slotted attribute already scopes the selector there's no
  147. // // need for the non-slot attribute.
  148. // shouldInject = false
  149. // return false
  150. // }
  151. // global: replace with inner selector and do not inject [id].
  152. // ::v-global(.foo) -> .foo
  153. if (value === ':global' || value === '::v-global') {
  154. selectorRoot.insertAfter(selector, n.nodes[0])
  155. selectorRoot.removeChild(selector)
  156. return false
  157. }
  158. }
  159. if (n.type !== 'pseudo' && n.type !== 'combinator') {
  160. node = n
  161. }
  162. })
  163. if (node) {
  164. ;(node as selectorParser.Node).spaces.after = ''
  165. } else {
  166. // For deep selectors & standalone pseudo selectors,
  167. // the attribute selectors are prepended rather than appended.
  168. // So all leading spaces must be eliminated to avoid problems.
  169. selector.first.spaces.before = ''
  170. }
  171. if (shouldInject) {
  172. selector.insertAfter(
  173. // If node is null it means we need to inject [id] at the start
  174. // insertAfter can handle `null` here
  175. node as any,
  176. selectorParser.attribute({
  177. attribute: id,
  178. value: id,
  179. raws: {},
  180. quoteMark: `"`
  181. })
  182. )
  183. }
  184. }
  185. function isSpaceCombinator(node: selectorParser.Node) {
  186. return node.type === 'combinator' && /^\s+$/.test(node.value)
  187. }
  188. scopedPlugin.postcss = true
  189. export default scopedPlugin