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 } } + +//