]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler-sfc): new SFC css varaible injection implementation
authorEvan You <yyx990803@gmail.com>
Mon, 16 Nov 2020 23:27:15 +0000 (18:27 -0500)
committerEvan You <yyx990803@gmail.com>
Mon, 16 Nov 2020 23:27:25 +0000 (18:27 -0500)
ref: https://github.com/vuejs/rfcs/pull/231

16 files changed:
packages/compiler-core/src/codegen.ts
packages/compiler-core/src/transforms/transformExpression.ts
packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
packages/compiler-sfc/__tests__/__snapshots__/cssVars.spec.ts.snap [new file with mode: 0644]
packages/compiler-sfc/__tests__/compileScript.spec.ts
packages/compiler-sfc/__tests__/compileStyle.spec.ts
packages/compiler-sfc/__tests__/cssVars.spec.ts [new file with mode: 0644]
packages/compiler-sfc/__tests__/utils.ts [new file with mode: 0644]
packages/compiler-sfc/src/compileScript.ts
packages/compiler-sfc/src/compileStyle.ts
packages/compiler-sfc/src/compileTemplate.ts
packages/compiler-sfc/src/cssVars.ts [moved from packages/compiler-sfc/src/genCssVars.ts with 52% similarity]
packages/compiler-sfc/src/parse.ts
packages/compiler-sfc/src/stylePluginScopedVars.ts [deleted file]
packages/runtime-dom/__tests__/helpers/useCssVars.spec.ts
packages/runtime-dom/src/helpers/useCssVars.ts

index 9e58d751da96d2c956a1edb580f1cdadd45cf102..a481ddcf55a93c8461f624706c942d65fa77b723 100644 (file)
@@ -215,9 +215,10 @@ export function generate(
   }
 
   // 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) {
index f72bd16bb34cefe4c81aade8dbe32d95166617eb..0a6cc8ef00f9b982d4d53c516d7a57f43f901126 100644 (file)
@@ -113,17 +113,16 @@ export function processExpression(
         // 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.
index baac4d5815ddbc65d079314037c55dd0a7fe9fea..907a47c8dc678f894b6788160ec2299f1e617c4a 100644 (file)
@@ -1,62 +1,5 @@
 // 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: [],
@@ -86,7 +29,7 @@ export default {
                 default: () => bar
               }
             },
-  setup() {
+  setup(__props) {
 
           
         
@@ -104,7 +47,7 @@ exports[`SFC compile <script setup> errors should allow defineOptions() referenc
                 default: bar => bar + 1
               }
             },
-  setup() {
+  setup(__props) {
 
           const bar = 1
           
@@ -121,7 +64,7 @@ import { ref } from 'vue'
       
 export default {
   expose: [],
-  setup() {
+  setup(__props) {
 
       const foo = _ref(1)
       
@@ -136,7 +79,7 @@ exports[`SFC compile <script setup> imports import dedupe between <script> and <
         
 export default {
   expose: [],
-  setup() {
+  setup(__props) {
 
         x()
         
@@ -152,7 +95,7 @@ exports[`SFC compile <script setup> imports should extract comment for import or
         
 export default {
   expose: [],
-  setup() {
+  setup(__props) {
 
         
 return { a, b }
@@ -165,7 +108,7 @@ exports[`SFC compile <script setup> imports should hoist and expose imports 1`]
 "import { ref } from 'vue'
 export default {
   expose: [],
-  setup() {
+  setup(__props) {
 
 return { ref }
 }
@@ -182,13 +125,13 @@ import { ref } from 'vue'
         
 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 */)
@@ -208,11 +151,11 @@ import { ref } from 'vue'
         
 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
@@ -228,7 +171,7 @@ exports[`SFC compile <script setup> ref: syntax sugar accessing ref binding 1`]
 
 export default {
   expose: [],
-  setup() {
+  setup(__props) {
 
       const a = _ref(1)
       console.log(a.value)
@@ -247,7 +190,7 @@ exports[`SFC compile <script setup> ref: syntax sugar array destructure 1`] = `
 
 export default {
   expose: [],
-  setup() {
+  setup(__props) {
 
       const n = _ref(1), [__a, __b = 1, ...__c] = useFoo()
 const a = _ref(__a);
@@ -266,7 +209,7 @@ exports[`SFC compile <script setup> ref: syntax sugar convert ref declarations 1
 
 export default {
   expose: [],
-  setup() {
+  setup(__props) {
 
       const foo = _ref()
       const a = _ref(1)
@@ -287,7 +230,7 @@ exports[`SFC compile <script setup> ref: syntax sugar multi ref declarations 1`]
 
 export default {
   expose: [],
-  setup() {
+  setup(__props) {
 
       const a = _ref(1), b = _ref(2), c = _ref({
         count: 0
@@ -304,7 +247,7 @@ exports[`SFC compile <script setup> ref: syntax sugar mutating ref binding 1`] =
 
 export default {
   expose: [],
-  setup() {
+  setup(__props) {
 
       const a = _ref(1)
       const b = _ref({ count: 0 })
@@ -326,7 +269,7 @@ exports[`SFC compile <script setup> ref: syntax sugar nested destructure 1`] = `
 
 export default {
   expose: [],
-  setup() {
+  setup(__props) {
 
       const [{ a: { b: __b }}] = useFoo()
 const b = _ref(__b);
@@ -346,7 +289,7 @@ exports[`SFC compile <script setup> ref: syntax sugar object destructure 1`] = `
 
 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);
@@ -365,7 +308,7 @@ return { n, a, c, d, f, g }
 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
@@ -382,7 +325,7 @@ exports[`SFC compile <script setup> ref: syntax sugar using ref binding in prope
 
 export default {
   expose: [],
-  setup() {
+  setup(__props) {
 
       const a = _ref(1)
       const b = { a: a.value }
@@ -401,7 +344,7 @@ exports[`SFC compile <script setup> should expose top level declarations 1`] = `
       
 export default {
   expose: [],
-  setup() {
+  setup(__props) {
 
       let a = 1
       const b = 2
@@ -509,7 +452,7 @@ export default _defineComponent({
     literalUnionMixed: { type: [String, Number, Boolean], required: true },
     intersection: { type: Object, required: true }
   } as unknown as undefined,
-  setup() {
+  setup(__props) {
 
       
       
@@ -526,7 +469,7 @@ export interface Foo {}
       
 export default _defineComponent({
   expose: [],
-  setup() {
+  setup(__props) {
 
         
 return {  }
diff --git a/packages/compiler-sfc/__tests__/__snapshots__/cssVars.spec.ts.snap b/packages/compiler-sfc/__tests__/__snapshots__/cssVars.spec.ts.snap
new file mode 100644 (file)
index 0000000..41c165f
--- /dev/null
@@ -0,0 +1,107 @@
+// 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 }
+}
+
+}"
+`;
index 5a8e24c4ea3c3386c43c952f6ab3fee89c60ab80..a336dd2c07f9daefcaab9eab1d4f0a008af0911c 100644 (file)
@@ -1,25 +1,4 @@
-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', () => {
@@ -323,51 +302,10 @@ const { props, emit } = defineOptions({
     })
   })
 
-  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', () => {
index ce83747033b3d86d43d641173f2af11c8a48d329..a1b3b21eeba0be84590cdc9b3868c1e319b3da6d 100644 (file)
@@ -9,27 +9,27 @@ import {
 } 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;`
@@ -266,27 +266,6 @@ describe('SFC scoped CSS', () => {
       ).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', () => {
diff --git a/packages/compiler-sfc/__tests__/cssVars.spec.ts b/packages/compiler-sfc/__tests__/cssVars.spec.ts
new file mode 100644 (file)
index 0000000..c32daf0
--- /dev/null
@@ -0,0 +1,111 @@
+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);
+      }"
+    `)
+  })
+})
diff --git a/packages/compiler-sfc/__tests__/utils.ts b/packages/compiler-sfc/__tests__/utils.ts
new file mode 100644 (file)
index 0000000..ae68611
--- /dev/null
@@ -0,0 +1,28 @@
+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()
+}
index a049d205971fe1c135858c4a1c93e60e16389f9f..522277db2f464957b76d49c4d7b1f3794af7dfdd 100644 (file)
@@ -1,5 +1,5 @@
 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'
@@ -26,14 +26,20 @@ import { walk } from 'estree-walker'
 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
    */
@@ -52,7 +58,7 @@ export interface SFCScriptCompileOptions {
    * from being hot-reloaded separately from component state.
    */
   inlineTemplate?: boolean
-  templateOptions?: SFCTemplateCompileOptions
+  templateOptions?: Partial<SFCTemplateCompileOptions>
 }
 
 const hasWarned: Record<string, boolean> = {}
@@ -71,19 +77,33 @@ function warnOnce(msg: string) {
  */
 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'
@@ -104,10 +124,13 @@ export function compileScript(
         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) {
@@ -491,7 +514,9 @@ export function compileScript(
         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,
@@ -512,10 +537,22 @@ export function compileScript(
     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' &&
@@ -524,19 +561,11 @@ export function compileScript(
         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)
           }
@@ -546,7 +575,6 @@ export function compileScript(
             source: node.source.value
           }
         }
-        prev = specifier
       }
       if (removed === node.specifiers.length) {
         s.remove(node.start! + startOffset, node.end! + startOffset)
@@ -732,7 +760,7 @@ export function compileScript(
   }
 
   // 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')
@@ -745,26 +773,7 @@ export function compileScript(
 }`
   }
 
-  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))
   }
@@ -785,13 +794,23 @@ export function compileScript(
     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,
@@ -813,12 +832,22 @@ export function compileScript(
       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`)
index cf0761ffc0904550b44fc0fb81115828f45cb435..529682fbe8122bebeaa768b939636763a2580902 100644 (file)
@@ -7,7 +7,6 @@ import postcss, {
 } from 'postcss'
 import trimPlugin from './stylePluginTrim'
 import scopedPlugin from './stylePluginScoped'
-import scopedVarsPlugin from './stylePluginScopedVars'
 import {
   processors,
   StylePreprocessor,
@@ -15,6 +14,7 @@ import {
   PreprocessLang
 } from './stylePreprocessors'
 import { RawSourceMap } from 'source-map'
+import { cssVarsPlugin } from './cssVars'
 
 export interface SFCStyleCompileOptions {
   source: string
@@ -22,7 +22,6 @@ export interface SFCStyleCompileOptions {
   id: string
   map?: RawSourceMap
   scoped?: boolean
-  vars?: boolean
   trim?: boolean
   preprocessLang?: PreprocessLang
   preprocessOptions?: any
@@ -82,7 +81,6 @@ export function doCompileStyle(
     filename,
     id,
     scoped = false,
-    vars = false,
     trim = true,
     modules = false,
     modulesOptions = {},
@@ -96,11 +94,7 @@ export function doCompileStyle(
   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())
   }
index 85c26eae886611c7cfdeb058db0b32d86052d4ea..4f8fc11eecb941c2625a4813882cc84e854c6c9c 100644 (file)
@@ -30,6 +30,7 @@ export interface TemplateCompiler {
 
 export interface SFCTemplateCompileResults {
   code: string
+  ast?: RootNode
   preamble?: string
   source: string
   tips: string[]
@@ -169,7 +170,7 @@ function doCompileTemplate({
     nodeTransforms = [transformAssetUrl, transformSrcset]
   }
 
-  let { code, preamble, map } = compiler.compile(source, {
+  let { code, ast, preamble, map } = compiler.compile(source, {
     mode: 'module',
     prefixIdentifiers: true,
     hoistStatic: true,
@@ -193,7 +194,7 @@ function doCompileTemplate({
     }
   }
 
-  return { code, preamble, source, errors, tips: [], map }
+  return { code, ast, preamble, source, errors, tips: [], map }
 }
 
 function mapLines(oldMap: RawSourceMap, newMap: RawSourceMap): RawSourceMap {
similarity index 52%
rename from packages/compiler-sfc/src/genCssVars.ts
rename to packages/compiler-sfc/src/cssVars.ts
index 69d1457f6ff45b1900702f7f5e3203487bfeb8a1..f88de5bb6b119e327f089a56582888ff40c1ca93 100644 (file)
@@ -4,30 +4,62 @@ import {
   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
@@ -40,15 +72,16 @@ export function genCssVarsCode(
           })
           .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(
@@ -57,18 +90,14 @@ export function injectCssVarsCalls(
     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` +
index 9780e350ec4781d4f824abbd67a819c41ae2e937..7c44d5e2e9b3e06fc6d685043023b0985f90a5b4 100644 (file)
@@ -45,7 +45,6 @@ export interface SFCScriptBlock extends SFCBlock {
 export interface SFCStyleBlock extends SFCBlock {
   type: 'style'
   scoped?: boolean
-  vars?: string
   module?: string | boolean
 }
 
@@ -269,8 +268,6 @@ function createBlock(
       } 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]
         }
diff --git a/packages/compiler-sfc/src/stylePluginScopedVars.ts b/packages/compiler-sfc/src/stylePluginScopedVars.ts
deleted file mode 100644 (file)
index c56c8ba..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-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})`
-      })
-    }
-  })
-})
index 0766bda806d60489e79f5b8c112e5b064ec3198c..7ed30911a64de2e1b00f9e523e919b7fdc2c810a 100644 (file)
@@ -10,28 +10,26 @@ import {
 } 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'
+      )
     }
   }
 
@@ -39,9 +37,12 @@ describe('useCssVars', () => {
     await assertCssVars(state => ({
       setup() {
         // test receiving render context
-        useCssVars((ctx: any) => ({
-          color: ctx.color
-        }))
+        useCssVars(
+          (ctx: any) => ({
+            color: ctx.color
+          }),
+          id
+        )
         return state
       },
       render() {
@@ -53,7 +54,7 @@ describe('useCssVars', () => {
   test('on fragment root', async () => {
     await assertCssVars(state => ({
       setup() {
-        useCssVars(() => state)
+        useCssVars(() => state, id)
         return () => [h('div'), h('div')]
       }
     }))
@@ -64,7 +65,7 @@ describe('useCssVars', () => {
 
     await assertCssVars(state => ({
       setup() {
-        useCssVars(() => state)
+        useCssVars(() => state, id)
         return () => h(Child)
       }
     }))
@@ -74,15 +75,23 @@ describe('useCssVars', () => {
     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),
@@ -92,39 +101,42 @@ describe('useCssVars', () => {
     }
 
     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 () => {
@@ -134,21 +146,26 @@ describe('useCssVars', () => {
 
     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'
+      )
     }
   })
 })
index 1f5ac7fae48c9b7a8cc67498637802f215dd3734..7dfdaf41fbeb5cf0cf2a2517a489930b46f4ec75 100644 (file)
@@ -5,15 +5,18 @@ import {
   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 */
@@ -23,13 +26,8 @@ export function useCssVars(
     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)
 }
@@ -37,14 +35,14 @@ export function useCssVars(
 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)
       })
     }
   }
@@ -57,9 +55,9 @@ function setVarsOnVNode(
   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))
   }
 }