}
// 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) {
// 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.
// 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: [],
default: () => bar
}
},
- setup() {
+ setup(__props) {
default: bar => bar + 1
}
},
- setup() {
+ setup(__props) {
const bar = 1
export default {
expose: [],
- setup() {
+ setup(__props) {
const foo = _ref(1)
export default {
expose: [],
- setup() {
+ setup(__props) {
x()
export default {
expose: [],
- setup() {
+ setup(__props) {
return { a, b }
"import { ref } from 'vue'
export default {
expose: [],
- setup() {
+ setup(__props) {
return { ref }
}
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 */)
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
export default {
expose: [],
- setup() {
+ setup(__props) {
const a = _ref(1)
console.log(a.value)
export default {
expose: [],
- setup() {
+ setup(__props) {
const n = _ref(1), [__a, __b = 1, ...__c] = useFoo()
const a = _ref(__a);
export default {
expose: [],
- setup() {
+ setup(__props) {
const foo = _ref()
const a = _ref(1)
export default {
expose: [],
- setup() {
+ setup(__props) {
const a = _ref(1), b = _ref(2), c = _ref({
count: 0
export default {
expose: [],
- setup() {
+ setup(__props) {
const a = _ref(1)
const b = _ref({ count: 0 })
export default {
expose: [],
- setup() {
+ setup(__props) {
const [{ a: { b: __b }}] = useFoo()
const b = _ref(__b);
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);
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
export default {
expose: [],
- setup() {
+ setup(__props) {
const a = _ref(1)
const b = { a: a.value }
export default {
expose: [],
- setup() {
+ setup(__props) {
let a = 1
const b = 2
literalUnionMixed: { type: [String, Number, Boolean], required: true },
intersection: { type: Object, required: true }
} as unknown as undefined,
- setup() {
+ setup(__props) {
export default _defineComponent({
expose: [],
- setup() {
+ setup(__props) {
return { }
--- /dev/null
+// 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 }
+}
+
+}"
+`;
-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', () => {
})
})
- 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', () => {
} 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;`
).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', () => {
--- /dev/null
+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);
+ }"
+ `)
+ })
+})
--- /dev/null
+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()
+}
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'
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
*/
* from being hot-reloaded separately from component state.
*/
inlineTemplate?: boolean
- templateOptions?: SFCTemplateCompileOptions
+ templateOptions?: Partial<SFCTemplateCompileOptions>
}
const hasWarned: Record<string, boolean> = {}
*/
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'
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) {
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,
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' &&
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)
}
source: node.source.value
}
}
- prev = specifier
}
if (removed === node.specifiers.length) {
s.remove(node.start! + startOffset, node.end! + startOffset)
}
// 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')
}`
}
- 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))
}
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,
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`)
} from 'postcss'
import trimPlugin from './stylePluginTrim'
import scopedPlugin from './stylePluginScoped'
-import scopedVarsPlugin from './stylePluginScopedVars'
import {
processors,
StylePreprocessor,
PreprocessLang
} from './stylePreprocessors'
import { RawSourceMap } from 'source-map'
+import { cssVarsPlugin } from './cssVars'
export interface SFCStyleCompileOptions {
source: string
id: string
map?: RawSourceMap
scoped?: boolean
- vars?: boolean
trim?: boolean
preprocessLang?: PreprocessLang
preprocessOptions?: any
filename,
id,
scoped = false,
- vars = false,
trim = true,
modules = false,
modulesOptions = {},
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())
}
export interface SFCTemplateCompileResults {
code: string
+ ast?: RootNode
preamble?: string
source: string
tips: string[]
nodeTransforms = [transformAssetUrl, transformSrcset]
}
- let { code, preamble, map } = compiler.compile(source, {
+ let { code, ast, preamble, map } = compiler.compile(source, {
mode: 'module',
prefixIdentifiers: true,
hoistStatic: true,
}
}
- return { code, preamble, source, errors, tips: [], map }
+ return { code, ast, preamble, source, errors, tips: [], map }
}
function mapLines(oldMap: RawSourceMap, newMap: RawSourceMap): RawSourceMap {
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
})
.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(
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` +
export interface SFCStyleBlock extends SFCBlock {
type: 'style'
scoped?: boolean
- vars?: string
module?: string | boolean
}
} 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]
}
+++ /dev/null
-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})`
- })
- }
- })
-})
} 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'
+ )
}
}
await assertCssVars(state => ({
setup() {
// test receiving render context
- useCssVars((ctx: any) => ({
- color: ctx.color
- }))
+ useCssVars(
+ (ctx: any) => ({
+ color: ctx.color
+ }),
+ id
+ )
return state
},
render() {
test('on fragment root', async () => {
await assertCssVars(state => ({
setup() {
- useCssVars(() => state)
+ useCssVars(() => state, id)
return () => [h('div'), h('div')]
}
}))
await assertCssVars(state => ({
setup() {
- useCssVars(() => state)
+ useCssVars(() => state, id)
return () => h(Child)
}
}))
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),
}
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 () => {
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'
+ )
}
})
})
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 */
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)
}
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)
})
}
}
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))
}
}