]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: ssr support for `<style vars>`
authorEvan You <yyx990803@gmail.com>
Sun, 12 Jul 2020 22:04:09 +0000 (18:04 -0400)
committerEvan You <yyx990803@gmail.com>
Sun, 12 Jul 2020 22:04:09 +0000 (18:04 -0400)
17 files changed:
packages/compiler-core/src/codegen.ts
packages/compiler-core/src/options.ts
packages/compiler-core/src/transform.ts
packages/compiler-sfc/src/compileScript.ts
packages/compiler-ssr/__tests__/ssrInjectCssVars.spec.ts [new file with mode: 0644]
packages/compiler-ssr/src/index.ts
packages/compiler-ssr/src/runtimeHelpers.ts
packages/compiler-ssr/src/ssrCodegenTransform.ts
packages/compiler-ssr/src/transforms/ssrInjectCssVars.ts [new file with mode: 0644]
packages/runtime-dom/__tests__/helpers/useCssVars.spec.ts
packages/runtime-dom/src/helpers/useCssModule.ts
packages/runtime-dom/src/helpers/useCssVars.ts
packages/runtime-dom/src/index.ts
packages/server-renderer/__tests__/ssrResolveCssVars.spec.ts [new file with mode: 0644]
packages/server-renderer/src/helpers/ssrResolveCssVars.ts [new file with mode: 0644]
packages/server-renderer/src/index.ts
packages/template-explorer/src/options.ts

index 53551be9bc7242eedb5442423fd8e58b0fa9a76b..fab88a2ff4cf79dab7a11b99d30fdbce803f83ca 100644 (file)
@@ -204,10 +204,11 @@ export function generate(
     genFunctionPreamble(ast, context)
   }
 
-  // enter render function
+  // binding optimizations
   const optimizeSources = options.bindingMetadata
     ? `, $props, $setup, $data, $options`
     : ``
+  // enter render function
   if (!ssr) {
     if (genScopeId) {
       push(`const render = ${PURE_ANNOTATION}_withId(`)
index d819b226b55be1c38d665e5d509ea7f61714a7c4..c5e9deaf02f3b6c57947fa7f4e2c0388efe897a8 100644 (file)
@@ -126,6 +126,11 @@ export interface TransformOptions {
    * `ssrRender` option instead of `render`.
    */
   ssr?: boolean
+  /**
+   * SFC <style vars> injection string
+   * needed to render inline CSS variables on component root
+   */
+  ssrCssVars?: string
   /**
    * Optional binding metadata analyzed from script - used to optimize
    * binding access when `prefixIdentifiers` is enabled.
index c73e43c5457259c77c6b6330535c5b3f638b57a7..f93b617cbcfe475f512ce2c8303235c4d796c846 100644 (file)
@@ -120,6 +120,7 @@ export function createTransformContext(
     expressionPlugins = [],
     scopeId = null,
     ssr = false,
+    ssrCssVars = ``,
     bindingMetadata = {},
     onError = defaultOnError
   }: TransformOptions
@@ -136,6 +137,7 @@ export function createTransformContext(
     expressionPlugins,
     scopeId,
     ssr,
+    ssrCssVars,
     bindingMetadata,
     onError,
 
@@ -148,7 +150,7 @@ export function createTransformContext(
     imports: new Set(),
     temps: 0,
     cached: 0,
-    identifiers: {},
+    identifiers: Object.create(null),
     scopes: {
       vFor: 0,
       vSlot: 0,
index 4080762848aa9b122ea8fe5d3c5ac5cb12fa7297..6137f271558f54a990cfc4f5374c90b6ae602c69 100644 (file)
@@ -591,6 +591,11 @@ export function compileScript(
   Object.keys(setupExports).forEach(key => {
     bindings[key] = 'setup'
   })
+  Object.keys(typeDeclaredProps).forEach(key => {
+    bindings[key] = 'props'
+  })
+  // TODO analyze props if user declared props via `export default {}` inside
+  // <script setup>
 
   s.trim()
   return {
diff --git a/packages/compiler-ssr/__tests__/ssrInjectCssVars.spec.ts b/packages/compiler-ssr/__tests__/ssrInjectCssVars.spec.ts
new file mode 100644 (file)
index 0000000..2ea2b7f
--- /dev/null
@@ -0,0 +1,99 @@
+import { compile } from '../src'
+
+describe('ssr: inject <style vars>', () => {
+  test('basic', () => {
+    expect(
+      compile(`<div/>`, {
+        ssrCssVars: `{ color }`
+      }).code
+    ).toMatchInlineSnapshot(`
+      "const { mergeProps: _mergeProps } = require(\\"vue\\")
+      const { ssrResolveCssVars: _ssrResolveCssVars, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
+
+      return function ssrRender(_ctx, _push, _parent, _attrs) {
+        const _cssVars = ssrResolveCssVars({ color: _ctx.color })
+        _push(\`<div\${_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))}></div>\`)
+      }"
+    `)
+  })
+
+  test('fragment', () => {
+    expect(
+      compile(`<div/><div/>`, {
+        ssrCssVars: `{ color }`
+      }).code
+    ).toMatchInlineSnapshot(`
+      "const { ssrResolveCssVars: _ssrResolveCssVars, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
+
+      return function ssrRender(_ctx, _push, _parent, _attrs) {
+        const _cssVars = ssrResolveCssVars({ color: _ctx.color })
+        _push(\`<!--[--><div\${
+          _ssrRenderAttrs(_cssVars)
+        }></div><div\${
+          _ssrRenderAttrs(_cssVars)
+        }></div><!--]-->\`)
+      }"
+    `)
+  })
+
+  test('passing on to components', () => {
+    expect(
+      compile(`<div/><foo/>`, {
+        ssrCssVars: `{ color }`
+      }).code
+    ).toMatchInlineSnapshot(`
+      "const { resolveComponent: _resolveComponent } = require(\\"vue\\")
+      const { ssrResolveCssVars: _ssrResolveCssVars, ssrRenderAttrs: _ssrRenderAttrs, ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
+
+      return function ssrRender(_ctx, _push, _parent, _attrs) {
+        const _component_foo = _resolveComponent(\\"foo\\")
+
+        const _cssVars = ssrResolveCssVars({ color: _ctx.color })
+        _push(\`<!--[--><div\${_ssrRenderAttrs(_cssVars)}></div>\`)
+        _push(_ssrRenderComponent(_component_foo, _cssVars, null, _parent))
+        _push(\`<!--]-->\`)
+      }"
+    `)
+  })
+
+  test('v-if branches', () => {
+    expect(
+      compile(`<div v-if="ok"/><template v-else><div/><div/></template>`, {
+        ssrCssVars: `{ color }`
+      }).code
+    ).toMatchInlineSnapshot(`
+      "const { mergeProps: _mergeProps } = require(\\"vue\\")
+      const { ssrResolveCssVars: _ssrResolveCssVars, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
+
+      return function ssrRender(_ctx, _push, _parent, _attrs) {
+        const _cssVars = ssrResolveCssVars({ color: _ctx.color })
+        if (_ctx.ok) {
+          _push(\`<div\${_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))}></div>\`)
+        } else {
+          _push(\`<!--[--><div\${
+            _ssrRenderAttrs(_cssVars)
+          }></div><div\${
+            _ssrRenderAttrs(_cssVars)
+          }></div><!--]-->\`)
+        }
+      }"
+    `)
+  })
+
+  test('w/ scopeId', () => {
+    expect(
+      compile(`<div/>`, {
+        ssrCssVars: `{ color }`,
+        scopeId: 'data-v-foo'
+      }).code
+    ).toMatchInlineSnapshot(`
+      "const { mergeProps: _mergeProps } = require(\\"vue\\")
+      const { ssrResolveCssVars: _ssrResolveCssVars, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
+
+      return function ssrRender(_ctx, _push, _parent, _attrs) {
+        const _cssVars = ssrResolveCssVars({ color: _ctx.color }, \\"data-v-foo\\")
+        _push(\`<div\${_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))} data-v-foo></div>\`)
+      }"
+    `)
+  })
+})
index 02153773b6487473b90e44221e376a9475864228..c04529f993ee0006a8b14c29cf948a6283dc15a4 100644 (file)
@@ -24,6 +24,7 @@ import { ssrTransformFor } from './transforms/ssrVFor'
 import { ssrTransformModel } from './transforms/ssrVModel'
 import { ssrTransformShow } from './transforms/ssrVShow'
 import { ssrInjectFallthroughAttrs } from './transforms/ssrInjectFallthroughAttrs'
+import { ssrInjectCssVars } from './transforms/ssrInjectCssVars'
 
 export function compile(
   template: string,
@@ -57,6 +58,7 @@ export function compile(
       transformExpression,
       ssrTransformSlotOutlet,
       ssrInjectFallthroughAttrs,
+      ssrInjectCssVars,
       ssrTransformElement,
       ssrTransformComponent,
       trackSlotScopes,
index 2aa93c0bf1589ba7d5c0b582a69b23573be7e12f..ee0b7a2eafdf441746b3312c328bb8025606ee7c 100644 (file)
@@ -16,6 +16,7 @@ export const SSR_RENDER_DYNAMIC_MODEL = Symbol(`ssrRenderDynamicModel`)
 export const SSR_GET_DYNAMIC_MODEL_PROPS = Symbol(`ssrGetDynamicModelProps`)
 export const SSR_RENDER_TELEPORT = Symbol(`ssrRenderTeleport`)
 export const SSR_RENDER_SUSPENSE = Symbol(`ssrRenderSuspense`)
+export const SSR_RESOLVE_CSS_VARS = Symbol(`ssrResolveCssVars`)
 
 export const ssrHelpers = {
   [SSR_INTERPOLATE]: `ssrInterpolate`,
@@ -33,7 +34,8 @@ export const ssrHelpers = {
   [SSR_RENDER_DYNAMIC_MODEL]: `ssrRenderDynamicModel`,
   [SSR_GET_DYNAMIC_MODEL_PROPS]: `ssrGetDynamicModelProps`,
   [SSR_RENDER_TELEPORT]: `ssrRenderTeleport`,
-  [SSR_RENDER_SUSPENSE]: `ssrRenderSuspense`
+  [SSR_RENDER_SUSPENSE]: `ssrRenderSuspense`,
+  [SSR_RESOLVE_CSS_VARS]: `ssrResolveCssVars`
 }
 
 // Note: these are helpers imported from @vue/server-renderer
index cca7318e79ed9dbe9d80a7dbd4bd99f32955b477..45cc1a6d50f1f39b97eb879f7f5ddbfd702568b4 100644 (file)
@@ -11,10 +11,19 @@ import {
   CompilerOptions,
   IfStatement,
   CallExpression,
-  isText
+  isText,
+  processExpression,
+  createSimpleExpression,
+  createCompoundExpression,
+  createTransformContext,
+  createRoot
 } from '@vue/compiler-dom'
 import { isString, escapeHtml } from '@vue/shared'
-import { SSR_INTERPOLATE, ssrHelpers } from './runtimeHelpers'
+import {
+  SSR_INTERPOLATE,
+  ssrHelpers,
+  SSR_RESOLVE_CSS_VARS
+} from './runtimeHelpers'
 import { ssrProcessIf } from './transforms/ssrVIf'
 import { ssrProcessFor } from './transforms/ssrVFor'
 import { ssrProcessSlotOutlet } from './transforms/ssrTransformSlotOutlet'
@@ -30,6 +39,25 @@ import { createSSRCompilerError, SSRErrorCodes } from './errors'
 
 export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
   const context = createSSRTransformContext(ast, options)
+
+  // inject <style vars> resolution
+  // we do this instead of inlining the expression to ensure the vars are
+  // only resolved once per render
+  if (options.ssrCssVars) {
+    const varsExp = processExpression(
+      createSimpleExpression(options.ssrCssVars, false),
+      createTransformContext(createRoot([]), options)
+    )
+    context.body.push(
+      createCompoundExpression([
+        `const _cssVars = ${ssrHelpers[SSR_RESOLVE_CSS_VARS]}(`,
+        varsExp,
+        options.scopeId ? `, ${JSON.stringify(options.scopeId)}` : ``,
+        `)`
+      ])
+    )
+  }
+
   const isFragment =
     ast.children.length > 1 && ast.children.some(c => !isText(c))
   processChildren(ast.children, context, isFragment)
diff --git a/packages/compiler-ssr/src/transforms/ssrInjectCssVars.ts b/packages/compiler-ssr/src/transforms/ssrInjectCssVars.ts
new file mode 100644 (file)
index 0000000..0e6540c
--- /dev/null
@@ -0,0 +1,57 @@
+import {
+  NodeTransform,
+  NodeTypes,
+  ElementTypes,
+  locStub,
+  createSimpleExpression,
+  RootNode,
+  TemplateChildNode,
+  findDir
+} from '@vue/compiler-dom'
+import { SSR_RESOLVE_CSS_VARS } from '../runtimeHelpers'
+
+export const ssrInjectCssVars: NodeTransform = (node, context) => {
+  if (!context.ssrCssVars) {
+    return
+  }
+
+  // _cssVars is initailized once per render function
+  // the code is injected in ssrCodegenTrasnform when creating the
+  // ssr transform context
+  if (node.type === NodeTypes.ROOT) {
+    context.identifiers._cssVars = 1
+  }
+
+  const parent = context.parent
+  if (!parent || parent.type !== NodeTypes.ROOT) {
+    return
+  }
+
+  context.helper(SSR_RESOLVE_CSS_VARS)
+
+  if (node.type === NodeTypes.IF_BRANCH) {
+    for (const child of node.children) {
+      injectCssVars(child)
+    }
+  } else {
+    injectCssVars(node)
+  }
+}
+
+function injectCssVars(node: RootNode | TemplateChildNode) {
+  if (
+    node.type === NodeTypes.ELEMENT &&
+    (node.tagType === ElementTypes.ELEMENT ||
+      node.tagType === ElementTypes.COMPONENT) &&
+    !findDir(node, 'for')
+  ) {
+    node.props.push({
+      type: NodeTypes.DIRECTIVE,
+      name: 'bind',
+      arg: undefined,
+      exp: createSimpleExpression(`_cssVars`, false),
+      modifiers: [],
+      loc: locStub
+    })
+  }
+}
index a7d8e8e8603a64170b1a93e854a2c4194aa3c419..5260d2a82b34f2fe82d3a578e83db0c1e55b9acb 100644 (file)
@@ -1,6 +1,6 @@
 import {
   render,
-  useCSSVars,
+  useCssVars,
   h,
   reactive,
   nextTick,
@@ -37,7 +37,7 @@ describe('useCssVars', () => {
     await assertCssVars(state => ({
       setup() {
         // test receiving render context
-        useCSSVars((ctx: any) => ({
+        useCssVars((ctx: any) => ({
           color: ctx.color
         }))
         return state
@@ -51,7 +51,7 @@ describe('useCssVars', () => {
   test('on fragment root', async () => {
     await assertCssVars(state => ({
       setup() {
-        useCSSVars(() => state)
+        useCssVars(() => state)
         return () => [h('div'), h('div')]
       }
     }))
@@ -62,7 +62,7 @@ describe('useCssVars', () => {
 
     await assertCssVars(state => ({
       setup() {
-        useCSSVars(() => state)
+        useCssVars(() => state)
         return () => h(Child)
       }
     }))
@@ -75,7 +75,7 @@ describe('useCssVars', () => {
       state => ({
         __scopeId: id,
         setup() {
-          useCSSVars(() => state, true)
+          useCssVars(() => state, true)
           return () => h('div')
         }
       }),
index b54dcaf76ad92b3cbef22e918ecef903ba475735..18b7048f63f764ef490ef74abde8af2cf9008d9b 100644 (file)
@@ -1,7 +1,7 @@
 import { warn, getCurrentInstance } from '@vue/runtime-core'
 import { EMPTY_OBJ } from '@vue/shared'
 
-export function useCSSModule(name = '$style'): Record<string, string> {
+export function useCssModule(name = '$style'): Record<string, string> {
   if (!__GLOBAL__) {
     const instance = getCurrentInstance()!
     if (!instance) {
index 6c45894fe3b97404bdbb048a20fd14cfe6c9c0fe..5bf4f46971914be61161fb497119eb822ca81fb3 100644 (file)
@@ -9,7 +9,7 @@ import {
 } from '@vue/runtime-core'
 import { ShapeFlags } from '@vue/shared/src'
 
-export function useCSSVars(
+export function useCssVars(
   getter: (ctx: ComponentPublicInstance) => Record<string, string>,
   scoped = false
 ) {
index faa26ea2787420d7f532d4c42cfad87f839eca3a..fc73bf926bc52c4d1089756ef0e1d6f8870047b4 100644 (file)
@@ -114,8 +114,8 @@ function normalizeContainer(container: Element | string): Element | null {
 }
 
 // SFC CSS utilities
-export { useCSSModule } from './helpers/useCssModule'
-export { useCSSVars } from './helpers/useCssVars'
+export { useCssModule } from './helpers/useCssModule'
+export { useCssVars } from './helpers/useCssVars'
 
 // DOM-only components
 export { Transition, TransitionProps } from './components/Transition'
diff --git a/packages/server-renderer/__tests__/ssrResolveCssVars.spec.ts b/packages/server-renderer/__tests__/ssrResolveCssVars.spec.ts
new file mode 100644 (file)
index 0000000..c4bccfd
--- /dev/null
@@ -0,0 +1,27 @@
+import { ssrResolveCssVars } from '../src'
+
+describe('ssr: resolveCssVars', () => {
+  test('should work', () => {
+    expect(ssrResolveCssVars({ color: 'red' })).toMatchObject({
+      style: {
+        '--color': 'red'
+      }
+    })
+  })
+
+  test('should work with scopeId', () => {
+    expect(ssrResolveCssVars({ color: 'red' }, 'scoped')).toMatchObject({
+      style: {
+        '--scoped-color': 'red'
+      }
+    })
+  })
+
+  test('should strip data-v prefix', () => {
+    expect(ssrResolveCssVars({ color: 'red' }, 'data-v-123456')).toMatchObject({
+      style: {
+        '--123456-color': 'red'
+      }
+    })
+  })
+})
diff --git a/packages/server-renderer/src/helpers/ssrResolveCssVars.ts b/packages/server-renderer/src/helpers/ssrResolveCssVars.ts
new file mode 100644 (file)
index 0000000..7165dcc
--- /dev/null
@@ -0,0 +1,11 @@
+export function ssrResolveCssVars(
+  source: Record<string, string>,
+  scopeId?: string
+) {
+  const style: Record<string, string> = {}
+  const prefix = scopeId ? `${scopeId.replace(/^data-v-/, '')}-` : ``
+  for (const key in source) {
+    style[`--${prefix}${key}`] = source[key]
+  }
+  return { style }
+}
index 9c5066e851eca2bd60edbb6e481ee082e2a5f01f..7b59d05224a9c7f07edb160633cd210bfcba4c3c 100644 (file)
@@ -18,6 +18,7 @@ export {
 export { ssrInterpolate } from './helpers/ssrInterpolate'
 export { ssrRenderList } from './helpers/ssrRenderList'
 export { ssrRenderSuspense } from './helpers/ssrRenderSuspense'
+export { ssrResolveCssVars } from './helpers/ssrResolveCssVars'
 
 // v-model helpers
 export {
index 02ac3c9b71c5c3b23e90da096466a30b3325f349..167b23d0b869fe194367e6caada66c9478186638 100644 (file)
@@ -9,7 +9,8 @@ export const compilerOptions: CompilerOptions = reactive({
   optimizeImports: false,
   hoistStatic: false,
   cacheHandlers: false,
-  scopeId: null
+  scopeId: null,
+  ssrCssVars: `{ color }`
 })
 
 const App = {