mirror of
https://github.com/vuejs/vue.git
synced 2024-11-21 12:18:54 +00:00
feat(sfc): css v-bind
This commit is contained in:
parent
2d67641656
commit
8ab0074bab
@ -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"
|
||||
|
@ -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'
|
||||
@ -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
|
||||
*/
|
||||
@ -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 =
|
||||
@ -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,
|
||||
@ -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`
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
StylePreprocessor,
|
||||
StylePreprocessorResults
|
||||
} from './stylePreprocessors'
|
||||
import { cssVarsPlugin } from './cssVars'
|
||||
|
||||
export interface SFCStyleCompileOptions {
|
||||
source: string
|
||||
@ -19,6 +20,7 @@ export interface SFCStyleCompileOptions {
|
||||
preprocessOptions?: any
|
||||
postcssOptions?: any
|
||||
postcssPlugins?: any[]
|
||||
isProd?: boolean
|
||||
}
|
||||
|
||||
export interface SFCAsyncStyleCompileOptions extends SFCStyleCompileOptions {
|
||||
@ -52,6 +54,7 @@ export function doCompileStyle(
|
||||
id,
|
||||
scoped = true,
|
||||
trim = true,
|
||||
isProd = false,
|
||||
preprocessLang,
|
||||
postcssOptions,
|
||||
postcssPlugins
|
||||
@ -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())
|
||||
}
|
||||
|
@ -148,8 +148,7 @@ 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,
|
||||
@ -157,8 +156,7 @@ function actuallyCompile(
|
||||
)}\n` +
|
||||
`var __staticRenderFns__ = [${staticRenderFns.map(code =>
|
||||
prefixIdentifiers(
|
||||
code,
|
||||
``,
|
||||
`function (${isFunctional ? `_c,_vm` : ``}){${code}\n}`,
|
||||
isFunctional,
|
||||
isTS,
|
||||
transpileOptions,
|
||||
|
179
packages/compiler-sfc/src/cssVars.ts
Normal file
179
packages/compiler-sfc/src/cssVars.ts
Normal file
@ -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`
|
||||
)
|
||||
}
|
@ -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 {
|
||||
|
@ -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
|
||||
@ -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,
|
||||
|
@ -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'
|
||||
|
||||
@ -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
|
||||
@ -84,6 +87,7 @@ export function parseComponent(
|
||||
scriptSetup: null, // TODO
|
||||
styles: [],
|
||||
customBlocks: [],
|
||||
cssVars: [],
|
||||
errors: [],
|
||||
shouldForceReload: null as any // attached in parse() by compiler-sfc
|
||||
}
|
||||
@ -205,5 +209,8 @@ export function parseComponent(
|
||||
outputSourceRange: options.outputSourceRange
|
||||
})
|
||||
|
||||
// parse CSS vars
|
||||
sfc.cssVars = parseCssVars(sfc)
|
||||
|
||||
return sfc
|
||||
}
|
||||
|
@ -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[] = [
|
||||
|
189
packages/compiler-sfc/test/__snapshots__/cssVars.spec.ts.snap
Normal file
189
packages/compiler-sfc/test/__snapshots__/cssVars.spec.ts.snap
Normal file
@ -0,0 +1,189 @@
|
||||
// Vitest Snapshot v1
|
||||
|
||||
exports[`CSS vars injection > codegen > <script> w/ default export 1`] = `
|
||||
"const __default__ = { setup() {} }
|
||||
import { useCssVars as _useCssVars } from 'vue'
|
||||
const __injectCSSVars__ = () => {
|
||||
_useCssVars((_vm, _setup) => ({
|
||||
\\"xxxxxxxx-color\\": (_vm.color)
|
||||
}))}
|
||||
const __setup__ = __default__.setup
|
||||
__default__.setup = __setup__
|
||||
? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }
|
||||
: __injectCSSVars__
|
||||
|
||||
export default __default__"
|
||||
`;
|
||||
|
||||
exports[`CSS vars injection > codegen > <script> w/ default export in strings/comments 1`] = `
|
||||
"
|
||||
// export default {}
|
||||
const __default__ = {}
|
||||
|
||||
import { useCssVars as _useCssVars } from 'vue'
|
||||
const __injectCSSVars__ = () => {
|
||||
_useCssVars((_vm, _setup) => ({
|
||||
\\"xxxxxxxx-color\\": (_vm.color)
|
||||
}))}
|
||||
const __setup__ = __default__.setup
|
||||
__default__.setup = __setup__
|
||||
? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }
|
||||
: __injectCSSVars__
|
||||
|
||||
export default __default__"
|
||||
`;
|
||||
|
||||
exports[`CSS vars injection > codegen > <script> w/ no default export 1`] = `
|
||||
"const a = 1
|
||||
const __default__ = {}
|
||||
import { useCssVars as _useCssVars } from 'vue'
|
||||
const __injectCSSVars__ = () => {
|
||||
_useCssVars((_vm, _setup) => ({
|
||||
\\"xxxxxxxx-color\\": (_vm.color)
|
||||
}))}
|
||||
const __setup__ = __default__.setup
|
||||
__default__.setup = __setup__
|
||||
? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }
|
||||
: __injectCSSVars__
|
||||
|
||||
export default __default__"
|
||||
`;
|
||||
|
||||
exports[`CSS vars injection > codegen > should ignore comments 1`] = `
|
||||
"import { useCssVars as _useCssVars } from 'vue'
|
||||
|
||||
export default {
|
||||
setup(__props) {
|
||||
|
||||
_useCssVars((_vm, _setup) => ({
|
||||
\\"xxxxxxxx-width\\": (_setup.width)
|
||||
}))
|
||||
const color = 'red';const width = 100
|
||||
return { color, width }
|
||||
}
|
||||
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`CSS vars injection > codegen > should work with w/ complex expression 1`] = `
|
||||
"import { useCssVars as _useCssVars } from 'vue'
|
||||
|
||||
export default {
|
||||
setup(__props) {
|
||||
|
||||
_useCssVars((_vm, _setup) => ({
|
||||
\\"xxxxxxxx-foo\\": (_setup.foo),
|
||||
\\"xxxxxxxx-foo____px_\\": (_setup.foo + 'px'),
|
||||
\\"xxxxxxxx-_a___b____2____px_\\": ((_setup.a + _setup.b) / 2 + 'px'),
|
||||
\\"xxxxxxxx-__a___b______2___a_\\": (((_setup.a + _setup.b)) / (2 * _setup.a))
|
||||
}))
|
||||
|
||||
let a = 100
|
||||
let b = 200
|
||||
let foo = 300
|
||||
|
||||
return { a, b, foo }
|
||||
}
|
||||
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`CSS vars injection > codegen > w/ <script setup> 1`] = `
|
||||
"import { useCssVars as _useCssVars } from 'vue'
|
||||
|
||||
export default {
|
||||
setup(__props) {
|
||||
|
||||
_useCssVars((_vm, _setup) => ({
|
||||
\\"xxxxxxxx-color\\": (_setup.color)
|
||||
}))
|
||||
const color = 'red'
|
||||
return { color }
|
||||
}
|
||||
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`CSS vars injection > codegen > w/ <script setup> using the same var multiple times 1`] = `
|
||||
"import { useCssVars as _useCssVars } from 'vue'
|
||||
|
||||
export default {
|
||||
setup(__props) {
|
||||
|
||||
_useCssVars((_vm, _setup) => ({
|
||||
\\"xxxxxxxx-color\\": (_setup.color)
|
||||
}))
|
||||
|
||||
const color = 'red'
|
||||
|
||||
return { color }
|
||||
}
|
||||
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`CSS vars injection > generating correct code for nested paths 1`] = `
|
||||
"const a = 1
|
||||
const __default__ = {}
|
||||
import { useCssVars as _useCssVars } from 'vue'
|
||||
const __injectCSSVars__ = () => {
|
||||
_useCssVars((_vm, _setup) => ({
|
||||
\\"xxxxxxxx-color\\": (_vm.color),
|
||||
\\"xxxxxxxx-font_size\\": (_vm.font.size)
|
||||
}))}
|
||||
const __setup__ = __default__.setup
|
||||
__default__.setup = __setup__
|
||||
? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }
|
||||
: __injectCSSVars__
|
||||
|
||||
export default __default__"
|
||||
`;
|
||||
|
||||
exports[`CSS vars injection > w/ <script setup> binding analysis 1`] = `
|
||||
"import { useCssVars as _useCssVars } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
foo: String
|
||||
},
|
||||
setup(__props) {
|
||||
|
||||
_useCssVars((_vm, _setup) => ({
|
||||
\\"xxxxxxxx-color\\": (_setup.color),
|
||||
\\"xxxxxxxx-size\\": (_setup.size),
|
||||
\\"xxxxxxxx-foo\\": (_vm.foo)
|
||||
}))
|
||||
|
||||
const color = 'red'
|
||||
const size = ref('10px')
|
||||
|
||||
|
||||
return { color, size, ref }
|
||||
}
|
||||
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`CSS vars injection > w/ normal <script> binding analysis 1`] = `
|
||||
"
|
||||
const __default__ = {
|
||||
setup() {
|
||||
return {
|
||||
size: ref('100px')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
import { useCssVars as _useCssVars } from 'vue'
|
||||
const __injectCSSVars__ = () => {
|
||||
_useCssVars((_vm, _setup) => ({
|
||||
\\"xxxxxxxx-size\\": (_vm.size)
|
||||
}))}
|
||||
const __setup__ = __default__.setup
|
||||
__default__.setup = __setup__
|
||||
? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }
|
||||
: __injectCSSVars__
|
||||
|
||||
export default __default__"
|
||||
`;
|
@ -1,33 +1,5 @@
|
||||
import { BindingTypes } from '../src/types'
|
||||
import { parse, ParseOptions } from '../src/parse'
|
||||
import { parse as babelParse } from '@babel/parser'
|
||||
import { compileScript, SFCScriptCompileOptions } from '../src/compileScript'
|
||||
|
||||
function compile(
|
||||
source: string,
|
||||
options?: Partial<SFCScriptCompileOptions>,
|
||||
parseOptions?: Partial<ParseOptions>
|
||||
) {
|
||||
const sfc = parse({
|
||||
...parseOptions,
|
||||
source
|
||||
})
|
||||
return compileScript(sfc, options)
|
||||
}
|
||||
|
||||
function assertCode(code: string) {
|
||||
// parse the generated code to make sure it is valid
|
||||
try {
|
||||
babelParse(code, {
|
||||
sourceType: 'module',
|
||||
plugins: ['typescript']
|
||||
})
|
||||
} catch (e: any) {
|
||||
console.log(code)
|
||||
throw e
|
||||
}
|
||||
expect(code).toMatchSnapshot()
|
||||
}
|
||||
import { compile, assertCode } from './util'
|
||||
|
||||
describe('SFC compile <script setup>', () => {
|
||||
test('should expose top level declarations', () => {
|
||||
|
247
packages/compiler-sfc/test/cssVars.spec.ts
Normal file
247
packages/compiler-sfc/test/cssVars.spec.ts
Normal file
@ -0,0 +1,247 @@
|
||||
import { compileStyle, parse } from '../src'
|
||||
import { mockId, compile, assertCode } from './util'
|
||||
|
||||
describe('CSS vars injection', () => {
|
||||
test('generating correct code for nested paths', () => {
|
||||
const { content } = compile(
|
||||
`<script>const a = 1</script>\n` +
|
||||
`<style>div{
|
||||
color: v-bind(color);
|
||||
font-size: v-bind('font.size');
|
||||
}</style>`
|
||||
)
|
||||
expect(content).toMatch(`_useCssVars((_vm, _setup) => ({
|
||||
"${mockId}-color": (_vm.color),
|
||||
"${mockId}-font_size": (_vm.font.size)
|
||||
})`)
|
||||
assertCode(content)
|
||||
})
|
||||
|
||||
test('w/ normal <script> binding analysis', () => {
|
||||
const { content } = compile(
|
||||
`<script>
|
||||
export default {
|
||||
setup() {
|
||||
return {
|
||||
size: ref('100px')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>\n` +
|
||||
`<style>
|
||||
div {
|
||||
font-size: v-bind(size);
|
||||
}
|
||||
</style>`
|
||||
)
|
||||
expect(content).toMatch(`_useCssVars((_vm, _setup) => ({
|
||||
"${mockId}-size": (_vm.size)
|
||||
})`)
|
||||
expect(content).toMatch(`import { useCssVars as _useCssVars } from 'vue'`)
|
||||
assertCode(content)
|
||||
})
|
||||
|
||||
test('w/ <script setup> binding analysis', () => {
|
||||
const { content } = compile(
|
||||
`<script setup>
|
||||
import { defineProps, ref } from 'vue'
|
||||
const color = 'red'
|
||||
const size = ref('10px')
|
||||
defineProps({
|
||||
foo: String
|
||||
})
|
||||
</script>\n` +
|
||||
`<style>
|
||||
div {
|
||||
color: v-bind(color);
|
||||
font-size: v-bind(size);
|
||||
border: v-bind(foo);
|
||||
}
|
||||
</style>`
|
||||
)
|
||||
// should handle:
|
||||
// 1. local const bindings
|
||||
// 2. local potential ref bindings
|
||||
// 3. props bindings (analyzed)
|
||||
expect(content).toMatch(`_useCssVars((_vm, _setup) => ({
|
||||
"${mockId}-color": (_setup.color),
|
||||
"${mockId}-size": (_setup.size),
|
||||
"${mockId}-foo": (_vm.foo)
|
||||
})`)
|
||||
expect(content).toMatch(`import { useCssVars as _useCssVars } from 'vue'`)
|
||||
assertCode(content)
|
||||
})
|
||||
|
||||
test('should rewrite CSS vars in compileStyle', () => {
|
||||
const { code } = compileStyle({
|
||||
source: `.foo {
|
||||
color: v-bind(color);
|
||||
font-size: v-bind('font.size');
|
||||
}`,
|
||||
filename: 'test.css',
|
||||
id: 'data-v-test'
|
||||
})
|
||||
expect(code).toMatchInlineSnapshot(`
|
||||
".foo[data-v-test] {
|
||||
color: var(--test-color);
|
||||
font-size: var(--test-font_size);
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('prod mode', () => {
|
||||
const { content } = compile(
|
||||
`<script>const a = 1</script>\n` +
|
||||
`<style>div{
|
||||
color: v-bind(color);
|
||||
font-size: v-bind('font.size');
|
||||
}</style>`,
|
||||
{ isProd: true }
|
||||
)
|
||||
expect(content).toMatch(`_useCssVars((_vm, _setup) => ({
|
||||
"4003f1a6": (_vm.color),
|
||||
"41b6490a": (_vm.font.size)
|
||||
}))}`)
|
||||
|
||||
const { code } = compileStyle({
|
||||
source: `.foo {
|
||||
color: v-bind(color);
|
||||
font-size: v-bind('font.size');
|
||||
}`,
|
||||
filename: 'test.css',
|
||||
id: mockId,
|
||||
isProd: true
|
||||
})
|
||||
expect(code).toMatchInlineSnapshot(`
|
||||
".foo[xxxxxxxx] {
|
||||
color: var(--4003f1a6);
|
||||
font-size: var(--41b6490a);
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
describe('codegen', () => {
|
||||
test('<script> w/ no default export', () => {
|
||||
assertCode(
|
||||
compile(
|
||||
`<script>const a = 1</script>\n` +
|
||||
`<style>div{ color: v-bind(color); }</style>`
|
||||
).content
|
||||
)
|
||||
})
|
||||
|
||||
test('<script> w/ default export', () => {
|
||||
assertCode(
|
||||
compile(
|
||||
`<script>export default { setup() {} }</script>\n` +
|
||||
`<style>div{ color: v-bind(color); }</style>`
|
||||
).content
|
||||
)
|
||||
})
|
||||
|
||||
test('<script> w/ default export in strings/comments', () => {
|
||||
assertCode(
|
||||
compile(
|
||||
`<script>
|
||||
// export default {}
|
||||
export default {}
|
||||
</script>\n` + `<style>div{ color: v-bind(color); }</style>`
|
||||
).content
|
||||
)
|
||||
})
|
||||
|
||||
test('w/ <script setup>', () => {
|
||||
assertCode(
|
||||
compile(
|
||||
`<script setup>const color = 'red'</script>\n` +
|
||||
`<style>div{ color: v-bind(color); }</style>`
|
||||
).content
|
||||
)
|
||||
})
|
||||
|
||||
//#4185
|
||||
test('should ignore comments', () => {
|
||||
const { content } = compile(
|
||||
`<script setup>const color = 'red';const width = 100</script>\n` +
|
||||
`<style>
|
||||
/* comment **/
|
||||
div{ /* color: v-bind(color); */ width:20; }
|
||||
div{ width: v-bind(width); }
|
||||
/* comment */
|
||||
</style>`
|
||||
)
|
||||
|
||||
expect(content).not.toMatch(`"${mockId}-color": (_setup.color)`)
|
||||
expect(content).toMatch(`"${mockId}-width": (_setup.width)`)
|
||||
assertCode(content)
|
||||
})
|
||||
|
||||
test('w/ <script setup> using the same var multiple times', () => {
|
||||
const { content } = compile(
|
||||
`<script setup>
|
||||
const color = 'red'
|
||||
</script>\n` +
|
||||
`<style>
|
||||
div {
|
||||
color: v-bind(color);
|
||||
}
|
||||
p {
|
||||
color: v-bind(color);
|
||||
}
|
||||
</style>`
|
||||
)
|
||||
// color should only be injected once, even if it is twice in style
|
||||
expect(content).toMatch(`_useCssVars((_vm, _setup) => ({
|
||||
"${mockId}-color": (_setup.color)
|
||||
})`)
|
||||
assertCode(content)
|
||||
})
|
||||
|
||||
test('should work with w/ complex expression', () => {
|
||||
const { content } = compile(
|
||||
`<script setup>
|
||||
let a = 100
|
||||
let b = 200
|
||||
let foo = 300
|
||||
</script>\n` +
|
||||
`<style>
|
||||
p{
|
||||
width: calc(v-bind(foo) - 3px);
|
||||
height: calc(v-bind('foo') - 3px);
|
||||
top: calc(v-bind(foo + 'px') - 3px);
|
||||
}
|
||||
div {
|
||||
color: v-bind((a + b) / 2 + 'px' );
|
||||
}
|
||||
div {
|
||||
color: v-bind ((a + b) / 2 + 'px' );
|
||||
}
|
||||
p {
|
||||
color: v-bind(((a + b)) / (2 * a));
|
||||
}
|
||||
</style>`
|
||||
)
|
||||
expect(content).toMatch(`_useCssVars((_vm, _setup) => ({
|
||||
"${mockId}-foo": (_setup.foo),
|
||||
"${mockId}-foo____px_": (_setup.foo + 'px'),
|
||||
"${mockId}-_a___b____2____px_": ((_setup.a + _setup.b) / 2 + 'px'),
|
||||
"${mockId}-__a___b______2___a_": (((_setup.a + _setup.b)) / (2 * _setup.a))
|
||||
})`)
|
||||
assertCode(content)
|
||||
})
|
||||
|
||||
// #6022
|
||||
test('should be able to parse incomplete expressions', () => {
|
||||
const { cssVars } = parse({
|
||||
source: `<script setup>let xxx = 1</script>
|
||||
<style scoped>
|
||||
label {
|
||||
font-weight: v-bind("count.toString(");
|
||||
font-weight: v-bind(xxx);
|
||||
}
|
||||
</style>`
|
||||
})
|
||||
expect(cssVars).toMatchObject([`count.toString(`, `xxx`])
|
||||
})
|
||||
})
|
||||
})
|
@ -3,6 +3,8 @@ import { compile } from 'web/entry-compiler'
|
||||
import { format } from 'prettier'
|
||||
import { BindingTypes } from '../src/types'
|
||||
|
||||
const toFn = (source: string) => `function render(){${source}\n}`
|
||||
|
||||
it('should work', () => {
|
||||
const { render } = compile(`<div id="app">
|
||||
<div>{{ foo }}</div>
|
||||
@ -12,7 +14,7 @@ it('should work', () => {
|
||||
</foo>
|
||||
</div>`)
|
||||
|
||||
const result = format(prefixIdentifiers(render, `render`), {
|
||||
const result = format(prefixIdentifiers(toFn(render)), {
|
||||
semi: false,
|
||||
parser: 'babel'
|
||||
})
|
||||
@ -59,7 +61,7 @@ it('setup bindings', () => {
|
||||
const { render } = compile(`<div @click="count++">{{ count }}</div>`)
|
||||
|
||||
const result = format(
|
||||
prefixIdentifiers(render, `render`, false, false, undefined, {
|
||||
prefixIdentifiers(toFn(render), false, false, undefined, {
|
||||
count: BindingTypes.SETUP_REF
|
||||
}),
|
||||
{
|
||||
|
35
packages/compiler-sfc/test/util.ts
Normal file
35
packages/compiler-sfc/test/util.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import {
|
||||
parse,
|
||||
compileScript,
|
||||
type SFCParseOptions,
|
||||
type SFCScriptCompileOptions
|
||||
} from '../src'
|
||||
import { parse as babelParse } from '@babel/parser'
|
||||
|
||||
export const mockId = 'xxxxxxxx'
|
||||
|
||||
export function compile(
|
||||
source: string,
|
||||
options?: Partial<SFCScriptCompileOptions>,
|
||||
parseOptions?: Partial<SFCParseOptions>
|
||||
) {
|
||||
const sfc = parse({
|
||||
...parseOptions,
|
||||
source
|
||||
})
|
||||
return compileScript(sfc, { id: mockId, ...options })
|
||||
}
|
||||
|
||||
export function assertCode(code: string) {
|
||||
// parse the generated code to make sure it is valid
|
||||
try {
|
||||
babelParse(code, {
|
||||
sourceType: 'module',
|
||||
plugins: ['typescript']
|
||||
})
|
||||
} catch (e: any) {
|
||||
console.log(code)
|
||||
throw e
|
||||
}
|
||||
expect(code).toMatchSnapshot()
|
||||
}
|
@ -86,13 +86,17 @@ export function proxyWithRefUnwrap(
|
||||
source: Record<string, any>,
|
||||
key: string
|
||||
) {
|
||||
let raw = source[key]
|
||||
Object.defineProperty(target, key, {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get: () => (isRef(raw) ? raw.value : raw),
|
||||
set: newVal =>
|
||||
isRef(raw) ? (raw.value = newVal) : (raw = source[key] = newVal)
|
||||
get: () => {
|
||||
const raw = source[key]
|
||||
return isRef(raw) ? raw.value : raw
|
||||
},
|
||||
set: newVal => {
|
||||
const raw = source[key]
|
||||
isRef(raw) ? (raw.value = newVal) : (source[key] = newVal)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -77,6 +77,7 @@ export { nextTick } from 'core/util/next-tick'
|
||||
export { set, del } from 'core/observer'
|
||||
|
||||
export { useCssModule } from './sfc-helpers/useCssModule'
|
||||
export { useCssVars } from './sfc-helpers/useCssVars'
|
||||
|
||||
/**
|
||||
* @internal type is manually declared in <root>/types/v3-define-component.d.ts
|
||||
|
34
src/v3/sfc-helpers/useCssVars.ts
Normal file
34
src/v3/sfc-helpers/useCssVars.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { watchPostEffect } from '../'
|
||||
import { inBrowser, warn } from 'core/util'
|
||||
import { currentInstance } from '../currentInstance'
|
||||
|
||||
/**
|
||||
* Runtime helper for SFC's CSS variable injection feature.
|
||||
* @private
|
||||
*/
|
||||
export function useCssVars(
|
||||
getter: (
|
||||
vm: Record<string, any>,
|
||||
setupProxy: Record<string, any>
|
||||
) => Record<string, string>
|
||||
) {
|
||||
if (!inBrowser && !__TEST__) return
|
||||
|
||||
const instance = currentInstance
|
||||
if (!instance) {
|
||||
__DEV__ &&
|
||||
warn(`useCssVars is called without current active component instance.`)
|
||||
return
|
||||
}
|
||||
|
||||
watchPostEffect(() => {
|
||||
const el = instance.$el
|
||||
const vars = getter(instance, instance._setupProxy!)
|
||||
if (el && el.nodeType === 1) {
|
||||
const style = (el as HTMLElement).style
|
||||
for (const key in vars) {
|
||||
style.setProperty(`--${key}`, vars[key])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
48
test/unit/features/v3/useCssVars.spec.ts
Normal file
48
test/unit/features/v3/useCssVars.spec.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import Vue from 'vue'
|
||||
import { useCssVars, h, reactive, nextTick } from 'v3'
|
||||
|
||||
describe('useCssVars', () => {
|
||||
async function assertCssVars(getApp: (state: any) => any) {
|
||||
const state = reactive({ color: 'red' })
|
||||
const App = getApp(state)
|
||||
const vm = new Vue(App).$mount()
|
||||
await nextTick()
|
||||
expect((vm.$el as HTMLElement).style.getPropertyValue(`--color`)).toBe(
|
||||
`red`
|
||||
)
|
||||
|
||||
state.color = 'green'
|
||||
await nextTick()
|
||||
expect((vm.$el as HTMLElement).style.getPropertyValue(`--color`)).toBe(
|
||||
`green`
|
||||
)
|
||||
}
|
||||
|
||||
test('basic', async () => {
|
||||
await assertCssVars(state => ({
|
||||
setup() {
|
||||
// test receiving render context
|
||||
useCssVars(vm => ({
|
||||
color: vm.color
|
||||
}))
|
||||
return state
|
||||
},
|
||||
render() {
|
||||
return h('div')
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
test('on HOCs', async () => {
|
||||
const Child = {
|
||||
render: () => h('div')
|
||||
}
|
||||
|
||||
await assertCssVars(state => ({
|
||||
setup() {
|
||||
useCssVars(() => state)
|
||||
return () => h(Child)
|
||||
}
|
||||
}))
|
||||
})
|
||||
})
|
@ -13,7 +13,8 @@ export default defineConfig({
|
||||
shared: resolve('src/shared'),
|
||||
web: resolve('src/platforms/web'),
|
||||
v3: resolve('src/v3'),
|
||||
vue: resolve('src/platforms/web/entry-runtime-with-compiler')
|
||||
vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
|
||||
types: resolve('src/types')
|
||||
}
|
||||
},
|
||||
define: {
|
||||
|
Loading…
Reference in New Issue
Block a user