diff --git a/packages/compiler-sfc/__tests__/__snapshots__/ceStyleAttrs.spec.ts.snap b/packages/compiler-sfc/__tests__/__snapshots__/ceStyleAttrs.spec.ts.snap
new file mode 100644
index 00000000000..9663539e12a
--- /dev/null
+++ b/packages/compiler-sfc/__tests__/__snapshots__/ceStyleAttrs.spec.ts.snap
@@ -0,0 +1,192 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`CE style attrs injection > codegen > \n` +
+ ``,
+ )
+
+ expect(content).toMatch(`_useCEStyleAttrs(_ctx => ([
+ {
+ "id": { id: _ctx.msg3, 'other-attr': _ctx.msg },
+ "src": _ctx.msg,
+ [_ctx.msg]: _ctx.msg,
+ "xlink:special": _ctx.msg,
+ },
+]))}`)
+ assertCode(content)
+ })
+
+ test('w/ normal \n` +
+ ``,
+ )
+ expect(content).toMatch(`_useCEStyleAttrs(_ctx => ([
+ {
+ "id": { id: _ctx.msg3, 'other-attr': _ctx.msg },
+ "src": _ctx.msg,
+ [_ctx.msg]: _ctx.msg,
+ "xlink:special": _ctx.msg,
+ },
+]))}`)
+ expect(content).toMatch(
+ `import { useCEStyleAttrs as _useCEStyleAttrs } from 'vue'`,
+ )
+ assertCode(content)
+ })
+
+ test('w/ \n` +
+ ``,
+ )
+ // should handle:
+ // 1. local const bindings
+ // 2. local potential ref bindings
+ // 3. props bindings (analyzed)
+ expect(content).toMatch(`_useCEStyleAttrs(_ctx => ([
+ {
+ "id": { id: __props.msg3, 'other-attr': msg },
+ "src": msg2.value,
+ [msg]: msg,
+ "xlink:special": __props.msg3,
+ },
+]))`)
+ expect(content).toMatch(
+ `import { useCEStyleAttrs as _useCEStyleAttrs, unref as _unref } from 'vue'`,
+ )
+ assertCode(content)
+ })
+
+ describe('codegen', () => {
+ test('\n` +
+ ``,
+ ).content,
+ )
+ })
+
+ test('\n` +
+ ``,
+ ).content,
+ )
+ })
+
+ test('\n` +
+ ``,
+ ).content,
+ )
+ })
+
+ test(`w/ \n` +
+ ``,
+ ).content,
+ )
+ })
+
+ test(`w/ \n` +
+ ``,
+ )
+
+ // id should only be injected once, even if it is twice in style
+ expect(content).toMatch(`_useCEStyleAttrs(_ctx => ([
+ {
+ "id": { id: _ctx.msg3, 'other-attr': msg },
+ },
+]))`)
+ assertCode(content)
+ })
+
+ test('should be able to parse incomplete expressions', () => {
+ const {
+ descriptor: { ceStyleAttrs },
+ } = parse(
+ `
+ `,
+ )
+ expect(ceStyleAttrs[0].length).toBe(2)
+ expect(ceStyleAttrs[0]).toMatchObject([
+ {
+ name: 'bind',
+ exp: {
+ content: 'xxx',
+ isStatic: false,
+ constType: 0,
+ },
+ arg: {
+ content: 'id',
+ isStatic: true,
+ constType: 3,
+ },
+ },
+ {
+ name: 'bind',
+ exp: {
+ content: 'count.toString(',
+ isStatic: false,
+ constType: 0,
+ },
+ arg: {
+ content: 'data-name',
+ isStatic: true,
+ constType: 3,
+ },
+ },
+ ])
+ })
+
+ test('It should correctly parse the case where there is no space after the script tag', () => {
+ const { content } = compileSFCScript(
+ `
+ `,
+ )
+ expect(content).toMatch(
+ `import { useCEStyleAttrs as _useCEStyleAttrs, unref as _unref } from 'vue'\nimport { ref as _ref } from 'vue';\n \nexport default {\n setup(__props, { expose: __expose }) {\n __expose();\n\n_useCEStyleAttrs(_ctx => ([\n {\n \"id\": _unref(background),\n },\n]))\nlet background = _ref('red')\n \nreturn { get background() { return background }, set background(v) { background = v }, _ref }\n}\n\n}`,
+ )
+ })
+
+ test('With cssvars', () => {
+ const { content } = compileSFCScript(
+ `
+ `,
+ )
+ expect(content).toMatch(
+ `import { useCssVars as _useCssVars, unref as _unref, useCEStyleAttrs as _useCEStyleAttrs } from 'vue'\nimport { ref as _ref } from 'vue';\n \nexport default {\n setup(__props, { expose: __expose }) {\n __expose();\n\n_useCEStyleAttrs(_ctx => ([\n {\n \"id\": _unref(background),\n },\n]))\n\n_useCssVars(_ctx => ({\n \"xxxxxxxx-background\": (_unref(background))\n}))\nlet background = _ref('red')\n \nreturn { get background() { return background }, set background(v) { background = v }, _ref }\n}\n\n}`,
+ )
+ })
+
+ test('Multiple style tags', () => {
+ const { content } = compileSFCScript(
+ `
+
+ `,
+ )
+ expect(content).toMatch(
+ `import { useCssVars as _useCssVars, unref as _unref, useCEStyleAttrs as _useCEStyleAttrs } from 'vue'\nimport { ref as _ref } from 'vue';\n \nexport default {\n setup(__props, { expose: __expose }) {\n __expose();\n\n_useCEStyleAttrs(_ctx => ([\n {\n \"id\": _unref(background),\n },\n {\n \"id\": _unref(background),\n },\n]))\n\n_useCssVars(_ctx => ({\n \"xxxxxxxx-background\": (_unref(background))\n}))\nlet background = _ref('red')\n \nreturn { get background() { return background }, set background(v) { background = v }, _ref }\n}\n\n}`,
+ )
+ })
+
+ describe('skip codegen in SSR', () => {
+ test('script setup, inline', () => {
+ const { content } = compileSFCScript(
+ `\n` +
+ ``,
+ {
+ inlineTemplate: true,
+ templateOptions: {
+ ssr: true,
+ },
+ },
+ )
+ expect(content).not.toMatch(`_useCssVars`)
+ })
+
+ // #6926
+ test('script, non-inline', () => {
+ const { content } = compileSFCScript(
+ `\n` +
+ `\``,
+ {
+ inlineTemplate: false,
+ templateOptions: {
+ ssr: true,
+ },
+ },
+ )
+ expect(content).not.toMatch(`_useCssVars`)
+ })
+
+ test('normal script', () => {
+ const { content } = compileSFCScript(
+ `\n` +
+ `\``,
+ {
+ templateOptions: {
+ ssr: true,
+ },
+ },
+ )
+ expect(content).not.toMatch(`_useCssVars`)
+ })
+ })
+ })
+})
diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts
index 2fa2241a7de..d75a82789d2 100644
--- a/packages/compiler-sfc/src/compileScript.ts
+++ b/packages/compiler-sfc/src/compileScript.ts
@@ -55,6 +55,7 @@ import { getImportedName, isCallOf, isLiteralNode } from './script/utils'
import { analyzeScriptBindings } from './script/analyzeScriptBindings'
import { isImportUsed } from './script/importUsageCheck'
import { processAwait } from './script/topLevelAwait'
+import { CE_STYLE_ATTRS_HELPER, genCEStyleAttrs } from './style/ceStyleAttrs'
export interface SFCScriptCompileOptions {
/**
@@ -764,6 +765,22 @@ export function compileScript(
)
}
+ if (
+ ctx.descriptor.ceStyleAttrs.length &&
+ // no need to do this when targeting SSR
+ !options.templateOptions?.ssr
+ ) {
+ ctx.helperImports.add(CE_STYLE_ATTRS_HELPER)
+ ctx.helperImports.add('unref')
+ ctx.s.prependLeft(
+ startOffset,
+ `\n${genCEStyleAttrs(
+ ctx.descriptor.ceStyleAttrs,
+ ctx.bindingMetadata,
+ )}\n`,
+ )
+ }
+
// 8. finalize setup() argument signature
let args = `__props`
if (ctx.propsTypeDecl) {
diff --git a/packages/compiler-sfc/src/parse.ts b/packages/compiler-sfc/src/parse.ts
index f0ec926d1b9..9bbcf16ead2 100644
--- a/packages/compiler-sfc/src/parse.ts
+++ b/packages/compiler-sfc/src/parse.ts
@@ -1,12 +1,15 @@
import {
+ type AttributeNode,
type BindingMetadata,
type CodegenSourceMapGenerator,
type CompilerError,
+ type DirectiveNode,
type ElementNode,
NodeTypes,
type ParserOptions,
type RawSourceMap,
type RootNode,
+ type SimpleExpressionNode,
type SourceLocation,
createRoot,
} from '@vue/compiler-core'
@@ -79,6 +82,7 @@ export interface SFCDescriptor {
script: SFCScriptBlock | null
scriptSetup: SFCScriptBlock | null
styles: SFCStyleBlock[]
+ ceStyleAttrs: Array[]
customBlocks: SFCBlock[]
cssVars: string[]
/**
@@ -146,6 +150,7 @@ export function parse(
script: null,
scriptSetup: null,
styles: [],
+ ceStyleAttrs: [],
customBlocks: [],
cssVars: [],
slotted: false,
@@ -229,6 +234,8 @@ export function parse(
)
}
descriptor.styles.push(styleBlock)
+ // ce style attrs
+ setPropsNodeForStyleAttrs(descriptor, node.props)
break
default:
descriptor.customBlocks.push(createBlock(node, source, pad))
@@ -467,6 +474,28 @@ export function hmrShouldReload(
return false
}
+function setPropsNodeForStyleAttrs(
+ descriptor: SFCDescriptor,
+ props: Array,
+) {
+ const propsArr = props.filter(prop => {
+ // skip scoped and lang
+ if (prop.type === 6 && (prop.name === 'scoped' || prop.name === 'lang')) {
+ return false
+ }
+ return !(
+ prop.type === 7 &&
+ (((prop as DirectiveNode).arg! as SimpleExpressionNode).content ===
+ 'scoped' ||
+ ((prop as DirectiveNode).arg! as SimpleExpressionNode).content ===
+ 'lang')
+ )
+ })
+
+ if (propsArr.length > 0) {
+ descriptor.ceStyleAttrs.push(propsArr)
+ }
+}
/**
* Dedent a string.
diff --git a/packages/compiler-sfc/src/script/normalScript.ts b/packages/compiler-sfc/src/script/normalScript.ts
index 3b2f21d4863..59997c9198a 100644
--- a/packages/compiler-sfc/src/script/normalScript.ts
+++ b/packages/compiler-sfc/src/script/normalScript.ts
@@ -2,7 +2,8 @@ import { analyzeScriptBindings } from './analyzeScriptBindings'
import type { ScriptCompileContext } from './context'
import MagicString from 'magic-string'
import { rewriteDefaultAST } from '../rewriteDefault'
-import { genNormalScriptCssVarsCode } from '../style/cssVars'
+import { CSS_VARS_HELPER, genCssVarsCode } from '../style/cssVars'
+import { CE_STYLE_ATTRS_HELPER, genCEStyleAttrs } from '../style/ceStyleAttrs'
export const normalScriptDefaultVar = `__default__`
@@ -20,23 +21,30 @@ export function processNormalScript(
let map = script.map
const scriptAst = ctx.scriptAst!
const bindings = analyzeScriptBindings(scriptAst.body)
- const { cssVars } = ctx.descriptor
+ const { cssVars, ceStyleAttrs } = ctx.descriptor
const { genDefaultAs, isProd } = ctx.options
- if (cssVars.length || genDefaultAs) {
+ if (cssVars.length || ceStyleAttrs.length || genDefaultAs) {
const defaultVar = genDefaultAs || normalScriptDefaultVar
const s = new MagicString(content)
rewriteDefaultAST(scriptAst.body, s, defaultVar)
content = s.toString()
+ let injectCode = ''
+ let injectImporter = []
if (cssVars.length && !ctx.options.templateOptions?.ssr) {
- content += genNormalScriptCssVarsCode(
- cssVars,
- bindings,
- scopeId,
- !!isProd,
- defaultVar,
- )
+ injectCode += genCssVarsCode(cssVars, bindings, scopeId, !!isProd)
+ injectImporter.push(CSS_VARS_HELPER)
}
+
+ if (ceStyleAttrs.length && !ctx.options.templateOptions?.ssr) {
+ injectCode += '\n' + genCEStyleAttrs(ceStyleAttrs, bindings)
+ injectImporter.push(CE_STYLE_ATTRS_HELPER)
+ }
+
+ if (injectCode) {
+ content += genInjectCode(injectCode, defaultVar, injectImporter)
+ }
+
if (!genDefaultAs) {
content += `\nexport default ${defaultVar}`
}
@@ -54,3 +62,21 @@ export function processNormalScript(
return script
}
}
+
+//