]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(reactivity-transform): use toRef() for $() destructure codegen
authorEvan You <yyx990803@gmail.com>
Sat, 11 Dec 2021 09:10:31 +0000 (17:10 +0800)
committerEvan You <yyx990803@gmail.com>
Sat, 11 Dec 2021 09:13:15 +0000 (17:13 +0800)
- now supports destructuring reactive objects
- no longer supports rest elements

packages/compiler-sfc/src/compileScript.ts
packages/ref-transform/__tests__/__snapshots__/refTransform.spec.ts.snap
packages/ref-transform/__tests__/refTransform.spec.ts
packages/ref-transform/src/refTransform.ts

index bec502741e0e4d1092301fe628a98220fd477271..1823d28cba6a05ee83a519b004a48a599b230810 100644 (file)
@@ -81,15 +81,22 @@ export interface SFCScriptCompileOptions {
    * https://babeljs.io/docs/en/babel-parser#plugins
    */
   babelParserPlugins?: ParserPlugin[]
+  /**
+   * (Experimental) Enable syntax transform for using refs without `.value` and
+   * using destructured props with reactivity
+   */
+  reactivityTransform?: boolean
   /**
    * (Experimental) Enable syntax transform for using refs without `.value`
    * https://github.com/vuejs/rfcs/discussions/369
+   * @deprecated now part of `reactivityTransform`
    * @default false
    */
   refTransform?: boolean
   /**
    * (Experimental) Enable syntax transform for destructuring from defineProps()
    * https://github.com/vuejs/rfcs/discussions/394
+   * @deprecated now part of `reactivityTransform`
    * @default false
    */
   propsDestructureTransform?: boolean
@@ -132,8 +139,13 @@ export function compileScript(
 ): SFCScriptBlock {
   let { script, scriptSetup, source, filename } = sfc
   // feature flags
-  const enableRefTransform = !!options.refSugar || !!options.refTransform
-  const enablePropsTransform = !!options.propsDestructureTransform
+  // TODO remove support for deprecated options when out of experimental
+  const enableRefTransform =
+    !!options.reactivityTransform ||
+    !!options.refSugar ||
+    !!options.refTransform
+  const enablePropsTransform =
+    !!options.reactivityTransform || !!options.propsDestructureTransform
   const isProd = !!options.isProd
   const genSourceMap = options.sourceMap !== false
   let refBindings: string[] | undefined
@@ -1097,8 +1109,7 @@ export function compileScript(
       s,
       startOffset,
       refBindings,
-      propsDestructuredBindings,
-      !enableRefTransform
+      propsDestructuredBindings
     )
     refBindings = refBindings ? [...refBindings, ...rootRefs] : rootRefs
     for (const h of importedHelpers) {
index 4234d830826b9bd6f6f60d2c4fa08b8ef05397b4..5253bcce1983bc61d7400816c7ad891e029b9e03 100644 (file)
@@ -55,13 +55,12 @@ exports[`accessing ref binding 1`] = `
 `;
 
 exports[`array destructure 1`] = `
-"import { ref as _ref, shallowRef as _shallowRef } from 'vue'
+"import { ref as _ref, toRef as _toRef } from 'vue'
 
-    let n = _ref(1), [__a, __b = 1, ...__c] = (useFoo())
-const a = _shallowRef(__a);
-const b = _shallowRef(__b);
-const c = _shallowRef(__c);
-    console.log(n.value, a.value, b.value, c.value)
+    let n = _ref(1), __$temp_1 = (useFoo()),
+  a = _toRef(__$temp_1, 0),
+  b = _toRef(__$temp_1, 1, 1)
+    console.log(n.value, a.value, b.value)
     "
 `;
 
@@ -114,13 +113,13 @@ exports[`mutating ref binding 1`] = `
 `;
 
 exports[`nested destructure 1`] = `
-"import { shallowRef as _shallowRef } from 'vue'
+"import { toRef as _toRef } from 'vue'
 
-    let [{ a: { b: __b }}] = (useFoo())
-const b = _shallowRef(__b);
-    let { c: [__d, __e] } = (useBar())
-const d = _shallowRef(__d);
-const e = _shallowRef(__e);
+    let __$temp_1 = (useFoo()),
+  b = _toRef(__$temp_1[0].a, 'b')
+    let __$temp_2 = (useBar()),
+  d = _toRef(__$temp_2.c, 0),
+  e = _toRef(__$temp_2.c, 1)
     console.log(b.value, d.value, e.value)
     "
 `;
@@ -163,20 +162,29 @@ exports[`nested scopes 1`] = `
 `;
 
 exports[`object destructure 1`] = `
-"import { ref as _ref, shallowRef as _shallowRef } from 'vue'
-
-    let n = _ref(1), { a: __a, b: __c, d: __d = 1, e: __f = 2, ...__g } = (useFoo())
-const a = _shallowRef(__a);
-const c = _shallowRef(__c);
-const d = _shallowRef(__d);
-const f = _shallowRef(__f);
-const g = _shallowRef(__g);
-    let { foo: __foo } = (useSomthing(() => 1));
-const foo = _shallowRef(__foo);
-    console.log(n.value, a.value, c.value, d.value, f.value, g.value, foo.value)
+"import { ref as _ref, toRef as _toRef } from 'vue'
+
+    let n = _ref(1), __$temp_1 = (useFoo()),
+  a = _toRef(__$temp_1, 'a'),
+  c = _toRef(__$temp_1, 'b'),
+  d = _toRef(__$temp_1, 'd', 1),
+  f = _toRef(__$temp_1, 'e', 2),
+  h = _toRef(__$temp_1, g)
+    let __$temp_2 = (useSomthing(() => 1)),
+  foo = _toRef(__$temp_2, 'foo');
+    console.log(n.value, a.value, c.value, d.value, f.value, h.value, foo.value)
     "
 `;
 
+exports[`object destructure w/ mid-path default values 1`] = `
+"import { toRef as _toRef } from 'vue'
+
+    const __$temp_1 = (useFoo()),
+  b = _toRef((__$temp_1.a || { b: 123 }), 'b')
+    console.log(b.value)
+  "
+`;
+
 exports[`should not rewrite scope variable 1`] = `
 "import { ref as _ref } from 'vue'
 
index 5e8775b6c52708fe6f65e4e85088484211333813..712a234027a0d99d9ce018c53b84a862b49356d3 100644 (file)
@@ -201,40 +201,43 @@ test('should not rewrite scope variable', () => {
 
 test('object destructure', () => {
   const { code, rootRefs } = transform(`
-    let n = $ref(1), { a, b: c, d = 1, e: f = 2, ...g } = $(useFoo())
+    let n = $ref(1), { a, b: c, d = 1, e: f = 2, [g]: h } = $(useFoo())
     let { foo } = $(useSomthing(() => 1));
-    console.log(n, a, c, d, f, g, foo)
+    console.log(n, a, c, d, f, h, foo)
     `)
+  expect(code).toMatch(`a = _toRef(__$temp_1, 'a')`)
+  expect(code).toMatch(`c = _toRef(__$temp_1, 'b')`)
+  expect(code).toMatch(`d = _toRef(__$temp_1, 'd', 1)`)
+  expect(code).toMatch(`f = _toRef(__$temp_1, 'e', 2)`)
+  expect(code).toMatch(`h = _toRef(__$temp_1, g)`)
+  expect(code).toMatch(`foo = _toRef(__$temp_2, 'foo')`)
   expect(code).toMatch(
-    `let n = _ref(1), { a: __a, b: __c, d: __d = 1, e: __f = 2, ...__g } = (useFoo())`
+    `console.log(n.value, a.value, c.value, d.value, f.value, h.value, foo.value)`
   )
-  expect(code).toMatch(`let { foo: __foo } = (useSomthing(() => 1))`)
-  expect(code).toMatch(`\nconst a = _shallowRef(__a);`)
-  expect(code).not.toMatch(`\nconst b = _shallowRef(__b);`)
-  expect(code).toMatch(`\nconst c = _shallowRef(__c);`)
-  expect(code).toMatch(`\nconst d = _shallowRef(__d);`)
-  expect(code).not.toMatch(`\nconst e = _shallowRef(__e);`)
-  expect(code).toMatch(`\nconst f = _shallowRef(__f);`)
-  expect(code).toMatch(`\nconst g = _shallowRef(__g);`)
-  expect(code).toMatch(`\nconst foo = _shallowRef(__foo);`)
-  expect(code).toMatch(
-    `console.log(n.value, a.value, c.value, d.value, f.value, g.value, foo.value)`
-  )
-  expect(rootRefs).toStrictEqual(['n', 'a', 'c', 'd', 'f', 'g', 'foo'])
+  expect(rootRefs).toStrictEqual(['n', 'a', 'c', 'd', 'f', 'h', 'foo'])
+  assertCode(code)
+})
+
+test('object destructure w/ mid-path default values', () => {
+  const { code, rootRefs } = transform(`
+    const { a: { b } = { b: 123 }} = $(useFoo())
+    console.log(b)
+  `)
+  expect(code).toMatch(`b = _toRef((__$temp_1.a || { b: 123 }), 'b')`)
+  expect(code).toMatch(`console.log(b.value)`)
+  expect(rootRefs).toStrictEqual(['b'])
   assertCode(code)
 })
 
 test('array destructure', () => {
   const { code, rootRefs } = transform(`
-    let n = $ref(1), [a, b = 1, ...c] = $(useFoo())
-    console.log(n, a, b, c)
+    let n = $ref(1), [a, b = 1] = $(useFoo())
+    console.log(n, a, b)
     `)
-  expect(code).toMatch(`let n = _ref(1), [__a, __b = 1, ...__c] = (useFoo())`)
-  expect(code).toMatch(`\nconst a = _shallowRef(__a);`)
-  expect(code).toMatch(`\nconst b = _shallowRef(__b);`)
-  expect(code).toMatch(`\nconst c = _shallowRef(__c);`)
-  expect(code).toMatch(`console.log(n.value, a.value, b.value, c.value)`)
-  expect(rootRefs).toStrictEqual(['n', 'a', 'b', 'c'])
+  expect(code).toMatch(`a = _toRef(__$temp_1, 0)`)
+  expect(code).toMatch(`b = _toRef(__$temp_1, 1, 1)`)
+  expect(code).toMatch(`console.log(n.value, a.value, b.value)`)
+  expect(rootRefs).toStrictEqual(['n', 'a', 'b'])
   assertCode(code)
 })
 
@@ -244,13 +247,9 @@ test('nested destructure', () => {
     let { c: [d, e] } = $(useBar())
     console.log(b, d, e)
     `)
-  expect(code).toMatch(`let [{ a: { b: __b }}] = (useFoo())`)
-  expect(code).toMatch(`let { c: [__d, __e] } = (useBar())`)
-  expect(code).not.toMatch(`\nconst a = _shallowRef(__a);`)
-  expect(code).not.toMatch(`\nconst c = _shallowRef(__c);`)
-  expect(code).toMatch(`\nconst b = _shallowRef(__b);`)
-  expect(code).toMatch(`\nconst d = _shallowRef(__d);`)
-  expect(code).toMatch(`\nconst e = _shallowRef(__e);`)
+  expect(code).toMatch(`b = _toRef(__$temp_1[0].a, 'b')`)
+  expect(code).toMatch(`d = _toRef(__$temp_2.c, 0)`)
+  expect(code).toMatch(`e = _toRef(__$temp_2.c, 1)`)
   expect(rootRefs).toStrictEqual(['b', 'd', 'e'])
   assertCode(code)
 })
@@ -396,4 +395,13 @@ describe('errors', () => {
     `)
     expect(code).not.toMatch('.value')
   })
+
+  test('rest element in $() destructure', () => {
+    expect(() => transform(`let { a, ...b } = $(foo())`)).toThrow(
+      `does not support rest element`
+    )
+    expect(() => transform(`let [a, ...b] = $(foo())`)).toThrow(
+      `does not support rest element`
+    )
+  })
 })
index 9f7a4fd72fa0c16a117971c1fb917413cd7ad121..a3d50d918b060180013cb7cf66addf32493ec4dc 100644 (file)
@@ -4,10 +4,10 @@ import {
   BlockStatement,
   CallExpression,
   ObjectPattern,
-  VariableDeclaration,
   ArrayPattern,
   Program,
-  VariableDeclarator
+  VariableDeclarator,
+  Expression
 } from '@babel/types'
 import MagicString, { SourceMap } from 'magic-string'
 import { walk } from 'estree-walker'
@@ -20,7 +20,7 @@ import {
   walkFunctionParams
 } from '@vue/compiler-core'
 import { parse, ParserPlugin } from '@babel/parser'
-import { hasOwn } from '@vue/shared'
+import { hasOwn, isArray, isString } from '@vue/shared'
 
 const TO_VAR_SYMBOL = '$'
 const TO_REF_SYMBOL = '$$'
@@ -71,7 +71,7 @@ export function transform(
     plugins
   })
   const s = new MagicString(src)
-  const res = transformAST(ast.program, s)
+  const res = transformAST(ast.program, s, 0)
 
   // inject helper imports
   if (res.importedHelpers.length) {
@@ -106,16 +106,13 @@ export function transformAST(
       local: string // local identifier, may be different
       default?: any
     }
-  >,
-  rewritePropsOnly = false
+  >
 ): {
   rootRefs: string[]
   importedHelpers: string[]
 } {
   // TODO remove when out of experimental
-  if (!rewritePropsOnly) {
-    warnExperimental()
-  }
+  warnExperimental()
 
   const importedHelpers = new Set<string>()
   const rootScope: Scope = {}
@@ -139,7 +136,6 @@ export function transformAST(
   }
 
   function error(msg: string, node: Node) {
-    if (rewritePropsOnly) return
     const e = new Error(msg)
     ;(e as any).node = node
     throw e
@@ -164,6 +160,15 @@ export function transformAST(
 
   const registerRefBinding = (id: Identifier) => registerBinding(id, true)
 
+  let tempVarCount = 0
+  function genTempVar() {
+    return `__$temp_${++tempVarCount}`
+  }
+
+  function snip(node: Node) {
+    return s.original.slice(node.start! + offset, node.end! + offset)
+  }
+
   function walkScope(node: Program | BlockStatement, isRoot = false) {
     for (const stmt of node.body) {
       if (stmt.type === 'VariableDeclaration') {
@@ -180,9 +185,8 @@ export function transformAST(
           ) {
             processRefDeclaration(
               toVarCall,
-              decl.init as CallExpression,
               decl.id,
-              stmt
+              decl.init as CallExpression
             )
           } else {
             const isProps =
@@ -212,9 +216,8 @@ export function transformAST(
 
   function processRefDeclaration(
     method: string,
-    call: CallExpression,
     id: VariableDeclarator['id'],
-    statement: VariableDeclaration
+    call: CallExpression
   ) {
     excludedIds.add(call.callee as Identifier)
     if (method === TO_VAR_SYMBOL) {
@@ -225,9 +228,9 @@ export function transformAST(
         // single variable
         registerRefBinding(id)
       } else if (id.type === 'ObjectPattern') {
-        processRefObjectPattern(id, statement)
+        processRefObjectPattern(id, call)
       } else if (id.type === 'ArrayPattern') {
-        processRefArrayPattern(id, statement)
+        processRefArrayPattern(id, call)
       }
     } else {
       // shorthands
@@ -247,15 +250,24 @@ export function transformAST(
 
   function processRefObjectPattern(
     pattern: ObjectPattern,
-    statement: VariableDeclaration
+    call: CallExpression,
+    tempVar?: string,
+    path: PathSegment[] = []
   ) {
+    if (!tempVar) {
+      tempVar = genTempVar()
+      // const { x } = $(useFoo()) --> const __$temp_1 = useFoo()
+      s.overwrite(pattern.start! + offset, pattern.end! + offset, tempVar)
+    }
+
     for (const p of pattern.properties) {
       let nameId: Identifier | undefined
+      let key: Expression | string | undefined
+      let defaultValue: Expression | undefined
       if (p.type === 'ObjectProperty') {
         if (p.key.start! === p.value.start!) {
-          // shorthand { foo } --> { foo: __foo }
+          // shorthand { foo }
           nameId = p.key as Identifier
-          s.appendLeft(nameId.end! + offset, `: __${nameId.name}`)
           if (p.value.type === 'Identifier') {
             // avoid shorthand value identifier from being processed
             excludedIds.add(p.value)
@@ -265,33 +277,56 @@ export function transformAST(
           ) {
             // { foo = 1 }
             excludedIds.add(p.value.left)
+            defaultValue = p.value.right
           }
         } else {
+          key = p.computed ? p.key : (p.key as Identifier).name
           if (p.value.type === 'Identifier') {
-            // { foo: bar } --> { foo: __bar }
+            // { foo: bar }
             nameId = p.value
-            s.prependRight(nameId.start! + offset, `__`)
           } else if (p.value.type === 'ObjectPattern') {
-            processRefObjectPattern(p.value, statement)
+            processRefObjectPattern(p.value, call, tempVar, [...path, key])
           } else if (p.value.type === 'ArrayPattern') {
-            processRefArrayPattern(p.value, statement)
+            processRefArrayPattern(p.value, call, tempVar, [...path, key])
           } else if (p.value.type === 'AssignmentPattern') {
-            // { foo: bar = 1 } --> { foo: __bar = 1 }
-            nameId = p.value.left as Identifier
-            s.prependRight(nameId.start! + offset, `__`)
+            if (p.value.left.type === 'Identifier') {
+              // { foo: bar = 1 }
+              nameId = p.value.left
+              defaultValue = p.value.right
+            } else if (p.value.left.type === 'ObjectPattern') {
+              processRefObjectPattern(p.value.left, call, tempVar, [
+                ...path,
+                [key, p.value.right]
+              ])
+            } else if (p.value.left.type === 'ArrayPattern') {
+              processRefArrayPattern(p.value.left, call, tempVar, [
+                ...path,
+                [key, p.value.right]
+              ])
+            } else {
+              // MemberExpression case is not possible here, ignore
+            }
           }
         }
       } else {
-        // rest element { ...foo } --> { ...__foo }
-        nameId = p.argument as Identifier
-        s.prependRight(nameId.start! + offset, `__`)
+        // rest element { ...foo }
+        error(`reactivity destructure does not support rest elements.`, p)
       }
       if (nameId) {
         registerRefBinding(nameId)
-        // append binding declarations after the parent statement
+        // inject toRef() after original replaced pattern
+        const source = pathToString(tempVar, path)
+        const keyStr = isString(key)
+          ? `'${key}'`
+          : key
+          ? snip(key)
+          : `'${nameId.name}'`
+        const defaultStr = defaultValue ? `, ${snip(defaultValue)}` : ``
         s.appendLeft(
-          statement.end! + offset,
-          `\nconst ${nameId.name} = ${helper('shallowRef')}(__${nameId.name});`
+          call.end! + offset,
+          `,\n  ${nameId.name} = ${helper(
+            'toRef'
+          )}(${source}, ${keyStr}${defaultStr})`
         )
       }
     }
@@ -299,38 +334,80 @@ export function transformAST(
 
   function processRefArrayPattern(
     pattern: ArrayPattern,
-    statement: VariableDeclaration
+    call: CallExpression,
+    tempVar?: string,
+    path: PathSegment[] = []
   ) {
-    for (const e of pattern.elements) {
+    if (!tempVar) {
+      // const [x] = $(useFoo()) --> const __$temp_1 = useFoo()
+      tempVar = genTempVar()
+      s.overwrite(pattern.start! + offset, pattern.end! + offset, tempVar)
+    }
+
+    for (let i = 0; i < pattern.elements.length; i++) {
+      const e = pattern.elements[i]
       if (!e) continue
       let nameId: Identifier | undefined
+      let defaultValue: Expression | undefined
       if (e.type === 'Identifier') {
         // [a] --> [__a]
         nameId = e
       } else if (e.type === 'AssignmentPattern') {
-        // [a = 1] --> [__a = 1]
+        // [a = 1]
         nameId = e.left as Identifier
+        defaultValue = e.right
       } else if (e.type === 'RestElement') {
-        // [...a] --> [...__a]
-        nameId = e.argument as Identifier
+        // [...a]
+        error(`reactivity destructure does not support rest elements.`, e)
       } else if (e.type === 'ObjectPattern') {
-        processRefObjectPattern(e, statement)
+        processRefObjectPattern(e, call, tempVar, [...path, i])
       } else if (e.type === 'ArrayPattern') {
-        processRefArrayPattern(e, statement)
+        processRefArrayPattern(e, call, tempVar, [...path, i])
       }
       if (nameId) {
         registerRefBinding(nameId)
-        // prefix original
-        s.prependRight(nameId.start! + offset, `__`)
-        // append binding declarations after the parent statement
+        // inject toRef() after original replaced pattern
+        const source = pathToString(tempVar, path)
+        const defaultStr = defaultValue ? `, ${snip(defaultValue)}` : ``
         s.appendLeft(
-          statement.end! + offset,
-          `\nconst ${nameId.name} = ${helper('shallowRef')}(__${nameId.name});`
+          call.end! + offset,
+          `,\n  ${nameId.name} = ${helper(
+            'toRef'
+          )}(${source}, ${i}${defaultStr})`
         )
       }
     }
   }
 
+  type PathSegmentAtom = Expression | string | number
+
+  type PathSegment =
+    | PathSegmentAtom
+    | [PathSegmentAtom, Expression /* default value */]
+
+  function pathToString(source: string, path: PathSegment[]): string {
+    if (path.length) {
+      for (const seg of path) {
+        if (isArray(seg)) {
+          source = `(${source}${segToString(seg[0])} || ${snip(seg[1])})`
+        } else {
+          source += segToString(seg)
+        }
+      }
+    }
+    return source
+  }
+
+  function segToString(seg: PathSegmentAtom): string {
+    if (typeof seg === 'number') {
+      return `[${seg}]`
+    } else if (typeof seg === 'string') {
+      return `.${seg}`
+    } else {
+      return snip(seg)
+    }
+  }
+
   function rewriteId(
     scope: Scope,
     id: Identifier,
@@ -341,10 +418,6 @@ export function transformAST(
       const bindingType = scope[id.name]
       if (bindingType) {
         const isProp = bindingType === 'prop'
-        if (rewritePropsOnly && !isProp) {
-          return true
-        }
-        // ref
         if (isStaticProperty(parent) && parent.shorthand) {
           // let binding used in a property shorthand
           // { foo } -> { foo: foo.value }
@@ -498,7 +571,7 @@ function warnExperimental() {
     return
   }
   warnOnce(
-    `@vue/ref-transform is an experimental feature.\n` +
+    `Reactivity transform is an experimental feature.\n` +
       `Experimental features may change behavior between patch versions.\n` +
       `It is recommended to pin your vue dependencies to exact versions to avoid breakage.\n` +
       `You can follow the proposal's status at ${RFC_LINK}.`