cssVars.ts 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  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. // match v-bind() with max 2-levels of nested parens.
  15. const cssVarRE = /v-bind\s*\(((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*)\)/g
  16. export function genCssVarsFromList(
  17. vars: string[],
  18. id: string,
  19. isProd: boolean,
  20. isSSR = false
  21. ): string {
  22. return `{\n ${vars
  23. .map(
  24. key => `"${isSSR ? `--` : ``}${genVarName(id, key, isProd)}": (${key})`
  25. )
  26. .join(',\n ')}\n}`
  27. }
  28. function genVarName(id: string, raw: string, isProd: boolean): string {
  29. if (isProd) {
  30. return hash(id + raw)
  31. } else {
  32. return `${id}-${raw.replace(/([^\w-])/g, '_')}`
  33. }
  34. }
  35. function normalizeExpression(exp: string) {
  36. exp = exp.trim()
  37. if (
  38. (exp[0] === `'` && exp[exp.length - 1] === `'`) ||
  39. (exp[0] === `"` && exp[exp.length - 1] === `"`)
  40. ) {
  41. return exp.slice(1, -1)
  42. }
  43. return exp
  44. }
  45. export function parseCssVars(sfc: SFCDescriptor): string[] {
  46. const vars: string[] = []
  47. sfc.styles.forEach(style => {
  48. let match
  49. // ignore v-bind() in comments /* ... */
  50. const content = style.content.replace(/\/\*([\s\S]*?)\*\//g, '')
  51. while ((match = cssVarRE.exec(content))) {
  52. const variable = normalizeExpression(match[1])
  53. if (!vars.includes(variable)) {
  54. vars.push(variable)
  55. }
  56. }
  57. })
  58. return vars
  59. }
  60. // for compileStyle
  61. export interface CssVarsPluginOptions {
  62. id: string
  63. isProd: boolean
  64. }
  65. export const cssVarsPlugin: PluginCreator<CssVarsPluginOptions> = opts => {
  66. const { id, isProd } = opts!
  67. return {
  68. postcssPlugin: 'vue-sfc-vars',
  69. Declaration(decl) {
  70. // rewrite CSS variables
  71. if (cssVarRE.test(decl.value)) {
  72. decl.value = decl.value.replace(cssVarRE, (_, $1) => {
  73. return `var(--${genVarName(id, normalizeExpression($1), isProd)})`
  74. })
  75. }
  76. }
  77. }
  78. }
  79. cssVarsPlugin.postcss = true
  80. export function genCssVarsCode(
  81. vars: string[],
  82. bindings: BindingMetadata,
  83. id: string,
  84. isProd: boolean
  85. ) {
  86. const varsExp = genCssVarsFromList(vars, id, isProd)
  87. const exp = createSimpleExpression(varsExp, false)
  88. const context = createTransformContext(createRoot([]), {
  89. prefixIdentifiers: true,
  90. inline: true,
  91. bindingMetadata: bindings.__isScriptSetup === false ? undefined : bindings
  92. })
  93. const transformed = processExpression(exp, context)
  94. const transformedString =
  95. transformed.type === NodeTypes.SIMPLE_EXPRESSION
  96. ? transformed.content
  97. : transformed.children
  98. .map(c => {
  99. return typeof c === 'string'
  100. ? c
  101. : (c as SimpleExpressionNode).content
  102. })
  103. .join('')
  104. return `_${CSS_VARS_HELPER}(_ctx => (${transformedString}))`
  105. }
  106. // <script setup> already gets the calls injected as part of the transform
  107. // this is only for single normal <script>
  108. export function genNormalScriptCssVarsCode(
  109. cssVars: string[],
  110. bindings: BindingMetadata,
  111. id: string,
  112. isProd: boolean
  113. ): string {
  114. return (
  115. `\nimport { ${CSS_VARS_HELPER} as _${CSS_VARS_HELPER} } from 'vue'\n` +
  116. `const __injectCSSVars__ = () => {\n${genCssVarsCode(
  117. cssVars,
  118. bindings,
  119. id,
  120. isProd
  121. )}}\n` +
  122. `const __setup__ = __default__.setup\n` +
  123. `__default__.setup = __setup__\n` +
  124. ` ? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }\n` +
  125. ` : __injectCSSVars__\n`
  126. )
  127. }