feat(compiler-sfc): new SFC css varaible injection implementation
ref: https://github.com/vuejs/rfcs/pull/231
This commit is contained in:
parent
62372e9943
commit
41bb7fa330
@ -215,9 +215,10 @@ export function generate(
|
||||
}
|
||||
|
||||
// binding optimizations
|
||||
const optimizeSources = options.bindingMetadata
|
||||
? `, $props, $setup, $data, $options`
|
||||
: ``
|
||||
const optimizeSources =
|
||||
options.bindingMetadata && !options.inline
|
||||
? `, $props, $setup, $data, $options`
|
||||
: ``
|
||||
// enter render function
|
||||
if (!ssr) {
|
||||
if (isSetupInlined) {
|
||||
|
@ -113,17 +113,16 @@ export function processExpression(
|
||||
// it gets correct type
|
||||
return `__props.${raw}`
|
||||
}
|
||||
}
|
||||
|
||||
if (type === BindingTypes.CONST) {
|
||||
// setup const binding in non-inline mode
|
||||
return `$setup.${raw}`
|
||||
} else if (type) {
|
||||
return `$${type}.${raw}`
|
||||
} else {
|
||||
// fallback to ctx
|
||||
return `_ctx.${raw}`
|
||||
if (type === BindingTypes.CONST) {
|
||||
// setup const binding in non-inline mode
|
||||
return `$setup.${raw}`
|
||||
} else if (type) {
|
||||
return `$${type}.${raw}`
|
||||
}
|
||||
}
|
||||
// fallback to ctx
|
||||
return `_ctx.${raw}`
|
||||
}
|
||||
|
||||
// fast path if expression is a simple identifier.
|
||||
|
@ -1,62 +1,5 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SFC compile <script setup> CSS vars injection <script> w/ default export 1`] = `
|
||||
"const __default__ = { setup() {} }
|
||||
import { useCssVars as _useCssVars } from 'vue'
|
||||
const __injectCSSVars__ = () => {
|
||||
_useCssVars(_ctx => ({ color: _ctx.color }))
|
||||
}
|
||||
const __setup__ = __default__.setup
|
||||
__default__.setup = __setup__
|
||||
? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }
|
||||
: __injectCSSVars__
|
||||
export default __default__"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> CSS vars injection <script> w/ default export in strings/comments 1`] = `
|
||||
"
|
||||
// export default {}
|
||||
const __default__ = {}
|
||||
|
||||
import { useCssVars as _useCssVars } from 'vue'
|
||||
const __injectCSSVars__ = () => {
|
||||
_useCssVars(_ctx => ({ color: _ctx.color }))
|
||||
}
|
||||
const __setup__ = __default__.setup
|
||||
__default__.setup = __setup__
|
||||
? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }
|
||||
: __injectCSSVars__
|
||||
export default __default__"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> CSS vars injection <script> w/ no default export 1`] = `
|
||||
"const a = 1
|
||||
const __default__ = {}
|
||||
import { useCssVars as _useCssVars } from 'vue'
|
||||
const __injectCSSVars__ = () => {
|
||||
_useCssVars(_ctx => ({ color: _ctx.color }))
|
||||
}
|
||||
const __setup__ = __default__.setup
|
||||
__default__.setup = __setup__
|
||||
? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }
|
||||
: __injectCSSVars__
|
||||
export default __default__"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> CSS vars injection w/ <script setup> 1`] = `
|
||||
"import { useCssVars as _useCssVars } from 'vue'
|
||||
|
||||
export default {
|
||||
expose: [],
|
||||
setup() {
|
||||
const color = 'red'
|
||||
_useCssVars(_ctx => ({ color }))
|
||||
return { color }
|
||||
}
|
||||
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> defineOptions() 1`] = `
|
||||
"export default {
|
||||
expose: [],
|
||||
@ -86,7 +29,7 @@ export default {
|
||||
default: () => bar
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
setup(__props) {
|
||||
|
||||
|
||||
|
||||
@ -104,7 +47,7 @@ exports[`SFC compile <script setup> errors should allow defineOptions() referenc
|
||||
default: bar => bar + 1
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
setup(__props) {
|
||||
|
||||
const bar = 1
|
||||
|
||||
@ -121,7 +64,7 @@ import { ref } from 'vue'
|
||||
|
||||
export default {
|
||||
expose: [],
|
||||
setup() {
|
||||
setup(__props) {
|
||||
|
||||
const foo = _ref(1)
|
||||
|
||||
@ -136,7 +79,7 @@ exports[`SFC compile <script setup> imports import dedupe between <script> and <
|
||||
|
||||
export default {
|
||||
expose: [],
|
||||
setup() {
|
||||
setup(__props) {
|
||||
|
||||
x()
|
||||
|
||||
@ -152,7 +95,7 @@ exports[`SFC compile <script setup> imports should extract comment for import or
|
||||
|
||||
export default {
|
||||
expose: [],
|
||||
setup() {
|
||||
setup(__props) {
|
||||
|
||||
|
||||
return { a, b }
|
||||
@ -165,7 +108,7 @@ exports[`SFC compile <script setup> imports should hoist and expose imports 1`]
|
||||
"import { ref } from 'vue'
|
||||
export default {
|
||||
expose: [],
|
||||
setup() {
|
||||
setup(__props) {
|
||||
|
||||
return { ref }
|
||||
}
|
||||
@ -182,13 +125,13 @@ import { ref } from 'vue'
|
||||
|
||||
export default {
|
||||
expose: [],
|
||||
setup() {
|
||||
setup(__props) {
|
||||
|
||||
const count = ref(0)
|
||||
const constant = {}
|
||||
function fn() {}
|
||||
|
||||
return (_ctx, _cache, $props, $setup, $data, $options) => {
|
||||
return (_ctx, _cache) => {
|
||||
return (_openBlock(), _createBlock(_Fragment, null, [
|
||||
_createVNode(Foo),
|
||||
_createVNode(\\"div\\", { onClick: fn }, _toDisplayString(_unref(count)) + \\" \\" + _toDisplayString(constant) + \\" \\" + _toDisplayString(_unref(other)), 1 /* TEXT */)
|
||||
@ -208,11 +151,11 @@ import { ref } from 'vue'
|
||||
|
||||
export default {
|
||||
expose: [],
|
||||
setup() {
|
||||
setup(__props) {
|
||||
|
||||
const count = ref(0)
|
||||
|
||||
return (_ctx, _cache, $props, $setup, $data, $options) => {
|
||||
return (_ctx, _cache) => {
|
||||
return (_openBlock(), _createBlock(_Fragment, null, [
|
||||
_createVNode(\\"div\\", null, _toDisplayString(_unref(count)), 1 /* TEXT */),
|
||||
_hoisted_1
|
||||
@ -228,7 +171,7 @@ exports[`SFC compile <script setup> ref: syntax sugar accessing ref binding 1`]
|
||||
|
||||
export default {
|
||||
expose: [],
|
||||
setup() {
|
||||
setup(__props) {
|
||||
|
||||
const a = _ref(1)
|
||||
console.log(a.value)
|
||||
@ -247,7 +190,7 @@ exports[`SFC compile <script setup> ref: syntax sugar array destructure 1`] = `
|
||||
|
||||
export default {
|
||||
expose: [],
|
||||
setup() {
|
||||
setup(__props) {
|
||||
|
||||
const n = _ref(1), [__a, __b = 1, ...__c] = useFoo()
|
||||
const a = _ref(__a);
|
||||
@ -266,7 +209,7 @@ exports[`SFC compile <script setup> ref: syntax sugar convert ref declarations 1
|
||||
|
||||
export default {
|
||||
expose: [],
|
||||
setup() {
|
||||
setup(__props) {
|
||||
|
||||
const foo = _ref()
|
||||
const a = _ref(1)
|
||||
@ -287,7 +230,7 @@ exports[`SFC compile <script setup> ref: syntax sugar multi ref declarations 1`]
|
||||
|
||||
export default {
|
||||
expose: [],
|
||||
setup() {
|
||||
setup(__props) {
|
||||
|
||||
const a = _ref(1), b = _ref(2), c = _ref({
|
||||
count: 0
|
||||
@ -304,7 +247,7 @@ exports[`SFC compile <script setup> ref: syntax sugar mutating ref binding 1`] =
|
||||
|
||||
export default {
|
||||
expose: [],
|
||||
setup() {
|
||||
setup(__props) {
|
||||
|
||||
const a = _ref(1)
|
||||
const b = _ref({ count: 0 })
|
||||
@ -326,7 +269,7 @@ exports[`SFC compile <script setup> ref: syntax sugar nested destructure 1`] = `
|
||||
|
||||
export default {
|
||||
expose: [],
|
||||
setup() {
|
||||
setup(__props) {
|
||||
|
||||
const [{ a: { b: __b }}] = useFoo()
|
||||
const b = _ref(__b);
|
||||
@ -346,7 +289,7 @@ exports[`SFC compile <script setup> ref: syntax sugar object destructure 1`] = `
|
||||
|
||||
export default {
|
||||
expose: [],
|
||||
setup() {
|
||||
setup(__props) {
|
||||
|
||||
const n = _ref(1), { a: __a, b: __c, d: __d = 1, e: __f = 2, ...__g } = useFoo()
|
||||
const a = _ref(__a);
|
||||
@ -365,7 +308,7 @@ return { n, a, c, d, f, g }
|
||||
exports[`SFC compile <script setup> ref: syntax sugar should not convert non ref labels 1`] = `
|
||||
"export default {
|
||||
expose: [],
|
||||
setup() {
|
||||
setup(__props) {
|
||||
|
||||
foo: a = 1, b = 2, c = {
|
||||
count: 0
|
||||
@ -382,7 +325,7 @@ exports[`SFC compile <script setup> ref: syntax sugar using ref binding in prope
|
||||
|
||||
export default {
|
||||
expose: [],
|
||||
setup() {
|
||||
setup(__props) {
|
||||
|
||||
const a = _ref(1)
|
||||
const b = { a: a.value }
|
||||
@ -401,7 +344,7 @@ exports[`SFC compile <script setup> should expose top level declarations 1`] = `
|
||||
|
||||
export default {
|
||||
expose: [],
|
||||
setup() {
|
||||
setup(__props) {
|
||||
|
||||
let a = 1
|
||||
const b = 2
|
||||
@ -509,7 +452,7 @@ export default _defineComponent({
|
||||
literalUnionMixed: { type: [String, Number, Boolean], required: true },
|
||||
intersection: { type: Object, required: true }
|
||||
} as unknown as undefined,
|
||||
setup() {
|
||||
setup(__props) {
|
||||
|
||||
|
||||
|
||||
@ -526,7 +469,7 @@ export interface Foo {}
|
||||
|
||||
export default _defineComponent({
|
||||
expose: [],
|
||||
setup() {
|
||||
setup(__props) {
|
||||
|
||||
|
||||
return { }
|
||||
|
@ -0,0 +1,107 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CSS vars injection codegen <script> w/ default export 1`] = `
|
||||
"const __default__ = { setup() {} }
|
||||
import { useCssVars as _useCssVars } from 'vue'
|
||||
const __injectCSSVars__ = () => {
|
||||
_useCssVars(_ctx => ({
|
||||
color: (_ctx.color)
|
||||
}), \\"xxxxxxxx\\")}
|
||||
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(_ctx => ({
|
||||
color: (_ctx.color)
|
||||
}), \\"xxxxxxxx\\")}
|
||||
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(_ctx => ({
|
||||
color: (_ctx.color)
|
||||
}), \\"xxxxxxxx\\")}
|
||||
const __setup__ = __default__.setup
|
||||
__default__.setup = __setup__
|
||||
? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }
|
||||
: __injectCSSVars__
|
||||
export default __default__"
|
||||
`;
|
||||
|
||||
exports[`CSS vars injection codegen w/ <script setup> 1`] = `
|
||||
"import { useCssVars as _useCssVars, unref as _unref } from 'vue'
|
||||
|
||||
export default {
|
||||
expose: [],
|
||||
setup(__props) {
|
||||
|
||||
_useCssVars(_ctx => ({
|
||||
color: (color)
|
||||
}), \\"xxxxxxxx\\")
|
||||
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(_ctx => ({
|
||||
color: (_ctx.color),
|
||||
font_size: (_ctx.font.size)
|
||||
}), \\"xxxxxxxx\\")}
|
||||
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, unref as _unref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export default {
|
||||
expose: [],
|
||||
props: {
|
||||
foo: String
|
||||
},
|
||||
setup(__props) {
|
||||
|
||||
_useCssVars(_ctx => ({
|
||||
color: (color),
|
||||
size: (_unref(size)),
|
||||
foo: (__props.foo)
|
||||
}), \\"xxxxxxxx\\")
|
||||
|
||||
const color = 'red'
|
||||
const size = ref('10px')
|
||||
|
||||
|
||||
return { color, size, ref }
|
||||
}
|
||||
|
||||
}"
|
||||
`;
|
@ -1,25 +1,4 @@
|
||||
import { parse, SFCScriptCompileOptions, compileScript } from '../src'
|
||||
import { parse as babelParse } from '@babel/parser'
|
||||
import { babelParserDefaultPlugins } from '@vue/shared'
|
||||
|
||||
function compile(src: string, options?: SFCScriptCompileOptions) {
|
||||
const { descriptor } = parse(src)
|
||||
return compileScript(descriptor, options)
|
||||
}
|
||||
|
||||
function assertCode(code: string) {
|
||||
// parse the generated code to make sure it is valid
|
||||
try {
|
||||
babelParse(code, {
|
||||
sourceType: 'module',
|
||||
plugins: [...babelParserDefaultPlugins, 'typescript']
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(code)
|
||||
throw e
|
||||
}
|
||||
expect(code).toMatchSnapshot()
|
||||
}
|
||||
import { compileSFCScript as compile, assertCode } from './utils'
|
||||
|
||||
describe('SFC compile <script setup>', () => {
|
||||
test('should expose top level declarations', () => {
|
||||
@ -323,51 +302,10 @@ const { props, emit } = defineOptions({
|
||||
})
|
||||
})
|
||||
|
||||
describe('CSS vars injection', () => {
|
||||
test('<script> w/ no default export', () => {
|
||||
assertCode(
|
||||
compile(
|
||||
`<script>const a = 1</script>\n` +
|
||||
`<style vars="{ color }">div{ color: var(--color); }</style>`
|
||||
).content
|
||||
)
|
||||
})
|
||||
|
||||
test('<script> w/ default export', () => {
|
||||
assertCode(
|
||||
compile(
|
||||
`<script>export default { setup() {} }</script>\n` +
|
||||
`<style vars="{ color }">div{ color: var(--color); }</style>`
|
||||
).content
|
||||
)
|
||||
})
|
||||
|
||||
test('<script> w/ default export in strings/comments', () => {
|
||||
assertCode(
|
||||
compile(
|
||||
`<script>
|
||||
// export default {}
|
||||
export default {}
|
||||
</script>\n` +
|
||||
`<style vars="{ color }">div{ color: var(--color); }</style>`
|
||||
).content
|
||||
)
|
||||
})
|
||||
|
||||
test('w/ <script setup>', () => {
|
||||
assertCode(
|
||||
compile(
|
||||
`<script setup>const color = 'red'</script>\n` +
|
||||
`<style vars="{ color }">div{ color: var(--color); }</style>`
|
||||
).content
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('async/await detection', () => {
|
||||
function assertAwaitDetection(code: string, shouldAsync = true) {
|
||||
const { content } = compile(`<script setup>${code}</script>`)
|
||||
expect(content).toMatch(`${shouldAsync ? `async ` : ``}setup()`)
|
||||
expect(content).toMatch(`${shouldAsync ? `async ` : ``}setup(`)
|
||||
}
|
||||
|
||||
test('expression statement', () => {
|
||||
|
@ -9,27 +9,27 @@ import {
|
||||
} from '../src/compileStyle'
|
||||
import path from 'path'
|
||||
|
||||
describe('SFC scoped CSS', () => {
|
||||
function compileScoped(
|
||||
source: string,
|
||||
options?: Partial<SFCStyleCompileOptions>
|
||||
): string {
|
||||
const res = compileStyle({
|
||||
source,
|
||||
filename: 'test.css',
|
||||
id: 'test',
|
||||
scoped: true,
|
||||
...options
|
||||
export function compileScoped(
|
||||
source: string,
|
||||
options?: Partial<SFCStyleCompileOptions>
|
||||
): string {
|
||||
const res = compileStyle({
|
||||
source,
|
||||
filename: 'test.css',
|
||||
id: 'test',
|
||||
scoped: true,
|
||||
...options
|
||||
})
|
||||
if (res.errors.length) {
|
||||
res.errors.forEach(err => {
|
||||
console.error(err)
|
||||
})
|
||||
if (res.errors.length) {
|
||||
res.errors.forEach(err => {
|
||||
console.error(err)
|
||||
})
|
||||
expect(res.errors.length).toBe(0)
|
||||
}
|
||||
return res.code
|
||||
expect(res.errors.length).toBe(0)
|
||||
}
|
||||
return res.code
|
||||
}
|
||||
|
||||
describe('SFC scoped CSS', () => {
|
||||
test('simple selectors', () => {
|
||||
expect(compileScoped(`h1 { color: red; }`)).toMatch(
|
||||
`h1[test] { color: red;`
|
||||
@ -266,27 +266,6 @@ describe('SFC scoped CSS', () => {
|
||||
).toHaveBeenWarned()
|
||||
})
|
||||
})
|
||||
|
||||
describe('<style vars>', () => {
|
||||
test('should rewrite CSS vars in scoped mode', () => {
|
||||
const code = compileScoped(
|
||||
`.foo {
|
||||
color: var(--color);
|
||||
font-size: var(--global:font);
|
||||
}`,
|
||||
{
|
||||
id: 'data-v-test',
|
||||
vars: true
|
||||
}
|
||||
)
|
||||
expect(code).toMatchInlineSnapshot(`
|
||||
".foo[data-v-test] {
|
||||
color: var(--test-color);
|
||||
font-size: var(--font);
|
||||
}"
|
||||
`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('SFC CSS modules', () => {
|
||||
|
111
packages/compiler-sfc/__tests__/cssVars.spec.ts
Normal file
111
packages/compiler-sfc/__tests__/cssVars.spec.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { compileStyle } from '../src'
|
||||
import { compileSFCScript, assertCode } from './utils'
|
||||
|
||||
describe('CSS vars injection', () => {
|
||||
describe('codegen', () => {
|
||||
test('<script> w/ no default export', () => {
|
||||
assertCode(
|
||||
compileSFCScript(
|
||||
`<script>const a = 1</script>\n` +
|
||||
`<style>div{ color: var(--v-bind:color); }</style>`
|
||||
).content
|
||||
)
|
||||
})
|
||||
|
||||
test('<script> w/ default export', () => {
|
||||
assertCode(
|
||||
compileSFCScript(
|
||||
`<script>export default { setup() {} }</script>\n` +
|
||||
`<style>div{ color: var(--:color); }</style>`
|
||||
).content
|
||||
)
|
||||
})
|
||||
|
||||
test('<script> w/ default export in strings/comments', () => {
|
||||
assertCode(
|
||||
compileSFCScript(
|
||||
`<script>
|
||||
// export default {}
|
||||
export default {}
|
||||
</script>\n` + `<style>div{ color: var(--:color); }</style>`
|
||||
).content
|
||||
)
|
||||
})
|
||||
|
||||
test('w/ <script setup>', () => {
|
||||
assertCode(
|
||||
compileSFCScript(
|
||||
`<script setup>const color = 'red'</script>\n` +
|
||||
`<style>div{ color: var(--:color); }</style>`
|
||||
).content
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test('generating correct code for nested paths', () => {
|
||||
const { content } = compileSFCScript(
|
||||
`<script>const a = 1</script>\n` +
|
||||
`<style>div{
|
||||
color: var(--v-bind:color);
|
||||
color: var(--v-bind:font.size);
|
||||
}</style>`
|
||||
)
|
||||
expect(content).toMatch(`_useCssVars(_ctx => ({
|
||||
color: (_ctx.color),
|
||||
font_size: (_ctx.font.size)
|
||||
})`)
|
||||
assertCode(content)
|
||||
})
|
||||
|
||||
test('w/ <script setup> binding analysis', () => {
|
||||
const { content } = compileSFCScript(
|
||||
`<script setup>
|
||||
import { defineOptions, ref } from 'vue'
|
||||
const color = 'red'
|
||||
const size = ref('10px')
|
||||
defineOptions({
|
||||
props: {
|
||||
foo: String
|
||||
}
|
||||
})
|
||||
</script>\n` +
|
||||
`<style>
|
||||
div {
|
||||
color: var(--:color);
|
||||
font-size: var(--:size);
|
||||
border: var(--:foo);
|
||||
}
|
||||
</style>`
|
||||
)
|
||||
// should handle:
|
||||
// 1. local const bindings
|
||||
// 2. local potential ref bindings
|
||||
// 3. props bindings (analyzed)
|
||||
expect(content).toMatch(`_useCssVars(_ctx => ({
|
||||
color: (color),
|
||||
size: (_unref(size)),
|
||||
foo: (__props.foo)
|
||||
})`)
|
||||
expect(content).toMatch(
|
||||
`import { useCssVars as _useCssVars, unref as _unref } from 'vue'`
|
||||
)
|
||||
assertCode(content)
|
||||
})
|
||||
|
||||
test('should rewrite CSS vars in scoped mode', () => {
|
||||
const { code } = compileStyle({
|
||||
source: `.foo {
|
||||
color: var(--v-bind:color);
|
||||
font-size: var(--:font.size);
|
||||
}`,
|
||||
filename: 'test.css',
|
||||
id: 'data-v-test'
|
||||
})
|
||||
expect(code).toMatchInlineSnapshot(`
|
||||
".foo {
|
||||
color: var(--test-color);
|
||||
font-size: var(--test-font_size);
|
||||
}"
|
||||
`)
|
||||
})
|
||||
})
|
28
packages/compiler-sfc/__tests__/utils.ts
Normal file
28
packages/compiler-sfc/__tests__/utils.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { parse, SFCScriptCompileOptions, compileScript } from '../src'
|
||||
import { parse as babelParse } from '@babel/parser'
|
||||
import { babelParserDefaultPlugins } from '@vue/shared'
|
||||
|
||||
export function compileSFCScript(
|
||||
src: string,
|
||||
options?: Partial<SFCScriptCompileOptions>
|
||||
) {
|
||||
const { descriptor } = parse(src)
|
||||
return compileScript(descriptor, {
|
||||
...options,
|
||||
id: 'xxxxxxxx'
|
||||
})
|
||||
}
|
||||
|
||||
export function assertCode(code: string) {
|
||||
// parse the generated code to make sure it is valid
|
||||
try {
|
||||
babelParse(code, {
|
||||
sourceType: 'module',
|
||||
plugins: [...babelParserDefaultPlugins, 'typescript']
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(code)
|
||||
throw e
|
||||
}
|
||||
expect(code).toMatchSnapshot()
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import MagicString from 'magic-string'
|
||||
import { BindingMetadata, BindingTypes } from '@vue/compiler-core'
|
||||
import { BindingMetadata, BindingTypes, UNREF } from '@vue/compiler-core'
|
||||
import { SFCDescriptor, SFCScriptBlock } from './parse'
|
||||
import { parse as _parse, ParserOptions, ParserPlugin } from '@babel/parser'
|
||||
import { babelParserDefaultPlugins, generateCodeFrame } from '@vue/shared'
|
||||
@ -26,14 +26,20 @@ import { walk } from 'estree-walker'
|
||||
import { RawSourceMap } from 'source-map'
|
||||
import {
|
||||
CSS_VARS_HELPER,
|
||||
parseCssVars,
|
||||
genCssVarsCode,
|
||||
injectCssVarsCalls
|
||||
} from './genCssVars'
|
||||
} from './cssVars'
|
||||
import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate'
|
||||
|
||||
const DEFINE_OPTIONS = 'defineOptions'
|
||||
|
||||
export interface SFCScriptCompileOptions {
|
||||
/**
|
||||
* Scope ID for prefixing injected CSS varialbes.
|
||||
* This must be consistent with the `id` passed to `compileStyle`.
|
||||
*/
|
||||
id: string
|
||||
/**
|
||||
* https://babeljs.io/docs/en/babel-parser#plugins
|
||||
*/
|
||||
@ -52,7 +58,7 @@ export interface SFCScriptCompileOptions {
|
||||
* from being hot-reloaded separately from component state.
|
||||
*/
|
||||
inlineTemplate?: boolean
|
||||
templateOptions?: SFCTemplateCompileOptions
|
||||
templateOptions?: Partial<SFCTemplateCompileOptions>
|
||||
}
|
||||
|
||||
const hasWarned: Record<string, boolean> = {}
|
||||
@ -71,19 +77,33 @@ function warnOnce(msg: string) {
|
||||
*/
|
||||
export function compileScript(
|
||||
sfc: SFCDescriptor,
|
||||
options: SFCScriptCompileOptions = {}
|
||||
options: SFCScriptCompileOptions
|
||||
): SFCScriptBlock {
|
||||
const { script, scriptSetup, styles, source, filename } = sfc
|
||||
const { script, scriptSetup, source, filename } = sfc
|
||||
|
||||
if (__DEV__ && !__TEST__ && scriptSetup) {
|
||||
warnOnce(
|
||||
`<script setup> is still an experimental proposal.\n` +
|
||||
`Follow its status at https://github.com/vuejs/rfcs/pull/227.`
|
||||
`Follow its status at https://github.com/vuejs/rfcs/pull/227.\n` +
|
||||
`It's also recommended to pin your vue dependencies to exact versions ` +
|
||||
`to avoid breakage.`
|
||||
)
|
||||
}
|
||||
|
||||
const hasCssVars = styles.some(s => typeof s.attrs.vars === 'string')
|
||||
// for backwards compat
|
||||
if (!options) {
|
||||
options = { id: '' }
|
||||
}
|
||||
if (!options.id) {
|
||||
warnOnce(
|
||||
`compileScript now requires passing the \`id\` option.\n` +
|
||||
`Upgrade your vite or vue-loader version for compatibility with ` +
|
||||
`the latest experimental proposals.`
|
||||
)
|
||||
}
|
||||
|
||||
const scopeId = options.id ? options.id.replace(/^data-v-/, '') : ''
|
||||
const cssVars = parseCssVars(sfc)
|
||||
const scriptLang = script && script.lang
|
||||
const scriptSetupLang = scriptSetup && scriptSetup.lang
|
||||
const isTS = scriptLang === 'ts' || scriptSetupLang === 'ts'
|
||||
@ -104,10 +124,13 @@ export function compileScript(
|
||||
plugins,
|
||||
sourceType: 'module'
|
||||
}).program.body
|
||||
const bindings = analyzeScriptBindings(scriptAst)
|
||||
return {
|
||||
...script,
|
||||
content: hasCssVars ? injectCssVarsCalls(sfc, plugins) : script.content,
|
||||
bindings: analyzeScriptBindings(scriptAst),
|
||||
content: cssVars.length
|
||||
? injectCssVarsCalls(sfc, cssVars, bindings, scopeId, plugins)
|
||||
: script.content,
|
||||
bindings,
|
||||
scriptAst
|
||||
}
|
||||
} catch (e) {
|
||||
@ -491,7 +514,9 @@ export function compileScript(
|
||||
warnOnce(
|
||||
`ref: sugar is still an experimental proposal and is not ` +
|
||||
`guaranteed to be a part of <script setup>.\n` +
|
||||
`Follow its status at https://github.com/vuejs/rfcs/pull/228.`
|
||||
`Follow its status at https://github.com/vuejs/rfcs/pull/228.\n` +
|
||||
`It's also recommended to pin your vue dependencies to exact versions ` +
|
||||
`to avoid breakage.`
|
||||
)
|
||||
s.overwrite(
|
||||
node.label.start! + startOffset,
|
||||
@ -512,10 +537,22 @@ export function compileScript(
|
||||
if (node.type === 'ImportDeclaration') {
|
||||
// import declarations are moved to top
|
||||
s.move(start, end, 0)
|
||||
|
||||
// dedupe imports
|
||||
let prev
|
||||
let removed = 0
|
||||
for (const specifier of node.specifiers) {
|
||||
let prev: Node | undefined, next: Node | undefined
|
||||
const removeSpecifier = (node: Node) => {
|
||||
removed++
|
||||
s.remove(
|
||||
prev ? prev.end! + startOffset : node.start! + startOffset,
|
||||
next ? next.start! + startOffset : node.end! + startOffset
|
||||
)
|
||||
}
|
||||
|
||||
for (let i = 0; i < node.specifiers.length; i++) {
|
||||
const specifier = node.specifiers[i]
|
||||
prev = node.specifiers[i - 1]
|
||||
next = node.specifiers[i + 1]
|
||||
const local = specifier.local.name
|
||||
const imported =
|
||||
specifier.type === 'ImportSpecifier' &&
|
||||
@ -524,19 +561,11 @@ export function compileScript(
|
||||
const source = node.source.value
|
||||
const existing = userImports[local]
|
||||
if (source === 'vue' && imported === DEFINE_OPTIONS) {
|
||||
removed++
|
||||
s.remove(
|
||||
prev ? prev.end! + startOffset : specifier.start! + startOffset,
|
||||
specifier.end! + startOffset
|
||||
)
|
||||
removeSpecifier(specifier)
|
||||
} else if (existing) {
|
||||
if (existing.source === source && existing.imported === imported) {
|
||||
// already imported in <script setup>, dedupe
|
||||
removed++
|
||||
s.remove(
|
||||
prev ? prev.end! + startOffset : specifier.start! + startOffset,
|
||||
specifier.end! + startOffset
|
||||
)
|
||||
removeSpecifier(specifier)
|
||||
} else {
|
||||
error(`different imports aliased to same local name.`, specifier)
|
||||
}
|
||||
@ -546,7 +575,6 @@ export function compileScript(
|
||||
source: node.source.value
|
||||
}
|
||||
}
|
||||
prev = specifier
|
||||
}
|
||||
if (removed === node.specifiers.length) {
|
||||
s.remove(node.start! + startOffset, node.end! + startOffset)
|
||||
@ -732,7 +760,7 @@ export function compileScript(
|
||||
}
|
||||
|
||||
// 7. finalize setup argument signature.
|
||||
let args = optionsExp ? `__props, ${optionsExp}` : ``
|
||||
let args = optionsExp ? `__props, ${optionsExp}` : `__props`
|
||||
if (optionsExp && optionsType) {
|
||||
if (slotsType === 'Slots') {
|
||||
helperImports.add('Slots')
|
||||
@ -745,26 +773,7 @@ export function compileScript(
|
||||
}`
|
||||
}
|
||||
|
||||
const allBindings: Record<string, any> = { ...setupBindings }
|
||||
for (const key in userImports) {
|
||||
allBindings[key] = true
|
||||
}
|
||||
|
||||
// 8. inject `useCssVars` calls
|
||||
if (hasCssVars) {
|
||||
helperImports.add(CSS_VARS_HELPER)
|
||||
for (const style of styles) {
|
||||
const vars = style.attrs.vars
|
||||
if (typeof vars === 'string') {
|
||||
s.prependRight(
|
||||
endOffset,
|
||||
`\n${genCssVarsCode(vars, !!style.scoped, allBindings)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 9. analyze binding metadata
|
||||
// 8. analyze binding metadata
|
||||
if (scriptAst) {
|
||||
Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst))
|
||||
}
|
||||
@ -785,13 +794,23 @@ export function compileScript(
|
||||
bindingMetadata[key] = setupBindings[key]
|
||||
}
|
||||
|
||||
// 9. inject `useCssVars` calls
|
||||
if (cssVars.length) {
|
||||
helperImports.add(CSS_VARS_HELPER)
|
||||
helperImports.add('unref')
|
||||
s.prependRight(
|
||||
startOffset,
|
||||
`\n${genCssVarsCode(cssVars, bindingMetadata, scopeId)}\n`
|
||||
)
|
||||
}
|
||||
|
||||
// 10. generate return statement
|
||||
let returned
|
||||
if (options.inlineTemplate) {
|
||||
if (sfc.template) {
|
||||
// inline render function mode - we are going to compile the template and
|
||||
// inline it right here
|
||||
const { code, preamble, tips, errors } = compileTemplate({
|
||||
const { code, ast, preamble, tips, errors } = compileTemplate({
|
||||
...options.templateOptions,
|
||||
filename,
|
||||
source: sfc.template.content,
|
||||
@ -813,12 +832,22 @@ export function compileScript(
|
||||
if (preamble) {
|
||||
s.prepend(preamble)
|
||||
}
|
||||
// avoid duplicated unref import
|
||||
// as this may get injected by the render function preamble OR the
|
||||
// css vars codegen
|
||||
if (ast && ast.helpers.includes(UNREF)) {
|
||||
helperImports.delete('unref')
|
||||
}
|
||||
returned = code
|
||||
} else {
|
||||
returned = `() => {}`
|
||||
}
|
||||
} else {
|
||||
// return bindings from setup
|
||||
const allBindings: Record<string, any> = { ...setupBindings }
|
||||
for (const key in userImports) {
|
||||
allBindings[key] = true
|
||||
}
|
||||
returned = `{ ${Object.keys(allBindings).join(', ')} }`
|
||||
}
|
||||
s.appendRight(endOffset, `\nreturn ${returned}\n}\n\n`)
|
||||
|
@ -7,7 +7,6 @@ import postcss, {
|
||||
} from 'postcss'
|
||||
import trimPlugin from './stylePluginTrim'
|
||||
import scopedPlugin from './stylePluginScoped'
|
||||
import scopedVarsPlugin from './stylePluginScopedVars'
|
||||
import {
|
||||
processors,
|
||||
StylePreprocessor,
|
||||
@ -15,6 +14,7 @@ import {
|
||||
PreprocessLang
|
||||
} from './stylePreprocessors'
|
||||
import { RawSourceMap } from 'source-map'
|
||||
import { cssVarsPlugin } from './cssVars'
|
||||
|
||||
export interface SFCStyleCompileOptions {
|
||||
source: string
|
||||
@ -22,7 +22,6 @@ export interface SFCStyleCompileOptions {
|
||||
id: string
|
||||
map?: RawSourceMap
|
||||
scoped?: boolean
|
||||
vars?: boolean
|
||||
trim?: boolean
|
||||
preprocessLang?: PreprocessLang
|
||||
preprocessOptions?: any
|
||||
@ -82,7 +81,6 @@ export function doCompileStyle(
|
||||
filename,
|
||||
id,
|
||||
scoped = false,
|
||||
vars = false,
|
||||
trim = true,
|
||||
modules = false,
|
||||
modulesOptions = {},
|
||||
@ -96,11 +94,7 @@ export function doCompileStyle(
|
||||
const source = preProcessedSource ? preProcessedSource.code : options.source
|
||||
|
||||
const plugins = (postcssPlugins || []).slice()
|
||||
if (vars && scoped) {
|
||||
// vars + scoped, only applies to raw source before other transforms
|
||||
// #1623
|
||||
plugins.unshift(scopedVarsPlugin(id))
|
||||
}
|
||||
plugins.unshift(cssVarsPlugin(id))
|
||||
if (trim) {
|
||||
plugins.push(trimPlugin())
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ export interface TemplateCompiler {
|
||||
|
||||
export interface SFCTemplateCompileResults {
|
||||
code: string
|
||||
ast?: RootNode
|
||||
preamble?: string
|
||||
source: string
|
||||
tips: string[]
|
||||
@ -169,7 +170,7 @@ function doCompileTemplate({
|
||||
nodeTransforms = [transformAssetUrl, transformSrcset]
|
||||
}
|
||||
|
||||
let { code, preamble, map } = compiler.compile(source, {
|
||||
let { code, ast, preamble, map } = compiler.compile(source, {
|
||||
mode: 'module',
|
||||
prefixIdentifiers: true,
|
||||
hoistStatic: true,
|
||||
@ -193,7 +194,7 @@ function doCompileTemplate({
|
||||
}
|
||||
}
|
||||
|
||||
return { code, preamble, source, errors, tips: [], map }
|
||||
return { code, ast, preamble, source, errors, tips: [], map }
|
||||
}
|
||||
|
||||
function mapLines(oldMap: RawSourceMap, newMap: RawSourceMap): RawSourceMap {
|
||||
|
@ -4,30 +4,62 @@ import {
|
||||
createSimpleExpression,
|
||||
createRoot,
|
||||
NodeTypes,
|
||||
SimpleExpressionNode
|
||||
SimpleExpressionNode,
|
||||
BindingMetadata
|
||||
} from '@vue/compiler-dom'
|
||||
import { SFCDescriptor } from './parse'
|
||||
import { rewriteDefault } from './rewriteDefault'
|
||||
import { ParserPlugin } from '@babel/parser'
|
||||
import postcss, { Root } from 'postcss'
|
||||
|
||||
export const CSS_VARS_HELPER = `useCssVars`
|
||||
export const cssVarRE = /\bvar\(--(?:v-bind)?:([^)]+)\)/g
|
||||
|
||||
export function convertCssVarCasing(raw: string): string {
|
||||
return raw.replace(/([^\w-])/g, '_')
|
||||
}
|
||||
|
||||
export function parseCssVars(sfc: SFCDescriptor): string[] {
|
||||
const vars: string[] = []
|
||||
sfc.styles.forEach(style => {
|
||||
let match
|
||||
while ((match = cssVarRE.exec(style.content))) {
|
||||
vars.push(match[1])
|
||||
}
|
||||
})
|
||||
return vars
|
||||
}
|
||||
|
||||
// for compileStyle
|
||||
export const cssVarsPlugin = postcss.plugin(
|
||||
'vue-scoped',
|
||||
(id: any) => (root: Root) => {
|
||||
const shortId = id.replace(/^data-v-/, '')
|
||||
root.walkDecls(decl => {
|
||||
// rewrite CSS variables
|
||||
if (cssVarRE.test(decl.value)) {
|
||||
decl.value = decl.value.replace(cssVarRE, (_, $1) => {
|
||||
return `var(--${shortId}-${convertCssVarCasing($1)})`
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
export function genCssVarsCode(
|
||||
varsExp: string,
|
||||
scoped: boolean,
|
||||
knownBindings?: Record<string, any>
|
||||
vars: string[],
|
||||
bindings: BindingMetadata,
|
||||
id: string
|
||||
) {
|
||||
const varsExp = `{\n ${vars
|
||||
.map(v => `${convertCssVarCasing(v)}: (${v})`)
|
||||
.join(',\n ')}\n}`
|
||||
const exp = createSimpleExpression(varsExp, false)
|
||||
const context = createTransformContext(createRoot([]), {
|
||||
prefixIdentifiers: true
|
||||
prefixIdentifiers: true,
|
||||
inline: true,
|
||||
bindingMetadata: bindings
|
||||
})
|
||||
if (knownBindings) {
|
||||
// when compiling <script setup> we already know what bindings are exposed
|
||||
// so we can avoid prefixing them from the ctx.
|
||||
for (const key in knownBindings) {
|
||||
context.identifiers[key] = 1
|
||||
}
|
||||
}
|
||||
const transformed = processExpression(exp, context)
|
||||
const transformedString =
|
||||
transformed.type === NodeTypes.SIMPLE_EXPRESSION
|
||||
@ -40,15 +72,16 @@ export function genCssVarsCode(
|
||||
})
|
||||
.join('')
|
||||
|
||||
return `_${CSS_VARS_HELPER}(_ctx => (${transformedString})${
|
||||
scoped ? `, true` : ``
|
||||
})`
|
||||
return `_${CSS_VARS_HELPER}(_ctx => (${transformedString}), "${id}")`
|
||||
}
|
||||
|
||||
// <script setup> already gets the calls injected as part of the transform
|
||||
// this is only for single normal <script>
|
||||
export function injectCssVarsCalls(
|
||||
sfc: SFCDescriptor,
|
||||
cssVars: string[],
|
||||
bindings: BindingMetadata,
|
||||
id: string,
|
||||
parserPlugins: ParserPlugin[]
|
||||
): string {
|
||||
const script = rewriteDefault(
|
||||
@ -57,18 +90,14 @@ export function injectCssVarsCalls(
|
||||
parserPlugins
|
||||
)
|
||||
|
||||
let calls = ``
|
||||
for (const style of sfc.styles) {
|
||||
const vars = style.attrs.vars
|
||||
if (typeof vars === 'string') {
|
||||
calls += genCssVarsCode(vars, !!style.scoped) + '\n'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
script +
|
||||
`\nimport { ${CSS_VARS_HELPER} as _${CSS_VARS_HELPER} } from 'vue'\n` +
|
||||
`const __injectCSSVars__ = () => {\n${calls}}\n` +
|
||||
`const __injectCSSVars__ = () => {\n${genCssVarsCode(
|
||||
cssVars,
|
||||
bindings,
|
||||
id
|
||||
)}}\n` +
|
||||
`const __setup__ = __default__.setup\n` +
|
||||
`__default__.setup = __setup__\n` +
|
||||
` ? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }\n` +
|
@ -45,7 +45,6 @@ export interface SFCScriptBlock extends SFCBlock {
|
||||
export interface SFCStyleBlock extends SFCBlock {
|
||||
type: 'style'
|
||||
scoped?: boolean
|
||||
vars?: string
|
||||
module?: string | boolean
|
||||
}
|
||||
|
||||
@ -269,8 +268,6 @@ function createBlock(
|
||||
} else if (type === 'style') {
|
||||
if (p.name === 'scoped') {
|
||||
;(block as SFCStyleBlock).scoped = true
|
||||
} else if (p.name === 'vars' && typeof attrs.vars === 'string') {
|
||||
;(block as SFCStyleBlock).vars = attrs.vars
|
||||
} else if (p.name === 'module') {
|
||||
;(block as SFCStyleBlock).module = attrs[p.name]
|
||||
}
|
||||
|
@ -1,15 +0,0 @@
|
||||
import postcss, { Root } from 'postcss'
|
||||
|
||||
const cssVarRE = /\bvar\(--(global:)?([^)]+)\)/g
|
||||
|
||||
export default postcss.plugin('vue-scoped', (id: any) => (root: Root) => {
|
||||
const shortId = id.replace(/^data-v-/, '')
|
||||
root.walkDecls(decl => {
|
||||
// rewrite CSS variables
|
||||
if (cssVarRE.test(decl.value)) {
|
||||
decl.value = decl.value.replace(cssVarRE, (_, $1, $2) => {
|
||||
return $1 ? `var(--${$2})` : `var(--${shortId}-${$2})`
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
@ -10,28 +10,26 @@ import {
|
||||
} from '@vue/runtime-dom'
|
||||
|
||||
describe('useCssVars', () => {
|
||||
async function assertCssVars(
|
||||
getApp: (state: any) => ComponentOptions,
|
||||
scopeId?: string
|
||||
) {
|
||||
const id = 'xxxxxx'
|
||||
async function assertCssVars(getApp: (state: any) => ComponentOptions) {
|
||||
const state = reactive({ color: 'red' })
|
||||
const App = getApp(state)
|
||||
const root = document.createElement('div')
|
||||
const prefix = scopeId ? `${scopeId.replace(/^data-v-/, '')}-` : ``
|
||||
|
||||
render(h(App), root)
|
||||
await nextTick()
|
||||
for (const c of [].slice.call(root.children as any)) {
|
||||
expect(
|
||||
(c as HTMLElement).style.getPropertyValue(`--${prefix}color`)
|
||||
).toBe(`red`)
|
||||
expect((c as HTMLElement).style.getPropertyValue(`--${id}-color`)).toBe(
|
||||
`red`
|
||||
)
|
||||
}
|
||||
|
||||
state.color = 'green'
|
||||
await nextTick()
|
||||
for (const c of [].slice.call(root.children as any)) {
|
||||
expect(
|
||||
(c as HTMLElement).style.getPropertyValue(`--${prefix}color`)
|
||||
).toBe('green')
|
||||
expect((c as HTMLElement).style.getPropertyValue(`--${id}-color`)).toBe(
|
||||
'green'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,9 +37,12 @@ describe('useCssVars', () => {
|
||||
await assertCssVars(state => ({
|
||||
setup() {
|
||||
// test receiving render context
|
||||
useCssVars((ctx: any) => ({
|
||||
color: ctx.color
|
||||
}))
|
||||
useCssVars(
|
||||
(ctx: any) => ({
|
||||
color: ctx.color
|
||||
}),
|
||||
id
|
||||
)
|
||||
return state
|
||||
},
|
||||
render() {
|
||||
@ -53,7 +54,7 @@ describe('useCssVars', () => {
|
||||
test('on fragment root', async () => {
|
||||
await assertCssVars(state => ({
|
||||
setup() {
|
||||
useCssVars(() => state)
|
||||
useCssVars(() => state, id)
|
||||
return () => [h('div'), h('div')]
|
||||
}
|
||||
}))
|
||||
@ -64,7 +65,7 @@ describe('useCssVars', () => {
|
||||
|
||||
await assertCssVars(state => ({
|
||||
setup() {
|
||||
useCssVars(() => state)
|
||||
useCssVars(() => state, id)
|
||||
return () => h(Child)
|
||||
}
|
||||
}))
|
||||
@ -74,15 +75,23 @@ describe('useCssVars', () => {
|
||||
const state = reactive({ color: 'red' })
|
||||
const root = document.createElement('div')
|
||||
|
||||
let resolveAsync: any
|
||||
let asyncPromise: any
|
||||
|
||||
const AsyncComp = {
|
||||
async setup() {
|
||||
return () => h('p', 'default')
|
||||
setup() {
|
||||
asyncPromise = new Promise(r => {
|
||||
resolveAsync = () => {
|
||||
r(() => h('p', 'default'))
|
||||
}
|
||||
})
|
||||
return asyncPromise
|
||||
}
|
||||
}
|
||||
|
||||
const App = {
|
||||
setup() {
|
||||
useCssVars(() => state)
|
||||
useCssVars(() => state, id)
|
||||
return () =>
|
||||
h(Suspense, null, {
|
||||
default: h(AsyncComp),
|
||||
@ -92,39 +101,42 @@ describe('useCssVars', () => {
|
||||
}
|
||||
|
||||
render(h(App), root)
|
||||
await nextTick()
|
||||
// css vars use with fallback tree
|
||||
for (const c of [].slice.call(root.children as any)) {
|
||||
expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe(`red`)
|
||||
expect((c as HTMLElement).style.getPropertyValue(`--${id}-color`)).toBe(
|
||||
`red`
|
||||
)
|
||||
}
|
||||
// AsyncComp resolve
|
||||
await nextTick()
|
||||
resolveAsync()
|
||||
await asyncPromise.then(() => {})
|
||||
// Suspense effects flush
|
||||
await nextTick()
|
||||
// css vars use with default tree
|
||||
for (const c of [].slice.call(root.children as any)) {
|
||||
expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe(`red`)
|
||||
expect((c as HTMLElement).style.getPropertyValue(`--${id}-color`)).toBe(
|
||||
`red`
|
||||
)
|
||||
}
|
||||
|
||||
state.color = 'green'
|
||||
await nextTick()
|
||||
for (const c of [].slice.call(root.children as any)) {
|
||||
expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe('green')
|
||||
expect((c as HTMLElement).style.getPropertyValue(`--${id}-color`)).toBe(
|
||||
'green'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
test('with <style scoped>', async () => {
|
||||
const id = 'data-v-12345'
|
||||
|
||||
await assertCssVars(
|
||||
state => ({
|
||||
__scopeId: id,
|
||||
setup() {
|
||||
useCssVars(() => state, true)
|
||||
return () => h('div')
|
||||
}
|
||||
}),
|
||||
id
|
||||
)
|
||||
await assertCssVars(state => ({
|
||||
__scopeId: id,
|
||||
setup() {
|
||||
useCssVars(() => state, id)
|
||||
return () => h('div')
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
test('with subTree changed', async () => {
|
||||
@ -134,21 +146,26 @@ describe('useCssVars', () => {
|
||||
|
||||
const App = {
|
||||
setup() {
|
||||
useCssVars(() => state)
|
||||
useCssVars(() => state, id)
|
||||
return () => (value.value ? [h('div')] : [h('div'), h('div')])
|
||||
}
|
||||
}
|
||||
|
||||
render(h(App), root)
|
||||
await nextTick()
|
||||
// css vars use with fallback tree
|
||||
for (const c of [].slice.call(root.children as any)) {
|
||||
expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe(`red`)
|
||||
expect((c as HTMLElement).style.getPropertyValue(`--${id}-color`)).toBe(
|
||||
`red`
|
||||
)
|
||||
}
|
||||
|
||||
value.value = false
|
||||
await nextTick()
|
||||
for (const c of [].slice.call(root.children as any)) {
|
||||
expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe('red')
|
||||
expect((c as HTMLElement).style.getPropertyValue(`--${id}-color`)).toBe(
|
||||
'red'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -5,15 +5,18 @@ import {
|
||||
warn,
|
||||
VNode,
|
||||
Fragment,
|
||||
unref,
|
||||
onUpdated,
|
||||
watchEffect
|
||||
} from '@vue/runtime-core'
|
||||
import { ShapeFlags } from '@vue/shared'
|
||||
|
||||
/**
|
||||
* Runtime helper for SFC's CSS variable injection feature.
|
||||
* @private
|
||||
*/
|
||||
export function useCssVars(
|
||||
getter: (ctx: ComponentPublicInstance) => Record<string, string>,
|
||||
scoped = false
|
||||
scopeId: string
|
||||
) {
|
||||
const instance = getCurrentInstance()
|
||||
/* istanbul ignore next */
|
||||
@ -23,13 +26,8 @@ export function useCssVars(
|
||||
return
|
||||
}
|
||||
|
||||
const prefix =
|
||||
scoped && instance.type.__scopeId
|
||||
? `${instance.type.__scopeId.replace(/^data-v-/, '')}-`
|
||||
: ``
|
||||
|
||||
const setVars = () =>
|
||||
setVarsOnVNode(instance.subTree, getter(instance.proxy!), prefix)
|
||||
setVarsOnVNode(instance.subTree, getter(instance.proxy!), scopeId)
|
||||
onMounted(() => watchEffect(setVars, { flush: 'post' }))
|
||||
onUpdated(setVars)
|
||||
}
|
||||
@ -37,14 +35,14 @@ export function useCssVars(
|
||||
function setVarsOnVNode(
|
||||
vnode: VNode,
|
||||
vars: Record<string, string>,
|
||||
prefix: string
|
||||
scopeId: string
|
||||
) {
|
||||
if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
|
||||
const suspense = vnode.suspense!
|
||||
vnode = suspense.activeBranch!
|
||||
if (suspense.pendingBranch && !suspense.isHydrating) {
|
||||
suspense.effects.push(() => {
|
||||
setVarsOnVNode(suspense.activeBranch!, vars, prefix)
|
||||
setVarsOnVNode(suspense.activeBranch!, vars, scopeId)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -57,9 +55,9 @@ function setVarsOnVNode(
|
||||
if (vnode.shapeFlag & ShapeFlags.ELEMENT && vnode.el) {
|
||||
const style = vnode.el.style
|
||||
for (const key in vars) {
|
||||
style.setProperty(`--${prefix}${key}`, unref(vars[key]))
|
||||
style.setProperty(`--${scopeId}-${key}`, vars[key])
|
||||
}
|
||||
} else if (vnode.type === Fragment) {
|
||||
;(vnode.children as VNode[]).forEach(c => setVarsOnVNode(c, vars, prefix))
|
||||
;(vnode.children as VNode[]).forEach(c => setVarsOnVNode(c, vars, scopeId))
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user