cssVars.ts 4.5 KB

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