Skip to content

Commit

Permalink
feat(sfc): css v-bind
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Jun 20, 2022
1 parent 2d67641 commit 8ab0074
Show file tree
Hide file tree
Showing 19 changed files with 798 additions and 52 deletions.
2 changes: 1 addition & 1 deletion examples/composition/todomvc.html
@@ -1,4 +1,4 @@
<script src="../../dist/vue.min.js"></script>
<script src="../../dist/vue.js"></script>
<link
rel="stylesheet"
href="../../node_modules/todomvc-app-css/index.css"
Expand Down
34 changes: 31 additions & 3 deletions packages/compiler-sfc/src/compileScript.ts
Expand Up @@ -42,6 +42,12 @@ import { isReservedTag } from 'web/util'
import { dirRE } from 'compiler/parser'
import { parseText } from 'compiler/parser/text-parser'
import { DEFAULT_FILENAME } from './parseComponent'
import {
CSS_VARS_HELPER,
genCssVarsCode,
genNormalScriptCssVarsCode
} from './cssVars'
import { rewriteDefault } from './rewriteDefault'

// Special compiler macros
const DEFINE_PROPS = 'defineProps'
Expand All @@ -57,6 +63,11 @@ const isBuiltInDir = makeMap(
)

export interface SFCScriptCompileOptions {
/**
* Scope ID for prefixing injected CSS variables.
* This must be consistent with the `id` passed to `compileStyle`.
*/
id: string
/**
* Production mode. Used to determine whether to generate hashed CSS variables
*/
Expand Down Expand Up @@ -86,14 +97,15 @@ export interface ImportBinding {
*/
export function compileScript(
sfc: SFCDescriptor,
options: SFCScriptCompileOptions = {}
options: SFCScriptCompileOptions = { id: '' }
): SFCScriptBlock {
let { filename, script, scriptSetup, source } = sfc
const isProd = !!options.isProd
const genSourceMap = options.sourceMap !== false
let refBindings: string[] | undefined

// const cssVars = sfc.cssVars
const cssVars = sfc.cssVars
const scopeId = options.id ? options.id.replace(/^data-v-/, '') : ''
const scriptLang = script && script.lang
const scriptSetupLang = scriptSetup && scriptSetup.lang
const isTS =
Expand Down Expand Up @@ -132,6 +144,16 @@ export function compileScript(
sourceType: 'module'
}).program
const bindings = analyzeScriptBindings(scriptAst.body)
if (cssVars.length) {
content = rewriteDefault(content, DEFAULT_VAR, plugins)
content += genNormalScriptCssVarsCode(
cssVars,
bindings,
scopeId,
isProd
)
content += `\nexport default ${DEFAULT_VAR}`
}
return {
...script,
content,
Expand Down Expand Up @@ -1082,7 +1104,13 @@ export function compileScript(
}

// 8. inject `useCssVars` calls
// Not backported in Vue 2
if (cssVars.length) {
helperImports.add(CSS_VARS_HELPER)
s.prependRight(
startOffset,
`\n${genCssVarsCode(cssVars, bindingMetadata, scopeId, isProd)}\n`
)
}

// 9. finalize setup() argument signature
let args = `__props`
Expand Down
4 changes: 4 additions & 0 deletions packages/compiler-sfc/src/compileStyle.ts
Expand Up @@ -7,6 +7,7 @@ import {
StylePreprocessor,
StylePreprocessorResults
} from './stylePreprocessors'
import { cssVarsPlugin } from './cssVars'

export interface SFCStyleCompileOptions {
source: string
Expand All @@ -19,6 +20,7 @@ export interface SFCStyleCompileOptions {
preprocessOptions?: any
postcssOptions?: any
postcssPlugins?: any[]
isProd?: boolean
}

export interface SFCAsyncStyleCompileOptions extends SFCStyleCompileOptions {
Expand Down Expand Up @@ -52,6 +54,7 @@ export function doCompileStyle(
id,
scoped = true,
trim = true,
isProd = false,
preprocessLang,
postcssOptions,
postcssPlugins
Expand All @@ -62,6 +65,7 @@ export function doCompileStyle(
const source = preProcessedSource ? preProcessedSource.code : options.source

const plugins = (postcssPlugins || []).slice()
plugins.unshift(cssVarsPlugin({ id: id.replace(/^data-v-/, ''), isProd }))
if (trim) {
plugins.push(trimPlugin())
}
Expand Down
6 changes: 2 additions & 4 deletions packages/compiler-sfc/src/compileTemplate.ts
Expand Up @@ -148,17 +148,15 @@ function actuallyCompile(
// version of Buble that applies ES2015 transforms + stripping `with` usage
let code =
`var __render__ = ${prefixIdentifiers(
render,
`render`,
`function render(${isFunctional ? `_c,_vm` : ``}){${render}\n}`,
isFunctional,
isTS,
transpileOptions,
bindings
)}\n` +
`var __staticRenderFns__ = [${staticRenderFns.map(code =>
prefixIdentifiers(
code,
``,
`function (${isFunctional ? `_c,_vm` : ``}){${code}\n}`,
isFunctional,
isTS,
transpileOptions,
Expand Down
179 changes: 179 additions & 0 deletions packages/compiler-sfc/src/cssVars.ts
@@ -0,0 +1,179 @@
import { BindingMetadata } from './types'
import { SFCDescriptor } from './parseComponent'
import { PluginCreator } from 'postcss'
import hash from 'hash-sum'
import { prefixIdentifiers } from './prefixIdentifiers'

export const CSS_VARS_HELPER = `useCssVars`

export function genCssVarsFromList(
vars: string[],
id: string,
isProd: boolean,
isSSR = false
): string {
return `{\n ${vars
.map(
key => `"${isSSR ? `--` : ``}${genVarName(id, key, isProd)}": (${key})`
)
.join(',\n ')}\n}`
}

function genVarName(id: string, raw: string, isProd: boolean): string {
if (isProd) {
return hash(id + raw)
} else {
return `${id}-${raw.replace(/([^\w-])/g, '_')}`
}
}

function normalizeExpression(exp: string) {
exp = exp.trim()
if (
(exp[0] === `'` && exp[exp.length - 1] === `'`) ||
(exp[0] === `"` && exp[exp.length - 1] === `"`)
) {
return exp.slice(1, -1)
}
return exp
}

const vBindRE = /v-bind\s*\(/g

export function parseCssVars(sfc: SFCDescriptor): string[] {
const vars: string[] = []
sfc.styles.forEach(style => {
let match
// ignore v-bind() in comments /* ... */
const content = style.content.replace(/\/\*([\s\S]*?)\*\//g, '')
while ((match = vBindRE.exec(content))) {
const start = match.index + match[0].length
const end = lexBinding(content, start)
if (end !== null) {
const variable = normalizeExpression(content.slice(start, end))
if (!vars.includes(variable)) {
vars.push(variable)
}
}
}
})
return vars
}

const enum LexerState {
inParens,
inSingleQuoteString,
inDoubleQuoteString
}

function lexBinding(content: string, start: number): number | null {
let state: LexerState = LexerState.inParens
let parenDepth = 0

for (let i = start; i < content.length; i++) {
const char = content.charAt(i)
switch (state) {
case LexerState.inParens:
if (char === `'`) {
state = LexerState.inSingleQuoteString
} else if (char === `"`) {
state = LexerState.inDoubleQuoteString
} else if (char === `(`) {
parenDepth++
} else if (char === `)`) {
if (parenDepth > 0) {
parenDepth--
} else {
return i
}
}
break
case LexerState.inSingleQuoteString:
if (char === `'`) {
state = LexerState.inParens
}
break
case LexerState.inDoubleQuoteString:
if (char === `"`) {
state = LexerState.inParens
}
break
}
}
return null
}

// for compileStyle
export interface CssVarsPluginOptions {
id: string
isProd: boolean
}

export const cssVarsPlugin: PluginCreator<CssVarsPluginOptions> = opts => {
const { id, isProd } = opts!
return {
postcssPlugin: 'vue-sfc-vars',
Declaration(decl) {
// rewrite CSS variables
const value = decl.value
if (vBindRE.test(value)) {
vBindRE.lastIndex = 0
let transformed = ''
let lastIndex = 0
let match
while ((match = vBindRE.exec(value))) {
const start = match.index + match[0].length
const end = lexBinding(value, start)
if (end !== null) {
const variable = normalizeExpression(value.slice(start, end))
transformed +=
value.slice(lastIndex, match.index) +
`var(--${genVarName(id, variable, isProd)})`
lastIndex = end + 1
}
}
decl.value = transformed + value.slice(lastIndex)
}
}
}
}
cssVarsPlugin.postcss = true

export function genCssVarsCode(
vars: string[],
bindings: BindingMetadata,
id: string,
isProd: boolean
) {
const varsExp = genCssVarsFromList(vars, id, isProd)
return `_${CSS_VARS_HELPER}((_vm, _setup) => ${prefixIdentifiers(
`(${varsExp})`,
false,
false,
undefined,
bindings
)})`
}

// <script setup> already gets the calls injected as part of the transform
// this is only for single normal <script>
export function genNormalScriptCssVarsCode(
cssVars: string[],
bindings: BindingMetadata,
id: string,
isProd: boolean
): string {
return (
`\nimport { ${CSS_VARS_HELPER} as _${CSS_VARS_HELPER} } from 'vue'\n` +
`const __injectCSSVars__ = () => {\n${genCssVarsCode(
cssVars,
bindings,
id,
isProd
)}}\n` +
`const __setup__ = __default__.setup\n` +
`__default__.setup = __setup__\n` +
` ? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }\n` +
` : __injectCSSVars__\n`
)
}
1 change: 1 addition & 0 deletions packages/compiler-sfc/src/index.ts
Expand Up @@ -7,6 +7,7 @@ export { generateCodeFrame } from 'compiler/codeframe'
export { rewriteDefault } from './rewriteDefault'

// types
export { SFCParseOptions } from './parse'
export { CompilerOptions, WarningMessage } from 'types/compiler'
export { TemplateCompiler } from './types'
export {
Expand Down
4 changes: 2 additions & 2 deletions packages/compiler-sfc/src/parse.ts
Expand Up @@ -16,7 +16,7 @@ const cache = new LRU<string, SFCDescriptor>(100)
const splitRE = /\r?\n/g
const emptyRE = /^(?:\/\/)?\s*$/

export interface ParseOptions {
export interface SFCParseOptions {
source: string
filename?: string
compiler?: TemplateCompiler
Expand All @@ -25,7 +25,7 @@ export interface ParseOptions {
sourceMap?: boolean
}

export function parse(options: ParseOptions): SFCDescriptor {
export function parse(options: SFCParseOptions): SFCDescriptor {
const {
source,
filename = DEFAULT_FILENAME,
Expand Down
9 changes: 8 additions & 1 deletion packages/compiler-sfc/src/parseComponent.ts
Expand Up @@ -4,6 +4,7 @@ import { makeMap } from 'shared/util'
import { ASTAttr, WarningMessage } from 'types/compiler'
import { BindingMetadata, RawSourceMap } from './types'
import type { ImportBinding } from './compileScript'
import { parseCssVars } from './cssVars'

export const DEFAULT_FILENAME = 'anonymous.vue'

Expand Down Expand Up @@ -50,7 +51,9 @@ export interface SFCDescriptor {
scriptSetup: SFCScriptBlock | null
styles: SFCBlock[]
customBlocks: SFCCustomBlock[]
errors: WarningMessage[]
cssVars: string[]

errors: (string | WarningMessage)[]

/**
* compare with an existing descriptor to determine whether HMR should perform
Expand Down Expand Up @@ -84,6 +87,7 @@ export function parseComponent(
scriptSetup: null, // TODO
styles: [],
customBlocks: [],
cssVars: [],
errors: [],
shouldForceReload: null as any // attached in parse() by compiler-sfc
}
Expand Down Expand Up @@ -205,5 +209,8 @@ export function parseComponent(
outputSourceRange: options.outputSourceRange
})

// parse CSS vars
sfc.cssVars = parseCssVars(sfc)

return sfc
}
6 changes: 1 addition & 5 deletions packages/compiler-sfc/src/prefixIdentifiers.ts
Expand Up @@ -14,19 +14,15 @@ const doNotPrefix = makeMap(
)

/**
* The input is expected to be the render function code directly returned from
* `compile()` calls, e.g. `with(this){return ...}`
* The input is expected to be a valid expression.
*/
export function prefixIdentifiers(
source: string,
fnName = '',
isFunctional = false,
isTS = false,
babelOptions: ParserOptions = {},
bindings?: BindingMetadata
) {
source = `function ${fnName}(${isFunctional ? `_c,_vm` : ``}){${source}\n}`

const s = new MagicString(source)

const plugins: ParserPlugin[] = [
Expand Down

0 comments on commit 8ab0074

Please sign in to comment.