cssVars.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import {
  2. processExpression,
  3. createTransformContext,
  4. createSimpleExpression,
  5. createRoot,
  6. NodeTypes,
  7. SimpleExpressionNode,
  8. BindingMetadata
  9. } from '@vue/compiler-dom'
  10. import { SFCDescriptor } from './parse'
  11. import { PluginCreator } from 'postcss'
  12. import hash from 'hash-sum'
  13. export const CSS_VARS_HELPER = `useCssVars`
  14. export function genCssVarsFromList(
  15. vars: string[],
  16. id: string,
  17. isProd: boolean,
  18. isSSR = false
  19. ): string {
  20. return `{\n ${vars
  21. .map(
  22. key => `"${isSSR ? `--` : ``}${genVarName(id, key, isProd)}": (${key})`
  23. )
  24. .join(',\n ')}\n}`
  25. }
  26. function genVarName(id: string, raw: string, isProd: boolean): string {
  27. if (isProd) {
  28. return hash(id + raw)
  29. } else {
  30. // escape ASCII Punctuation & Symbols
  31. return `${id}-${raw.replace(
  32. /[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g,
  33. s => `\\${s}`
  34. )}`
  35. }
  36. }
  37. function normalizeExpression(exp: string) {
  38. exp = exp.trim()
  39. if (
  40. (exp[0] === `'` && exp[exp.length - 1] === `'`) ||
  41. (exp[0] === `"` && exp[exp.length - 1] === `"`)
  42. ) {
  43. return exp.slice(1, -1)
  44. }
  45. return exp
  46. }
  47. const vBindRE = /v-bind\s*\(/g
  48. export function parseCssVars(sfc: SFCDescriptor): string[] {
  49. const vars: string[] = []
  50. sfc.styles.forEach(style => {
  51. let match
  52. // ignore v-bind() in comments /* ... */
  53. const content = style.content.replace(/\/\*([\s\S]*?)\*\//g, '')
  54. while ((match = vBindRE.exec(content))) {
  55. const start = match.index + match[0].length
  56. const end = lexBinding(content, start)
  57. if (end !== null) {
  58. const variable = normalizeExpression(content.slice(start, end))
  59. if (!vars.includes(variable)) {
  60. vars.push(variable)
  61. }
  62. }
  63. }
  64. })
  65. return vars
  66. }
  67. const enum LexerState {
  68. inParens,
  69. inSingleQuoteString,
  70. inDoubleQuoteString
  71. }
  72. function lexBinding(content: string, start: number): number | null {
  73. let state: LexerState = LexerState.inParens
  74. let parenDepth = 0
  75. for (let i = start; i < content.length; i++) {
  76. const char = content.charAt(i)
  77. switch (state) {
  78. case LexerState.inParens:
  79. if (char === `'`) {
  80. state = LexerState.inSingleQuoteString
  81. } else if (char === `"`) {
  82. state = LexerState.inDoubleQuoteString
  83. } else if (char === `(`) {
  84. parenDepth++
  85. } else if (char === `)`) {
  86. if (parenDepth > 0) {
  87. parenDepth--
  88. } else {
  89. return i
  90. }
  91. }
  92. break
  93. case LexerState.inSingleQuoteString:
  94. if (char === `'`) {
  95. state = LexerState.inParens
  96. }
  97. break
  98. case LexerState.inDoubleQuoteString:
  99. if (char === `"`) {
  100. state = LexerState.inParens
  101. }
  102. break
  103. }
  104. }
  105. return null
  106. }
  107. // for compileStyle
  108. export interface CssVarsPluginOptions {
  109. id: string
  110. isProd: boolean
  111. }
  112. export const cssVarsPlugin: PluginCreator<CssVarsPluginOptions> = opts => {
  113. const { id, isProd } = opts!
  114. return {
  115. postcssPlugin: 'vue-sfc-vars',
  116. Declaration(decl) {
  117. // rewrite CSS variables
  118. const value = decl.value
  119. if (vBindRE.test(value)) {
  120. vBindRE.lastIndex = 0
  121. let transformed = ''
  122. let lastIndex = 0
  123. let match
  124. while ((match = vBindRE.exec(value))) {
  125. const start = match.index + match[0].length
  126. const end = lexBinding(value, start)
  127. if (end !== null) {
  128. const variable = normalizeExpression(value.slice(start, end))
  129. transformed +=
  130. value.slice(lastIndex, match.index) +
  131. `var(--${genVarName(id, variable, isProd)})`
  132. lastIndex = end + 1
  133. }
  134. }
  135. decl.value = transformed + value.slice(lastIndex)
  136. }
  137. }
  138. }
  139. }
  140. cssVarsPlugin.postcss = true
  141. export function genCssVarsCode(
  142. vars: string[],
  143. bindings: BindingMetadata,
  144. id: string,
  145. isProd: boolean
  146. ) {
  147. const varsExp = genCssVarsFromList(vars, id, isProd)
  148. const exp = createSimpleExpression(varsExp, false)
  149. const context = createTransformContext(createRoot([]), {
  150. prefixIdentifiers: true,
  151. inline: true,
  152. bindingMetadata: bindings.__isScriptSetup === false ? undefined : bindings
  153. })
  154. const transformed = processExpression(exp, context)
  155. const transformedString =
  156. transformed.type === NodeTypes.SIMPLE_EXPRESSION
  157. ? transformed.content
  158. : transformed.children
  159. .map(c => {
  160. return typeof c === 'string'
  161. ? c
  162. : (c as SimpleExpressionNode).content
  163. })
  164. .join('')
  165. return `_${CSS_VARS_HELPER}(_ctx => (${transformedString}))`
  166. }
  167. // <script setup> already gets the calls injected as part of the transform
  168. // this is only for single normal <script>
  169. export function genNormalScriptCssVarsCode(
  170. cssVars: string[],
  171. bindings: BindingMetadata,
  172. id: string,
  173. isProd: boolean
  174. ): string {
  175. return (
  176. `\nimport { ${CSS_VARS_HELPER} as _${CSS_VARS_HELPER} } from 'vue'\n` +
  177. `const __injectCSSVars__ = () => {\n${genCssVarsCode(
  178. cssVars,
  179. bindings,
  180. id,
  181. isProd
  182. )}}\n` +
  183. `const __setup__ = __default__.setup\n` +
  184. `__default__.setup = __setup__\n` +
  185. ` ? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }\n` +
  186. ` : __injectCSSVars__\n`
  187. )
  188. }