Skip to content

Commit b9595e6

Browse files
committed
feat: ssr support for <style vars>
1 parent b6cdd56 commit b9595e6

File tree

17 files changed

+256
-15
lines changed

17 files changed

+256
-15
lines changed

packages/compiler-core/src/codegen.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -204,10 +204,11 @@ export function generate(
204204
genFunctionPreamble(ast, context)
205205
}
206206

207-
// enter render function
207+
// binding optimizations
208208
const optimizeSources = options.bindingMetadata
209209
? `, $props, $setup, $data, $options`
210210
: ``
211+
// enter render function
211212
if (!ssr) {
212213
if (genScopeId) {
213214
push(`const render = ${PURE_ANNOTATION}_withId(`)

packages/compiler-core/src/options.ts

+5
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,11 @@ export interface TransformOptions {
126126
* `ssrRender` option instead of `render`.
127127
*/
128128
ssr?: boolean
129+
/**
130+
* SFC <style vars> injection string
131+
* needed to render inline CSS variables on component root
132+
*/
133+
ssrCssVars?: string
129134
/**
130135
* Optional binding metadata analyzed from script - used to optimize
131136
* binding access when `prefixIdentifiers` is enabled.

packages/compiler-core/src/transform.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export function createTransformContext(
120120
expressionPlugins = [],
121121
scopeId = null,
122122
ssr = false,
123+
ssrCssVars = ``,
123124
bindingMetadata = {},
124125
onError = defaultOnError
125126
}: TransformOptions
@@ -136,6 +137,7 @@ export function createTransformContext(
136137
expressionPlugins,
137138
scopeId,
138139
ssr,
140+
ssrCssVars,
139141
bindingMetadata,
140142
onError,
141143

@@ -148,7 +150,7 @@ export function createTransformContext(
148150
imports: new Set(),
149151
temps: 0,
150152
cached: 0,
151-
identifiers: {},
153+
identifiers: Object.create(null),
152154
scopes: {
153155
vFor: 0,
154156
vSlot: 0,

packages/compiler-sfc/src/compileScript.ts

+5
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,11 @@ export function compileScript(
591591
Object.keys(setupExports).forEach(key => {
592592
bindings[key] = 'setup'
593593
})
594+
Object.keys(typeDeclaredProps).forEach(key => {
595+
bindings[key] = 'props'
596+
})
597+
// TODO analyze props if user declared props via `export default {}` inside
598+
// <script setup>
594599

595600
s.trim()
596601
return {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { compile } from '../src'
2+
3+
describe('ssr: inject <style vars>', () => {
4+
test('basic', () => {
5+
expect(
6+
compile(`<div/>`, {
7+
ssrCssVars: `{ color }`
8+
}).code
9+
).toMatchInlineSnapshot(`
10+
"const { mergeProps: _mergeProps } = require(\\"vue\\")
11+
const { ssrResolveCssVars: _ssrResolveCssVars, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
12+
13+
return function ssrRender(_ctx, _push, _parent, _attrs) {
14+
const _cssVars = ssrResolveCssVars({ color: _ctx.color })
15+
_push(\`<div\${_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))}></div>\`)
16+
}"
17+
`)
18+
})
19+
20+
test('fragment', () => {
21+
expect(
22+
compile(`<div/><div/>`, {
23+
ssrCssVars: `{ color }`
24+
}).code
25+
).toMatchInlineSnapshot(`
26+
"const { ssrResolveCssVars: _ssrResolveCssVars, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
27+
28+
return function ssrRender(_ctx, _push, _parent, _attrs) {
29+
const _cssVars = ssrResolveCssVars({ color: _ctx.color })
30+
_push(\`<!--[--><div\${
31+
_ssrRenderAttrs(_cssVars)
32+
}></div><div\${
33+
_ssrRenderAttrs(_cssVars)
34+
}></div><!--]-->\`)
35+
}"
36+
`)
37+
})
38+
39+
test('passing on to components', () => {
40+
expect(
41+
compile(`<div/><foo/>`, {
42+
ssrCssVars: `{ color }`
43+
}).code
44+
).toMatchInlineSnapshot(`
45+
"const { resolveComponent: _resolveComponent } = require(\\"vue\\")
46+
const { ssrResolveCssVars: _ssrResolveCssVars, ssrRenderAttrs: _ssrRenderAttrs, ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
47+
48+
return function ssrRender(_ctx, _push, _parent, _attrs) {
49+
const _component_foo = _resolveComponent(\\"foo\\")
50+
51+
const _cssVars = ssrResolveCssVars({ color: _ctx.color })
52+
_push(\`<!--[--><div\${_ssrRenderAttrs(_cssVars)}></div>\`)
53+
_push(_ssrRenderComponent(_component_foo, _cssVars, null, _parent))
54+
_push(\`<!--]-->\`)
55+
}"
56+
`)
57+
})
58+
59+
test('v-if branches', () => {
60+
expect(
61+
compile(`<div v-if="ok"/><template v-else><div/><div/></template>`, {
62+
ssrCssVars: `{ color }`
63+
}).code
64+
).toMatchInlineSnapshot(`
65+
"const { mergeProps: _mergeProps } = require(\\"vue\\")
66+
const { ssrResolveCssVars: _ssrResolveCssVars, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
67+
68+
return function ssrRender(_ctx, _push, _parent, _attrs) {
69+
const _cssVars = ssrResolveCssVars({ color: _ctx.color })
70+
if (_ctx.ok) {
71+
_push(\`<div\${_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))}></div>\`)
72+
} else {
73+
_push(\`<!--[--><div\${
74+
_ssrRenderAttrs(_cssVars)
75+
}></div><div\${
76+
_ssrRenderAttrs(_cssVars)
77+
}></div><!--]-->\`)
78+
}
79+
}"
80+
`)
81+
})
82+
83+
test('w/ scopeId', () => {
84+
expect(
85+
compile(`<div/>`, {
86+
ssrCssVars: `{ color }`,
87+
scopeId: 'data-v-foo'
88+
}).code
89+
).toMatchInlineSnapshot(`
90+
"const { mergeProps: _mergeProps } = require(\\"vue\\")
91+
const { ssrResolveCssVars: _ssrResolveCssVars, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
92+
93+
return function ssrRender(_ctx, _push, _parent, _attrs) {
94+
const _cssVars = ssrResolveCssVars({ color: _ctx.color }, \\"data-v-foo\\")
95+
_push(\`<div\${_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))} data-v-foo></div>\`)
96+
}"
97+
`)
98+
})
99+
})

packages/compiler-ssr/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { ssrTransformFor } from './transforms/ssrVFor'
2424
import { ssrTransformModel } from './transforms/ssrVModel'
2525
import { ssrTransformShow } from './transforms/ssrVShow'
2626
import { ssrInjectFallthroughAttrs } from './transforms/ssrInjectFallthroughAttrs'
27+
import { ssrInjectCssVars } from './transforms/ssrInjectCssVars'
2728

2829
export function compile(
2930
template: string,
@@ -57,6 +58,7 @@ export function compile(
5758
transformExpression,
5859
ssrTransformSlotOutlet,
5960
ssrInjectFallthroughAttrs,
61+
ssrInjectCssVars,
6062
ssrTransformElement,
6163
ssrTransformComponent,
6264
trackSlotScopes,

packages/compiler-ssr/src/runtimeHelpers.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const SSR_RENDER_DYNAMIC_MODEL = Symbol(`ssrRenderDynamicModel`)
1616
export const SSR_GET_DYNAMIC_MODEL_PROPS = Symbol(`ssrGetDynamicModelProps`)
1717
export const SSR_RENDER_TELEPORT = Symbol(`ssrRenderTeleport`)
1818
export const SSR_RENDER_SUSPENSE = Symbol(`ssrRenderSuspense`)
19+
export const SSR_RESOLVE_CSS_VARS = Symbol(`ssrResolveCssVars`)
1920

2021
export const ssrHelpers = {
2122
[SSR_INTERPOLATE]: `ssrInterpolate`,
@@ -33,7 +34,8 @@ export const ssrHelpers = {
3334
[SSR_RENDER_DYNAMIC_MODEL]: `ssrRenderDynamicModel`,
3435
[SSR_GET_DYNAMIC_MODEL_PROPS]: `ssrGetDynamicModelProps`,
3536
[SSR_RENDER_TELEPORT]: `ssrRenderTeleport`,
36-
[SSR_RENDER_SUSPENSE]: `ssrRenderSuspense`
37+
[SSR_RENDER_SUSPENSE]: `ssrRenderSuspense`,
38+
[SSR_RESOLVE_CSS_VARS]: `ssrResolveCssVars`
3739
}
3840

3941
// Note: these are helpers imported from @vue/server-renderer

packages/compiler-ssr/src/ssrCodegenTransform.ts

+30-2
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,19 @@ import {
1111
CompilerOptions,
1212
IfStatement,
1313
CallExpression,
14-
isText
14+
isText,
15+
processExpression,
16+
createSimpleExpression,
17+
createCompoundExpression,
18+
createTransformContext,
19+
createRoot
1520
} from '@vue/compiler-dom'
1621
import { isString, escapeHtml } from '@vue/shared'
17-
import { SSR_INTERPOLATE, ssrHelpers } from './runtimeHelpers'
22+
import {
23+
SSR_INTERPOLATE,
24+
ssrHelpers,
25+
SSR_RESOLVE_CSS_VARS
26+
} from './runtimeHelpers'
1827
import { ssrProcessIf } from './transforms/ssrVIf'
1928
import { ssrProcessFor } from './transforms/ssrVFor'
2029
import { ssrProcessSlotOutlet } from './transforms/ssrTransformSlotOutlet'
@@ -30,6 +39,25 @@ import { createSSRCompilerError, SSRErrorCodes } from './errors'
3039

3140
export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
3241
const context = createSSRTransformContext(ast, options)
42+
43+
// inject <style vars> resolution
44+
// we do this instead of inlining the expression to ensure the vars are
45+
// only resolved once per render
46+
if (options.ssrCssVars) {
47+
const varsExp = processExpression(
48+
createSimpleExpression(options.ssrCssVars, false),
49+
createTransformContext(createRoot([]), options)
50+
)
51+
context.body.push(
52+
createCompoundExpression([
53+
`const _cssVars = ${ssrHelpers[SSR_RESOLVE_CSS_VARS]}(`,
54+
varsExp,
55+
options.scopeId ? `, ${JSON.stringify(options.scopeId)}` : ``,
56+
`)`
57+
])
58+
)
59+
}
60+
3361
const isFragment =
3462
ast.children.length > 1 && ast.children.some(c => !isText(c))
3563
processChildren(ast.children, context, isFragment)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import {
2+
NodeTransform,
3+
NodeTypes,
4+
ElementTypes,
5+
locStub,
6+
createSimpleExpression,
7+
RootNode,
8+
TemplateChildNode,
9+
findDir
10+
} from '@vue/compiler-dom'
11+
import { SSR_RESOLVE_CSS_VARS } from '../runtimeHelpers'
12+
13+
export const ssrInjectCssVars: NodeTransform = (node, context) => {
14+
if (!context.ssrCssVars) {
15+
return
16+
}
17+
18+
// _cssVars is initailized once per render function
19+
// the code is injected in ssrCodegenTrasnform when creating the
20+
// ssr transform context
21+
if (node.type === NodeTypes.ROOT) {
22+
context.identifiers._cssVars = 1
23+
}
24+
25+
const parent = context.parent
26+
if (!parent || parent.type !== NodeTypes.ROOT) {
27+
return
28+
}
29+
30+
context.helper(SSR_RESOLVE_CSS_VARS)
31+
32+
if (node.type === NodeTypes.IF_BRANCH) {
33+
for (const child of node.children) {
34+
injectCssVars(child)
35+
}
36+
} else {
37+
injectCssVars(node)
38+
}
39+
}
40+
41+
function injectCssVars(node: RootNode | TemplateChildNode) {
42+
if (
43+
node.type === NodeTypes.ELEMENT &&
44+
(node.tagType === ElementTypes.ELEMENT ||
45+
node.tagType === ElementTypes.COMPONENT) &&
46+
!findDir(node, 'for')
47+
) {
48+
node.props.push({
49+
type: NodeTypes.DIRECTIVE,
50+
name: 'bind',
51+
arg: undefined,
52+
exp: createSimpleExpression(`_cssVars`, false),
53+
modifiers: [],
54+
loc: locStub
55+
})
56+
}
57+
}

packages/runtime-dom/__tests__/helpers/useCssVars.spec.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {
22
render,
3-
useCSSVars,
3+
useCssVars,
44
h,
55
reactive,
66
nextTick,
@@ -37,7 +37,7 @@ describe('useCssVars', () => {
3737
await assertCssVars(state => ({
3838
setup() {
3939
// test receiving render context
40-
useCSSVars((ctx: any) => ({
40+
useCssVars((ctx: any) => ({
4141
color: ctx.color
4242
}))
4343
return state
@@ -51,7 +51,7 @@ describe('useCssVars', () => {
5151
test('on fragment root', async () => {
5252
await assertCssVars(state => ({
5353
setup() {
54-
useCSSVars(() => state)
54+
useCssVars(() => state)
5555
return () => [h('div'), h('div')]
5656
}
5757
}))
@@ -62,7 +62,7 @@ describe('useCssVars', () => {
6262

6363
await assertCssVars(state => ({
6464
setup() {
65-
useCSSVars(() => state)
65+
useCssVars(() => state)
6666
return () => h(Child)
6767
}
6868
}))
@@ -75,7 +75,7 @@ describe('useCssVars', () => {
7575
state => ({
7676
__scopeId: id,
7777
setup() {
78-
useCSSVars(() => state, true)
78+
useCssVars(() => state, true)
7979
return () => h('div')
8080
}
8181
}),

packages/runtime-dom/src/helpers/useCssModule.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { warn, getCurrentInstance } from '@vue/runtime-core'
22
import { EMPTY_OBJ } from '@vue/shared'
33

4-
export function useCSSModule(name = '$style'): Record<string, string> {
4+
export function useCssModule(name = '$style'): Record<string, string> {
55
if (!__GLOBAL__) {
66
const instance = getCurrentInstance()!
77
if (!instance) {

packages/runtime-dom/src/helpers/useCssVars.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
} from '@vue/runtime-core'
1010
import { ShapeFlags } from '@vue/shared/src'
1111

12-
export function useCSSVars(
12+
export function useCssVars(
1313
getter: (ctx: ComponentPublicInstance) => Record<string, string>,
1414
scoped = false
1515
) {

packages/runtime-dom/src/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,8 @@ function normalizeContainer(container: Element | string): Element | null {
114114
}
115115

116116
// SFC CSS utilities
117-
export { useCSSModule } from './helpers/useCssModule'
118-
export { useCSSVars } from './helpers/useCssVars'
117+
export { useCssModule } from './helpers/useCssModule'
118+
export { useCssVars } from './helpers/useCssVars'
119119

120120
// DOM-only components
121121
export { Transition, TransitionProps } from './components/Transition'

0 commit comments

Comments
 (0)