cssVars.ts 5.2 KB

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