]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(experimental): standalone ref transform
authorEvan You <yyx990803@gmail.com>
Mon, 23 Aug 2021 02:21:42 +0000 (22:21 -0400)
committerEvan You <yyx990803@gmail.com>
Mon, 23 Aug 2021 02:21:42 +0000 (22:21 -0400)
16 files changed:
.eslintrc.js
packages/compiler-core/src/babelUtils.ts
packages/compiler-core/src/transforms/transformExpression.ts
packages/compiler-sfc/__tests__/__snapshots__/compileScriptRefSugar.spec.ts.snap
packages/compiler-sfc/__tests__/compileScriptRefSugar.spec.ts
packages/compiler-sfc/package.json
packages/compiler-sfc/src/compileScript.ts
packages/compiler-sfc/src/index.ts
packages/ref-transform/README.md [new file with mode: 0644]
packages/ref-transform/__tests__/__snapshots__/refTransform.spec.ts.snap [new file with mode: 0644]
packages/ref-transform/__tests__/refTransform.spec.ts [new file with mode: 0644]
packages/ref-transform/api-extractor.json [new file with mode: 0644]
packages/ref-transform/package.json [new file with mode: 0644]
packages/ref-transform/src/babelPlugin.ts [new file with mode: 0644]
packages/ref-transform/src/index.ts [new file with mode: 0644]
yarn.lock

index 88034d237940b235e9a2b6a6087c59e053b355b1..ea44a000114986215f9a5888dc7aed685f523146 100644 (file)
@@ -49,7 +49,9 @@ module.exports = {
     },
     // Packages targeting Node
     {
-      files: ['packages/{compiler-sfc,compiler-ssr,server-renderer}/**'],
+      files: [
+        'packages/{compiler-sfc,compiler-ssr,server-renderer,ref-transform}/**'
+      ],
       rules: {
         'no-restricted-globals': ['error', ...DOMGlobals],
         'no-restricted-syntax': 'off'
index 6586c44633cf7fa8298dd0da61d5dee67b17d302..6f19f3057ef5e81b7aec2b23803e44844785ef0e 100644 (file)
@@ -3,7 +3,8 @@ import {
   Identifier,
   Node,
   Function,
-  ObjectProperty
+  ObjectProperty,
+  BlockStatement
 } from '@babel/types'
 import { walk } from 'estree-walker'
 
@@ -17,9 +18,10 @@ export function walkIdentifiers(
     isLocal: boolean
   ) => void,
   onNode?: (node: Node, parent: Node, parentStack: Node[]) => void | boolean,
+  includeAll = false,
+  analyzeScope = true,
   parentStack: Node[] = [],
-  knownIds: Record<string, number> = Object.create(null),
-  includeAll = false
+  knownIds: Record<string, number> = Object.create(null)
 ) {
   const rootExp =
     root.type === 'Program' &&
@@ -42,62 +44,41 @@ export function walkIdentifiers(
         return this.skip()
       }
       if (node.type === 'Identifier') {
-        const isLocal = !!knownIds[node.name]
+        const isLocal = analyzeScope && !!knownIds[node.name]
         const isRefed = isReferencedIdentifier(node, parent!, parentStack)
         if (includeAll || (isRefed && !isLocal)) {
           onIdentifier(node, parent!, parentStack, isRefed, isLocal)
         }
-      } else if (isFunctionType(node)) {
-        // walk function expressions and add its arguments to known identifiers
-        // so that we don't prefix them
-        for (const p of node.params) {
-          ;(walk as any)(p, {
-            enter(child: Node, parent: Node) {
-              if (
-                child.type === 'Identifier' &&
-                // do not record as scope variable if is a destructured key
-                !isStaticPropertyKey(child, parent) &&
-                // do not record if this is a default value
-                // assignment of a destructured variable
-                !(
-                  parent &&
-                  parent.type === 'AssignmentPattern' &&
-                  parent.right === child
-                )
-              ) {
-                markScopeIdentifier(node, child, knownIds)
-              }
-            }
-          })
-        }
-      } else if (node.type === 'BlockStatement') {
-        // #3445 record block-level local variables
-        for (const stmt of node.body) {
-          if (stmt.type === 'VariableDeclaration') {
-            for (const decl of stmt.declarations) {
-              for (const id of extractIdentifiers(decl.id)) {
-                markScopeIdentifier(node, id, knownIds)
-              }
-            }
-          }
-        }
       } else if (
         node.type === 'ObjectProperty' &&
         parent!.type === 'ObjectPattern'
       ) {
         // mark property in destructure pattern
         ;(node as any).inPattern = true
+      } else if (analyzeScope) {
+        if (isFunctionType(node)) {
+          // walk function expressions and add its arguments to known identifiers
+          // so that we don't prefix them
+          walkFunctionParams(node, id =>
+            markScopeIdentifier(node, id, knownIds)
+          )
+        } else if (node.type === 'BlockStatement') {
+          // #3445 record block-level local variables
+          walkBlockDeclarations(node, id =>
+            markScopeIdentifier(node, id, knownIds)
+          )
+        }
       }
     },
     leave(node: Node & { scopeIds?: Set<string> }, parent: Node | undefined) {
       parent && parentStack.pop()
-      if (node !== rootExp && node.scopeIds) {
-        node.scopeIds.forEach((id: string) => {
+      if (analyzeScope && node !== rootExp && node.scopeIds) {
+        for (const id of node.scopeIds) {
           knownIds[id]--
           if (knownIds[id] === 0) {
             delete knownIds[id]
           }
-        })
+        }
       }
     }
   })
@@ -156,6 +137,47 @@ export function isInDestructureAssignment(
   return false
 }
 
+export function walkFunctionParams(
+  node: Function,
+  onIdent: (id: Identifier) => void
+) {
+  for (const p of node.params) {
+    ;(walk as any)(p, {
+      enter(child: Node, parent: Node) {
+        if (
+          child.type === 'Identifier' &&
+          // do not record as scope variable if is a destructured key
+          !isStaticPropertyKey(child, parent) &&
+          // do not record if this is a default value
+          // assignment of a destructured variable
+          !(
+            parent &&
+            parent.type === 'AssignmentPattern' &&
+            parent.right === child
+          )
+        ) {
+          onIdent(child)
+        }
+      }
+    })
+  }
+}
+
+export function walkBlockDeclarations(
+  block: BlockStatement,
+  onIdent: (node: Identifier) => void
+) {
+  for (const stmt of block.body) {
+    if (stmt.type === 'VariableDeclaration') {
+      for (const decl of stmt.declarations) {
+        for (const id of extractIdentifiers(decl.id)) {
+          onIdent(id)
+        }
+      }
+    }
+  }
+}
+
 function extractIdentifiers(
   param: Node,
   nodes: Identifier[] = []
index 4c30707fcf4fa7f0f0e0fdff3e9e210cf9bd7f65..d20fe735aac6046c30cee89f31ec9001c5252ebd 100644 (file)
@@ -283,10 +283,10 @@ export function processExpression(
       }
     },
     undefined,
+    true, // invoke on ALL identifiers
+    true, // isLocal scope analysis
     parentStack,
-    knownIds,
-    // invoke on ALL identifiers
-    true
+    knownIds
   )
 
   // We break up the compound expression into an array of strings and sub
index dbe6aa1dd1b1ac6096c3378e6c674c128584c5f7..ed4605c16625f97285fdb1bea3fb88f1eaa76262 100644 (file)
@@ -1,33 +1,21 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`<script setup> ref sugar $computed declaration 1`] = `
-"import { computed as _computed } from 'vue'
-
-export default {
-  setup(__props, { expose }) {
-  expose()
-
-    const a = _computed(() => 1)
+exports[`<script setup> ref sugar $ unwrapping 1`] = `
+"import { ref, shallowRef } from 'vue'
     
-return { a }
-}
-
-}"
-`;
-
-exports[`<script setup> ref sugar $raw 1`] = `
-"import { ref as _ref } from 'vue'
-
 export default {
   setup(__props, { expose }) {
   expose()
 
-    let a = _ref(1)
-    const b = (a)
-    const c = ({ a })
-    callExternal((a))
+    let foo = (ref())
+    let a = (ref(1))
+    let b = (shallowRef({
+      count: 0
+    }))
+    let c = () => {}
+    let d
     
-return { a, b, c }
+return { foo, a, b, c, d, ref, shallowRef }
 }
 
 }"
@@ -53,231 +41,3 @@ return { foo, a, b, c, d }
 
 }"
 `;
-
-exports[`<script setup> ref sugar accessing ref binding 1`] = `
-"import { ref as _ref } from 'vue'
-
-export default {
-  setup(__props, { expose }) {
-  expose()
-
-    let a = _ref(1)
-    console.log(a.value)
-    function get() {
-      return a.value + 1
-    }
-    
-return { a, get }
-}
-
-}"
-`;
-
-exports[`<script setup> ref sugar array destructure 1`] = `
-"import { ref as _ref, shallowRef as _shallowRef } from 'vue'
-
-export default {
-  setup(__props, { expose }) {
-  expose()
-
-    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)
-    
-return { n, a, b, c }
-}
-
-}"
-`;
-
-exports[`<script setup> ref sugar handle TS casting syntax 1`] = `
-"import { ref as _ref, defineComponent as _defineComponent } from 'vue'
-
-export default _defineComponent({
-  setup(__props, { expose }) {
-  expose()
-
-      let a = _ref(1)
-      console.log(a.value!) 
-      console.log(a.value! + 1) 
-      console.log(a.value as number) 
-      console.log((a.value as number) + 1) 
-      console.log(<number>a.value) 
-      console.log(<number>a.value + 1) 
-      console.log(a.value! + (a.value as number)) 
-      console.log(a.value! + <number>a.value) 
-      console.log((a.value as number) + <number>a.value)
-      
-return { a }
-}
-
-})"
-`;
-
-exports[`<script setup> ref sugar mixing $ref & $computed declarations 1`] = `
-"import { ref as _ref, computed as _computed } from 'vue'
-
-export default {
-  setup(__props, { expose }) {
-  expose()
-
-    let a = _ref(1), b = _computed(() => a.value + 1)
-    
-return { a, b }
-}
-
-}"
-`;
-
-exports[`<script setup> ref sugar multi $ref declarations 1`] = `
-"import { ref as _ref } from 'vue'
-
-export default {
-  setup(__props, { expose }) {
-  expose()
-
-    let a = _ref(1), b = _ref(2), c = _ref({
-      count: 0
-    })
-    
-return { a, b, c }
-}
-
-}"
-`;
-
-exports[`<script setup> ref sugar mutating ref binding 1`] = `
-"import { ref as _ref } from 'vue'
-
-export default {
-  setup(__props, { expose }) {
-  expose()
-
-    let a = _ref(1)
-    let b = _ref({ count: 0 })
-    function inc() {
-      a.value++
-      a.value = a.value + 1
-      b.value.count++
-      b.value.count = b.value.count + 1
-      ;({ a: a.value } = { a: 2 })
-      ;[a.value] = [1]
-    }
-    
-return { a, b, inc }
-}
-
-}"
-`;
-
-exports[`<script setup> ref sugar nested destructure 1`] = `
-"import { shallowRef as _shallowRef } from 'vue'
-
-export default {
-  setup(__props, { expose }) {
-  expose()
-
-    let [{ a: { b: __b }}] = (useFoo())
-const b = _shallowRef(__b);
-    let { c: [__d, __e] } = (useBar())
-const d = _shallowRef(__d);
-const e = _shallowRef(__e);
-    console.log(b.value, d.value, e.value)
-    
-return { b, d, e }
-}
-
-}"
-`;
-
-exports[`<script setup> ref sugar object destructure 1`] = `
-"import { ref as _ref, shallowRef as _shallowRef } from 'vue'
-
-export default {
-  setup(__props, { expose }) {
-  expose()
-
-    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)
-    
-return { n, a, c, d, f, g, foo }
-}
-
-}"
-`;
-
-exports[`<script setup> ref sugar should not rewrite scope variable 1`] = `
-"import { ref as _ref } from 'vue'
-
-export default {
-  setup(__props, { expose }) {
-  expose()
-
-      let a = _ref(1)
-      let b = _ref(1)
-      let d = _ref(1)
-      const e = 1
-      function test() {
-        const a = 2
-        console.log(a)
-        console.log(b.value)
-        let c = { c: 3 }
-        console.log(c)
-        console.log(d.value)
-        console.log(e)
-      }
-    
-return { a, b, d, e, test }
-}
-
-}"
-`;
-
-exports[`<script setup> ref sugar should not rewrite type identifiers 1`] = `
-"import { ref as _ref, defineComponent as _defineComponent } from 'vue'
-
-export default _defineComponent({
-  props: {
-    msg: { type: String, required: true },
-    ids: { type: Array, required: false }
-  } as unknown as undefined,
-  setup(__props: {msg: string; ids?: string[]}, { expose }) {
-  expose()
-
-const props = __props
-        
-        let ids = _ref([])
-      
-return { props, ids }
-}
-
-})"
-`;
-
-exports[`<script setup> ref sugar using ref binding in property shorthand 1`] = `
-"import { ref as _ref } from 'vue'
-
-export default {
-  setup(__props, { expose }) {
-  expose()
-
-    let a = _ref(1)
-    const b = { a: a.value }
-    function test() {
-      const { a } = b
-    }
-    
-return { a, b, test }
-}
-
-}"
-`;
index 12baa1049ed9b9ea7890879a725a165ff75ff6d4..67c66150c72f458c26d59747b483baf1af3467af 100644 (file)
@@ -1,11 +1,50 @@
 import { BindingTypes } from '@vue/compiler-core'
 import { compileSFCScript as compile, assertCode } from './utils'
 
+// this file only tests integration with SFC - main test case for the ref
+// transform can be found in <root>/packages/ref-transform/__tests__
 describe('<script setup> ref sugar', () => {
   function compileWithRefSugar(src: string) {
     return compile(src, { refSugar: true })
   }
 
+  test('$ unwrapping', () => {
+    const { content, bindings } = compileWithRefSugar(`<script setup>
+    import { ref, shallowRef } from 'vue'
+    let foo = $(ref())
+    let a = $(ref(1))
+    let b = $(shallowRef({
+      count: 0
+    }))
+    let c = () => {}
+    let d
+    </script>`)
+    expect(content).not.toMatch(`$(ref())`)
+    expect(content).not.toMatch(`$(ref(1))`)
+    expect(content).not.toMatch(`$(shallowRef({`)
+    expect(content).toMatch(`let foo = (ref())`)
+    expect(content).toMatch(`let a = (ref(1))`)
+    expect(content).toMatch(`
+    let b = (shallowRef({
+      count: 0
+    }))
+    `)
+    // normal declarations left untouched
+    expect(content).toMatch(`let c = () => {}`)
+    expect(content).toMatch(`let d`)
+    expect(content).toMatch(`return { foo, a, b, c, d, ref, shallowRef }`)
+    assertCode(content)
+    expect(bindings).toStrictEqual({
+      foo: BindingTypes.SETUP_REF,
+      a: BindingTypes.SETUP_REF,
+      b: BindingTypes.SETUP_REF,
+      c: BindingTypes.SETUP_LET,
+      d: BindingTypes.SETUP_LET,
+      ref: BindingTypes.SETUP_CONST,
+      shallowRef: BindingTypes.SETUP_CONST
+    })
+  })
+
   test('$ref & $shallowRef declarations', () => {
     const { content, bindings } = compileWithRefSugar(`<script setup>
     let foo = $ref()
@@ -42,307 +81,7 @@ describe('<script setup> ref sugar', () => {
     })
   })
 
-  test('multi $ref declarations', () => {
-    const { content, bindings } = compileWithRefSugar(`<script setup>
-    let a = $ref(1), b = $ref(2), c = $ref({
-      count: 0
-    })
-    </script>`)
-    expect(content).toMatch(`
-    let a = _ref(1), b = _ref(2), c = _ref({
-      count: 0
-    })
-    `)
-    expect(content).toMatch(`return { a, b, c }`)
-    assertCode(content)
-    expect(bindings).toStrictEqual({
-      a: BindingTypes.SETUP_REF,
-      b: BindingTypes.SETUP_REF,
-      c: BindingTypes.SETUP_REF
-    })
-  })
-
-  test('$computed declaration', () => {
-    const { content, bindings } = compileWithRefSugar(`<script setup>
-    const a = $computed(() => 1)
-    </script>`)
-    expect(content).toMatch(`
-    const a = _computed(() => 1)
-    `)
-    expect(content).toMatch(`return { a }`)
-    assertCode(content)
-    expect(bindings).toStrictEqual({
-      a: BindingTypes.SETUP_REF
-    })
-  })
-
-  test('mixing $ref & $computed declarations', () => {
-    const { content, bindings } = compileWithRefSugar(`<script setup>
-    let a = $ref(1), b = $computed(() => a + 1)
-    </script>`)
-    expect(content).toMatch(`
-    let a = _ref(1), b = _computed(() => a.value + 1)
-    `)
-    expect(content).toMatch(`return { a, b }`)
-    assertCode(content)
-    expect(bindings).toStrictEqual({
-      a: BindingTypes.SETUP_REF,
-      b: BindingTypes.SETUP_REF
-    })
-  })
-
-  test('accessing ref binding', () => {
-    const { content } = compileWithRefSugar(`<script setup>
-    let a = $ref(1)
-    console.log(a)
-    function get() {
-      return a + 1
-    }
-    </script>`)
-    expect(content).toMatch(`console.log(a.value)`)
-    expect(content).toMatch(`return a.value + 1`)
-    assertCode(content)
-  })
-
-  test('cases that should not append .value', () => {
-    const { content } = compileWithRefSugar(`<script setup>
-    let a = $ref(1)
-    console.log(b.a)
-    function get(a) {
-      return a + 1
-    }
-    </script>`)
-    expect(content).not.toMatch(`a.value`)
-  })
-
-  test('mutating ref binding', () => {
-    const { content } = compileWithRefSugar(`<script setup>
-    let a = $ref(1)
-    let b = $ref({ count: 0 })
-    function inc() {
-      a++
-      a = a + 1
-      b.count++
-      b.count = b.count + 1
-      ;({ a } = { a: 2 })
-      ;[a] = [1]
-    }
-    </script>`)
-    expect(content).toMatch(`a.value++`)
-    expect(content).toMatch(`a.value = a.value + 1`)
-    expect(content).toMatch(`b.value.count++`)
-    expect(content).toMatch(`b.value.count = b.value.count + 1`)
-    expect(content).toMatch(`;({ a: a.value } = { a: 2 })`)
-    expect(content).toMatch(`;[a.value] = [1]`)
-    assertCode(content)
-  })
-
-  test('using ref binding in property shorthand', () => {
-    const { content } = compileWithRefSugar(`<script setup>
-    let a = $ref(1)
-    const b = { a }
-    function test() {
-      const { a } = b
-    }
-    </script>`)
-    expect(content).toMatch(`const b = { a: a.value }`)
-    // should not convert destructure
-    expect(content).toMatch(`const { a } = b`)
-    assertCode(content)
-  })
-
-  test('should not rewrite scope variable', () => {
-    const { content } = compileWithRefSugar(`
-    <script setup>
-      let a = $ref(1)
-      let b = $ref(1)
-      let d = $ref(1)
-      const e = 1
-      function test() {
-        const a = 2
-        console.log(a)
-        console.log(b)
-        let c = { c: 3 }
-        console.log(c)
-        console.log(d)
-        console.log(e)
-      }
-    </script>`)
-    expect(content).toMatch('console.log(a)')
-    expect(content).toMatch('console.log(b.value)')
-    expect(content).toMatch('console.log(c)')
-    expect(content).toMatch('console.log(d.value)')
-    expect(content).toMatch('console.log(e)')
-    assertCode(content)
-  })
-
-  test('object destructure', () => {
-    const { content, bindings } = compileWithRefSugar(`<script setup>
-    let n = $ref(1), { a, b: c, d = 1, e: f = 2, ...g } = $fromRefs(useFoo())
-    let { foo } = $fromRefs(useSomthing(() => 1));
-    console.log(n, a, c, d, f, g, foo)
-    </script>`)
-    expect(content).toMatch(
-      `let n = _ref(1), { a: __a, b: __c, d: __d = 1, e: __f = 2, ...__g } = (useFoo())`
-    )
-    expect(content).toMatch(`let { foo: __foo } = (useSomthing(() => 1))`)
-    expect(content).toMatch(`\nconst a = _shallowRef(__a);`)
-    expect(content).not.toMatch(`\nconst b = _shallowRef(__b);`)
-    expect(content).toMatch(`\nconst c = _shallowRef(__c);`)
-    expect(content).toMatch(`\nconst d = _shallowRef(__d);`)
-    expect(content).not.toMatch(`\nconst e = _shallowRef(__e);`)
-    expect(content).toMatch(`\nconst f = _shallowRef(__f);`)
-    expect(content).toMatch(`\nconst g = _shallowRef(__g);`)
-    expect(content).toMatch(`\nconst foo = _shallowRef(__foo);`)
-    expect(content).toMatch(
-      `console.log(n.value, a.value, c.value, d.value, f.value, g.value, foo.value)`
-    )
-    expect(content).toMatch(`return { n, a, c, d, f, g, foo }`)
-    expect(bindings).toStrictEqual({
-      n: BindingTypes.SETUP_REF,
-      a: BindingTypes.SETUP_REF,
-      c: BindingTypes.SETUP_REF,
-      d: BindingTypes.SETUP_REF,
-      f: BindingTypes.SETUP_REF,
-      g: BindingTypes.SETUP_REF,
-      foo: BindingTypes.SETUP_REF
-    })
-    assertCode(content)
-  })
-
-  test('array destructure', () => {
-    const { content, bindings } = compileWithRefSugar(`<script setup>
-    let n = $ref(1), [a, b = 1, ...c] = $fromRefs(useFoo())
-    console.log(n, a, b, c)
-    </script>`)
-    expect(content).toMatch(
-      `let n = _ref(1), [__a, __b = 1, ...__c] = (useFoo())`
-    )
-    expect(content).toMatch(`\nconst a = _shallowRef(__a);`)
-    expect(content).toMatch(`\nconst b = _shallowRef(__b);`)
-    expect(content).toMatch(`\nconst c = _shallowRef(__c);`)
-    expect(content).toMatch(`console.log(n.value, a.value, b.value, c.value)`)
-    expect(content).toMatch(`return { n, a, b, c }`)
-    expect(bindings).toStrictEqual({
-      n: BindingTypes.SETUP_REF,
-      a: BindingTypes.SETUP_REF,
-      b: BindingTypes.SETUP_REF,
-      c: BindingTypes.SETUP_REF
-    })
-    assertCode(content)
-  })
-
-  test('nested destructure', () => {
-    const { content, bindings } = compileWithRefSugar(`<script setup>
-    let [{ a: { b }}] = $fromRefs(useFoo())
-    let { c: [d, e] } = $fromRefs(useBar())
-    console.log(b, d, e)
-    </script>`)
-    expect(content).toMatch(`let [{ a: { b: __b }}] = (useFoo())`)
-    expect(content).toMatch(`let { c: [__d, __e] } = (useBar())`)
-    expect(content).not.toMatch(`\nconst a = _shallowRef(__a);`)
-    expect(content).not.toMatch(`\nconst c = _shallowRef(__c);`)
-    expect(content).toMatch(`\nconst b = _shallowRef(__b);`)
-    expect(content).toMatch(`\nconst d = _shallowRef(__d);`)
-    expect(content).toMatch(`\nconst e = _shallowRef(__e);`)
-    expect(content).toMatch(`return { b, d, e }`)
-    expect(bindings).toStrictEqual({
-      b: BindingTypes.SETUP_REF,
-      d: BindingTypes.SETUP_REF,
-      e: BindingTypes.SETUP_REF
-    })
-    assertCode(content)
-  })
-
-  test('$raw', () => {
-    const { content } = compileWithRefSugar(`<script setup>
-    let a = $ref(1)
-    const b = $raw(a)
-    const c = $raw({ a })
-    callExternal($raw(a))
-    </script>`)
-    expect(content).toMatch(`const b = (a)`)
-    expect(content).toMatch(`const c = ({ a })`)
-    expect(content).toMatch(`callExternal((a))`)
-    assertCode(content)
-  })
-
-  //#4062
-  test('should not rewrite type identifiers', () => {
-    const { content } = compile(
-      `
-      <script setup lang="ts">
-        const props = defineProps<{msg: string; ids?: string[]}>()
-        let ids = $ref([])
-      </script>`,
-      {
-        refSugar: true
-      }
-    )
-    assertCode(content)
-    expect(content).not.toMatch('.value')
-  })
-
-  // #4254
-  test('handle TS casting syntax', () => {
-    const { content } = compile(
-      `
-      <script setup lang="ts">
-      let a = $ref(1)
-      console.log(a!) 
-      console.log(a! + 1) 
-      console.log(a as number) 
-      console.log((a as number) + 1) 
-      console.log(<number>a) 
-      console.log(<number>a + 1) 
-      console.log(a! + (a as number)) 
-      console.log(a! + <number>a) 
-      console.log((a as number) + <number>a)
-      </script>`,
-      {
-        refSugar: true
-      }
-    )
-    assertCode(content)
-    expect(content).toMatch('console.log(a.value!)')
-    expect(content).toMatch('console.log(a.value as number)')
-    expect(content).toMatch('console.log(<number>a.value)')
-  })
-
   describe('errors', () => {
-    test('non-let $ref declaration', () => {
-      expect(() =>
-        compile(
-          `<script setup>
-        const a = $ref(1)
-        </script>`,
-          { refSugar: true }
-        )
-      ).toThrow(`$ref() bindings can only be declared with let`)
-    })
-
-    test('$ref w/ destructure', () => {
-      expect(() =>
-        compile(
-          `<script setup>
-        let { a } = $ref(1)
-        </script>`,
-          { refSugar: true }
-        )
-      ).toThrow(`$ref() bindings cannot be used with destructuring`)
-    })
-
-    test('$computed w/ destructure', () => {
-      expect(() =>
-        compile(
-          `<script setup>
-        const { a } = $computed(() => 1)
-        </script>`,
-          { refSugar: true }
-        )
-      ).toThrow(`$computed() bindings cannot be used with destructuring`)
-    })
-
     test('defineProps/Emit() referencing ref declarations', () => {
       expect(() =>
         compile(
@@ -368,26 +107,5 @@ describe('<script setup> ref sugar', () => {
         )
       ).toThrow(`cannot reference locally declared variables`)
     })
-
-    test('warn usage in non-init positions', () => {
-      expect(() =>
-        compile(
-          `<script setup>
-      let bar = $ref(1)
-      bar = $ref(2)
-    </script>`,
-          { refSugar: true }
-        )
-      ).toThrow(`$ref can only be used directly as a variable initializer`)
-
-      expect(() =>
-        compile(
-          `<script setup>
-      let bar = { foo: $computed(1) }
-    </script>`,
-          { refSugar: true }
-        )
-      ).toThrow(`$computed can only be used directly as a variable initializer`)
-    })
   })
 })
index 58bd4c126bb2338c058e73862d4aa0cc2e99e008..5b8defac589afa78bbc2ea1b2e66d17e323f886a 100644 (file)
@@ -37,6 +37,7 @@
     "@vue/compiler-core": "3.2.4",
     "@vue/compiler-dom": "3.2.4",
     "@vue/compiler-ssr": "3.2.4",
+    "@vue/ref-transform": "3.2.4",
     "@vue/shared": "3.2.4",
     "consolidate": "^0.16.0",
     "estree-walker": "^2.0.2",
index 7857a12594b9b6b3db53e9ead967072d64b81b4f..8356d717883dfeae654cbec0786c77d70c888986 100644 (file)
@@ -10,7 +10,6 @@ import {
   UNREF,
   SimpleExpressionNode,
   isFunctionType,
-  isStaticProperty,
   walkIdentifiers
 } from '@vue/compiler-dom'
 import {
@@ -45,8 +44,7 @@ import {
   RestElement,
   TSInterfaceBody,
   AwaitExpression,
-  VariableDeclarator,
-  VariableDeclaration
+  Program
 } from '@babel/types'
 import { walk } from 'estree-walker'
 import { RawSourceMap } from 'source-map'
@@ -59,6 +57,7 @@ import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate'
 import { warnExperimental, warnOnce } from './warn'
 import { rewriteDefault } from './rewriteDefault'
 import { createCache } from './cache'
+import { transformAST as transformWithRefSugar } from '@vue/ref-transform'
 
 // Special compiler macros
 const DEFINE_PROPS = 'defineProps'
@@ -66,12 +65,6 @@ const DEFINE_EMITS = 'defineEmits'
 const DEFINE_EXPOSE = 'defineExpose'
 const WITH_DEFAULTS = 'withDefaults'
 
-const $REF = `$ref`
-const $SHALLOW_REF = '$shallowRef'
-const $COMPUTED = `$computed`
-const $FROM_REFS = `$fromRefs`
-const $RAW = `$raw`
-
 const isBuiltInDir = makeMap(
   `once,memo,if,else,else-if,slot,text,html,on,bind,model,show,cloak,is`
 )
@@ -144,6 +137,7 @@ export function compileScript(
   let { script, scriptSetup, source, filename } = sfc
   // feature flags
   const enableRefSugar = !!options.refSugar
+  let refBindings: string[] | undefined
   const parseOnly = !!options.parseOnly
 
   if (parseOnly && !scriptSetup) {
@@ -250,8 +244,7 @@ export function compileScript(
   const userImports: Record<string, ImportBinding> = Object.create(null)
   const userImportAlias: Record<string, string> = Object.create(null)
   const setupBindings: Record<string, VariableBinding> = Object.create(null)
-  const refBindings: Record<string, VariableBinding> = Object.create(null)
-  const refIdentifiers: Set<Identifier> = new Set()
+
   let defaultExport: Node | undefined
   let hasDefinePropsCall = false
   let hasDefineEmitCall = false
@@ -293,10 +286,10 @@ export function compileScript(
     input: string,
     options: ParserOptions,
     offset: number
-  ): Statement[] {
+  ): Program {
     try {
       options.errorRecovery = parseOnly
-      return _parse(input, options).program.body
+      return _parse(input, options).program
     } catch (e) {
       e.message = `[@vue/compiler-sfc] ${e.message}\n\n${
         sfc.filename
@@ -476,7 +469,7 @@ export function compileScript(
         }
       }
 
-      for (const node of scriptSetupAst) {
+      for (const node of scriptSetupAst.body) {
         const qualified = isQualifiedType(node)
         if (qualified) {
           return qualified
@@ -531,173 +524,6 @@ export function compileScript(
     )
   }
 
-  function isRefSugarCall(callee: string) {
-    return (
-      callee === $REF ||
-      callee === $COMPUTED ||
-      callee === $FROM_REFS ||
-      callee === $SHALLOW_REF
-    )
-  }
-
-  function processRefSugar(
-    decl: VariableDeclarator,
-    statement: VariableDeclaration
-  ) {
-    if (!isCallOf(decl.init, isRefSugarCall)) {
-      return
-    }
-
-    if (!enableRefSugar) {
-      error(
-        `ref sugar is an experimental proposal and must be explicitly ` +
-          `enabled via @vue/compiler-sfc options.`,
-        // TODO link to RFC details
-        decl.init
-      )
-    } else {
-      warnExperimental(
-        `ref sugar`,
-        `https://github.com/vuejs/rfcs/discussions/369`
-      )
-    }
-
-    const callee = (decl.init.callee as Identifier).name
-    const start = decl.init.start! + startOffset
-    if (callee === $REF || callee === $SHALLOW_REF) {
-      if (statement.kind !== 'let') {
-        error(`${callee}() bindings can only be declared with let.`, decl)
-      }
-      if (decl.id.type !== 'Identifier') {
-        error(
-          `${callee}() bindings cannot be used with destructuring. ` +
-            `If you are trying to destructure from an object of refs, ` +
-            `use \`let { x } = $fromRefs(obj)\`.`,
-          decl.id
-        )
-      }
-      registerRefBinding(decl.id)
-      s.overwrite(
-        start,
-        start + callee.length,
-        helper(callee === $REF ? 'ref' : 'shallowRef')
-      )
-    } else if (callee === $COMPUTED) {
-      if (decl.id.type !== 'Identifier') {
-        error(
-          `${callee}() bindings cannot be used with destructuring.`,
-          decl.id
-        )
-      }
-      registerRefBinding(decl.id)
-      s.overwrite(start, start + $COMPUTED.length, helper('computed'))
-    } else if (callee === $FROM_REFS) {
-      if (!decl.id.type.endsWith('Pattern')) {
-        error(
-          `${callee}() declaration must be used with destructure patterns.`,
-          decl
-        )
-      }
-      if (decl.id.type === 'ObjectPattern') {
-        processRefObjectPattern(decl.id, statement)
-      } else if (decl.id.type === 'ArrayPattern') {
-        processRefArrayPattern(decl.id, statement)
-      }
-      s.remove(start, start + callee.length)
-    }
-  }
-
-  function registerRefBinding(id: Identifier) {
-    if (id.name[0] === '$') {
-      error(`ref variable identifiers cannot start with $.`, id)
-    }
-    refBindings[id.name] = setupBindings[id.name] = {
-      type: BindingTypes.SETUP_REF,
-      rangeNode: id
-    }
-    refIdentifiers.add(id)
-  }
-
-  function processRefObjectPattern(
-    pattern: ObjectPattern,
-    statement: VariableDeclaration
-  ) {
-    for (const p of pattern.properties) {
-      let nameId: Identifier | undefined
-      if (p.type === 'ObjectProperty') {
-        if (p.key.start! === p.value.start!) {
-          // shorthand { foo } --> { foo: __foo }
-          nameId = p.key as Identifier
-          s.appendLeft(nameId.end! + startOffset, `: __${nameId.name}`)
-          if (p.value.type === 'AssignmentPattern') {
-            // { foo = 1 }
-            refIdentifiers.add(p.value.left as Identifier)
-          }
-        } else {
-          if (p.value.type === 'Identifier') {
-            // { foo: bar } --> { foo: __bar }
-            nameId = p.value
-            s.prependRight(nameId.start! + startOffset, `__`)
-          } else if (p.value.type === 'ObjectPattern') {
-            processRefObjectPattern(p.value, statement)
-          } else if (p.value.type === 'ArrayPattern') {
-            processRefArrayPattern(p.value, statement)
-          } else if (p.value.type === 'AssignmentPattern') {
-            // { foo: bar = 1 } --> { foo: __bar = 1 }
-            nameId = p.value.left as Identifier
-            s.prependRight(nameId.start! + startOffset, `__`)
-          }
-        }
-      } else {
-        // rest element { ...foo } --> { ...__foo }
-        nameId = p.argument as Identifier
-        s.prependRight(nameId.start! + startOffset, `__`)
-      }
-      if (nameId) {
-        registerRefBinding(nameId)
-        // append binding declarations after the parent statement
-        s.appendLeft(
-          statement.end! + startOffset,
-          `\nconst ${nameId.name} = ${helper('shallowRef')}(__${nameId.name});`
-        )
-      }
-    }
-  }
-
-  function processRefArrayPattern(
-    pattern: ArrayPattern,
-    statement: VariableDeclaration
-  ) {
-    for (const e of pattern.elements) {
-      if (!e) continue
-      let nameId: Identifier | undefined
-      if (e.type === 'Identifier') {
-        // [a] --> [__a]
-        nameId = e
-      } else if (e.type === 'AssignmentPattern') {
-        // [a = 1] --> [__a = 1]
-        nameId = e.left as Identifier
-      } else if (e.type === 'RestElement') {
-        // [...a] --> [...__a]
-        nameId = e.argument as Identifier
-      } else if (e.type === 'ObjectPattern') {
-        processRefObjectPattern(e, statement)
-      } else if (e.type === 'ArrayPattern') {
-        processRefArrayPattern(e, statement)
-      }
-      if (nameId) {
-        registerRefBinding(nameId)
-        // prefix original
-        s.prependRight(nameId.start! + startOffset, `__`)
-        // append binding declarations after the parent statement
-        s.appendLeft(
-          statement.end! + startOffset,
-          `\nconst ${nameId.name} = ${helper('shallowRef')}(__${nameId.name});`
-        )
-      }
-    }
-  }
-
   function genRuntimeProps(props: Record<string, PropTypeData>) {
     const keys = Object.keys(props)
     if (!keys.length) {
@@ -770,7 +596,7 @@ export function compileScript(
       scriptStartOffset!
     )
 
-    for (const node of scriptAst) {
+    for (const node of scriptAst.body) {
       if (node.type === 'ImportDeclaration') {
         // record imports for dedupe
         for (const specifier of node.specifiers) {
@@ -854,7 +680,7 @@ export function compileScript(
     startOffset
   )
 
-  for (const node of scriptSetupAst) {
+  for (const node of scriptSetupAst.body) {
     const start = node.start! + startOffset
     let end = node.end! + startOffset
     // locate comment
@@ -1005,8 +831,6 @@ export function compileScript(
               s.remove(start, end)
               left--
             }
-          } else {
-            processRefSugar(decl, node)
           }
         }
       }
@@ -1106,54 +930,25 @@ export function compileScript(
     return {
       ...scriptSetup,
       ranges,
-      scriptAst,
-      scriptSetupAst
+      scriptAst: scriptAst?.body,
+      scriptSetupAst: scriptSetupAst?.body
     }
   }
 
-  // 3. Do a full walk to rewrite identifiers referencing let exports with ref
-  // value access
+  // 3. Apply ref sugar transform
   if (enableRefSugar) {
-    const onIdent = (id: Identifier, parent: Node, parentStack: Node[]) => {
-      if (refBindings[id.name] && !refIdentifiers.has(id)) {
-        if (isStaticProperty(parent) && parent.shorthand) {
-          // let binding used in a property shorthand
-          // { foo } -> { foo: foo.value }
-          // skip for destructure patterns
-          if (
-            !(parent as any).inPattern ||
-            isInDestructureAssignment(parent, parentStack)
-          ) {
-            s.appendLeft(id.end! + startOffset, `: ${id.name}.value`)
-          }
-        } else {
-          s.appendLeft(id.end! + startOffset, '.value')
-        }
-      }
-    }
-
-    const onNode = (node: Node, parent: Node) => {
-      if (isCallOf(node, $RAW)) {
-        s.remove(
-          node.callee.start! + startOffset,
-          node.callee.end! + startOffset
-        )
-        return false // skip walk
-      } else if (
-        parent &&
-        isCallOf(node, isRefSugarCall) &&
-        (parent.type !== 'VariableDeclarator' || node !== parent.init)
-      ) {
-        error(
-          // @ts-ignore
-          `${node.callee.name} can only be used directly as a variable initializer.`,
-          node
-        )
-      }
-    }
-
-    for (const node of scriptSetupAst) {
-      walkIdentifiers(node, onIdent, onNode)
+    warnExperimental(
+      `ref sugar`,
+      `https://github.com/vuejs/rfcs/discussions/369`
+    )
+    const { rootVars, importedHelpers } = transformWithRefSugar(
+      scriptSetupAst,
+      s,
+      startOffset
+    )
+    refBindings = rootVars
+    for (const h of importedHelpers) {
+      helperImports.add(h)
     }
   }
 
@@ -1192,7 +987,7 @@ export function compileScript(
 
   // 7. analyze binding metadata
   if (scriptAst) {
-    Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst))
+    Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst.body))
   }
   if (propsRuntimeDecl) {
     for (const key of getObjectOrArrayExpressionKeys(propsRuntimeDecl)) {
@@ -1215,8 +1010,10 @@ export function compileScript(
     bindingMetadata[key] = setupBindings[key].type
   }
   // known ref bindings
-  for (const key in refBindings) {
-    bindingMetadata[key] = BindingTypes.SETUP_REF
+  if (refBindings) {
+    for (const key of refBindings) {
+      bindingMetadata[key] = BindingTypes.SETUP_REF
+    }
   }
 
   // 8. inject `useCssVars` calls
@@ -1437,8 +1234,8 @@ export function compileScript(
       hires: true,
       includeContent: true
     }) as unknown as RawSourceMap,
-    scriptAst,
-    scriptSetupAst
+    scriptAst: scriptAst?.body,
+    scriptSetupAst: scriptSetupAst?.body
   }
 }
 
@@ -1818,27 +1615,6 @@ function canNeverBeRef(node: Node, userReactiveImport: string): boolean {
   }
 }
 
-function isInDestructureAssignment(parent: Node, parentStack: Node[]): boolean {
-  if (
-    parent &&
-    (parent.type === 'ObjectProperty' || parent.type === 'ArrayPattern')
-  ) {
-    let i = parentStack.length
-    while (i--) {
-      const p = parentStack[i]
-      if (p.type === 'AssignmentExpression') {
-        const root = parentStack[0]
-        // if this is a ref: destructure, it should be treated like a
-        // variable decalration!
-        return !(root.type === 'LabeledStatement' && root.label.name === 'ref')
-      } else if (p.type !== 'ObjectProperty' && !p.type.endsWith('Pattern')) {
-        break
-      }
-    }
-  }
-  return false
-}
-
 /**
  * Analyze bindings in normal `<script>`
  * Note that `compileScriptSetup` already analyzes bindings as part of its
index 9d93832607d3bb0d94d3ba13baecf1b43f332341..ae403b741faabed5be380d2112e2cf73c6f7fe51 100644 (file)
@@ -5,6 +5,10 @@ export { compileStyle, compileStyleAsync } from './compileStyle'
 export { compileScript } from './compileScript'
 export { rewriteDefault } from './rewriteDefault'
 export { generateCodeFrame, walkIdentifiers } from '@vue/compiler-core'
+export {
+  transform as transformRef,
+  transformAST as transformRefAST
+} from '@vue/ref-transform'
 
 // Utilities
 export { parse as babelParse } from '@babel/parser'
diff --git a/packages/ref-transform/README.md b/packages/ref-transform/README.md
new file mode 100644 (file)
index 0000000..2a41354
--- /dev/null
@@ -0,0 +1,84 @@
+# @vue/ref-transform
+
+> ⚠️ This is experimental and currently only provided for testing and feedback. It may break during patches or even be removed. Use at your own risk!
+>
+> Follow https://github.com/vuejs/rfcs/discussions/369 for details and updates.
+
+## Basic Rules
+
+- `$()` to turn refs into reative variables
+- `$$()` to access the original refs from reative variables
+
+```js
+import { ref, watch } from 'vue'
+
+// bind ref as a variable
+let count = $(ref(0))
+
+// no need for .value
+console.log(count)
+
+// get the actual ref
+watch($$(count), c => console.log(`count changed to ${c}`))
+
+// assignments are reactive
+count++
+```
+
+### Shorthands
+
+A few commonly used APIs have shorthands (which also removes the need to import them):
+
+- `$(ref(0))` -> `$ref(0)`
+- `$(computed(() => 123))` -> `$computed(() => 123)`
+- `$(shallowRef({}))` -> `$shallowRef({})`
+
+## API
+
+This package is the lower-level transform that can be used standalone. Higher-level tooling (e.g. `@vitejs/plugin-vue` and `vue-loader`) will provide integration via options.
+
+### `transform`
+
+```js
+import { transform } from '@vue/ref-transform'
+
+const src = `let a = $ref(0); a++`
+const {
+  code, // import { ref as _ref } from 'vue'; let a = (ref(0)); a.value++"
+  map
+} = transform(src, {
+  filename: 'foo.ts',
+  sourceMap: true,
+  parserPlugins: ['typescript']
+})
+```
+
+**Options**
+
+```ts
+interface RefTransformOptions {
+  filename?: string
+  sourceMap?: boolean // default: false
+  parserPlugins?: ParserPlugin[]
+  importHelpersFrom?: string // default: "vue"
+}
+```
+
+### `transformAST`
+
+```js
+import { transformAST } from '@vue/ref-transform'
+import { parse } from '@babel/parser'
+import MagicString from 'magic-string'
+
+const src = `let a = $ref(0); a++`
+const ast = parse(src, { sourceType: 'module' })
+const s = new MagicString(src)
+
+const {
+  rootVars, // ['a']
+  importedHelpers // ['ref']
+} = transformAST(ast, s)
+
+console.log(s.toString()) // let a = _ref(0); a.value++
+```
diff --git a/packages/ref-transform/__tests__/__snapshots__/refTransform.spec.ts.snap b/packages/ref-transform/__tests__/__snapshots__/refTransform.spec.ts.snap
new file mode 100644 (file)
index 0000000..54044f4
--- /dev/null
@@ -0,0 +1,211 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`$ unwrapping 1`] = `
+"
+    import { ref, shallowRef } from 'vue'
+    let foo = (ref())
+    let a = (ref(1))
+    let b = (shallowRef({
+      count: 0
+    }))
+    let c = () => {}
+    let d
+    "
+`;
+
+exports[`$$ 1`] = `
+"import { ref as _ref } from 'vue'
+
+    let a = _ref(1)
+    const b = (a)
+    const c = ({ a })
+    callExternal((a))
+    "
+`;
+
+exports[`$computed declaration 1`] = `
+"import { computed as _computed } from 'vue'
+
+    let a = _computed(() => 1)
+    "
+`;
+
+exports[`$ref & $shallowRef declarations 1`] = `
+"import { ref as _ref, shallowRef as _shallowRef } from 'vue'
+
+    let foo = _ref()
+    let a = _ref(1)
+    let b = _shallowRef({
+      count: 0
+    })
+    let c = () => {}
+    let d
+    "
+`;
+
+exports[`accessing ref binding 1`] = `
+"import { ref as _ref } from 'vue'
+
+    let a = _ref(1)
+    console.log(a.value)
+    function get() {
+      return a.value + 1
+    }
+    "
+`;
+
+exports[`array destructure 1`] = `
+"import { ref as _ref } from 'vue'
+
+    let n = _ref(1), [__a, __b = 1, ...__c] = (useFoo())
+const a = _ref(__a);
+const b = _ref(__b);
+const c = _ref(__c);
+    console.log(n.value, a.value, b.value, c.value)
+    "
+`;
+
+exports[`handle TS casting syntax 1`] = `
+"import { ref as _ref } from 'vue'
+
+      let a = _ref(1)
+      console.log(a.value!)
+      console.log(a.value! + 1)
+      console.log(a.value as number)
+      console.log((a.value as number) + 1)
+      console.log(<number>a.value)
+      console.log(<number>a.value + 1)
+      console.log(a.value! + (a.value as number))
+      console.log(a.value! + <number>a.value)
+      console.log((a.value as number) + <number>a.value)
+      "
+`;
+
+exports[`mixing $ref & $computed declarations 1`] = `
+"import { ref as _ref, computed as _computed } from 'vue'
+
+    let a = _ref(1), b = _computed(() => a.value + 1)
+    "
+`;
+
+exports[`multi $ref declarations 1`] = `
+"import { ref as _ref } from 'vue'
+
+    let a = _ref(1), b = _ref(2), c = _ref({
+      count: 0
+    })
+    "
+`;
+
+exports[`mutating ref binding 1`] = `
+"import { ref as _ref } from 'vue'
+
+    let a = _ref(1)
+    let b = _ref({ count: 0 })
+    function inc() {
+      a.value++
+      a.value = a.value + 1
+      b.value.count++
+      b.value.count = b.value.count + 1
+      ;({ a: a.value } = { a: 2 })
+      ;[a.value] = [1]
+    }
+    "
+`;
+
+exports[`nested destructure 1`] = `
+"import { ref as _ref } from 'vue'
+
+    let [{ a: { b: __b }}] = (useFoo())
+const b = _ref(__b);
+    let { c: [__d, __e] } = (useBar())
+const d = _ref(__d);
+const e = _ref(__e);
+    console.log(b.value, d.value, e.value)
+    "
+`;
+
+exports[`nested scopes 1`] = `
+"import { ref as _ref } from 'vue'
+
+    let a = _ref(0)
+    let b = _ref(0)
+    let c = 0
+
+    a.value++ // outer a
+    b.value++ // outer b
+    c++ // outer c
+
+    function foo({ a }) {
+      a++ // inner a
+      b.value++ // inner b
+      let c = _ref(0)
+      c.value++ // inner c
+      let d = _ref(0)
+
+      const bar = (c) => {
+        c++ // nested c
+        d.value++ // nested d
+      }
+
+      if (true) {
+        let a = _ref(0)
+        a.value++ // if block a
+      }
+
+      return ({ a, b, c, d })
+    }
+    "
+`;
+
+exports[`object destructure 1`] = `
+"import { ref as _ref } from 'vue'
+
+    let n = _ref(1), { a: __a, b: __c, d: __d = 1, e: __f = 2, ...__g } = (useFoo())
+const a = _ref(__a);
+const c = _ref(__c);
+const d = _ref(__d);
+const f = _ref(__f);
+const g = _ref(__g);
+    let { foo: __foo } = (useSomthing(() => 1));
+const foo = _ref(__foo);
+    console.log(n.value, a.value, c.value, d.value, f.value, g.value, foo.value)
+    "
+`;
+
+exports[`should not rewrite scope variable 1`] = `
+"import { ref as _ref } from 'vue'
+
+
+      let a = _ref(1)
+      let b = _ref(1)
+      let d = _ref(1)
+      const e = 1
+      function test() {
+        const a = 2
+        console.log(a)
+        console.log(b.value)
+        let c = { c: 3 }
+        console.log(c)
+        console.log(d.value)
+        console.log(e)
+      }
+    "
+`;
+
+exports[`should not rewrite type identifiers 1`] = `
+"import { ref as _ref } from 'vue'
+const props = defineProps<{msg: string; ids?: string[]}>()
+        let ids = _ref([])"
+`;
+
+exports[`using ref binding in property shorthand 1`] = `
+"import { ref as _ref } from 'vue'
+
+    let a = _ref(1)
+    const b = { a: a.value }
+    function test() {
+      const { a } = b
+    }
+    "
+`;
diff --git a/packages/ref-transform/__tests__/refTransform.spec.ts b/packages/ref-transform/__tests__/refTransform.spec.ts
new file mode 100644 (file)
index 0000000..8164f97
--- /dev/null
@@ -0,0 +1,390 @@
+import { parse } from '@babel/parser'
+import { babelParserDefaultPlugins } from '@vue/shared'
+import { transform } from '../src'
+
+function assertCode(code: string) {
+  // parse the generated code to make sure it is valid
+  try {
+    parse(code, {
+      sourceType: 'module',
+      plugins: [...babelParserDefaultPlugins, 'typescript']
+    })
+  } catch (e) {
+    console.log(code)
+    throw e
+  }
+  expect(code).toMatchSnapshot()
+}
+
+test('$ unwrapping', () => {
+  const { code, rootVars } = transform(`
+    import { ref, shallowRef } from 'vue'
+    let foo = $(ref())
+    let a = $(ref(1))
+    let b = $(shallowRef({
+      count: 0
+    }))
+    let c = () => {}
+    let d
+    `)
+  expect(code).not.toMatch(`$(ref())`)
+  expect(code).not.toMatch(`$(ref(1))`)
+  expect(code).not.toMatch(`$(shallowRef({`)
+  expect(code).toMatch(`let foo = (ref())`)
+  expect(code).toMatch(`let a = (ref(1))`)
+  expect(code).toMatch(`
+    let b = (shallowRef({
+      count: 0
+    }))
+    `)
+  // normal declarations left untouched
+  expect(code).toMatch(`let c = () => {}`)
+  expect(code).toMatch(`let d`)
+  expect(rootVars).toStrictEqual(['foo', 'a', 'b'])
+  assertCode(code)
+})
+
+test('$ref & $shallowRef declarations', () => {
+  const { code, rootVars, importedHelpers } = transform(`
+    let foo = $ref()
+    let a = $ref(1)
+    let b = $shallowRef({
+      count: 0
+    })
+    let c = () => {}
+    let d
+    `)
+  expect(code).toMatch(
+    `import { ref as _ref, shallowRef as _shallowRef } from 'vue'`
+  )
+  expect(code).not.toMatch(`$ref()`)
+  expect(code).not.toMatch(`$ref(1)`)
+  expect(code).not.toMatch(`$shallowRef({`)
+  expect(code).toMatch(`let foo = _ref()`)
+  expect(code).toMatch(`let a = _ref(1)`)
+  expect(code).toMatch(`
+    let b = _shallowRef({
+      count: 0
+    })
+    `)
+  // normal declarations left untouched
+  expect(code).toMatch(`let c = () => {}`)
+  expect(code).toMatch(`let d`)
+  expect(rootVars).toStrictEqual(['foo', 'a', 'b'])
+  expect(importedHelpers).toStrictEqual(['ref', 'shallowRef'])
+  assertCode(code)
+})
+
+test('multi $ref declarations', () => {
+  const { code, rootVars, importedHelpers } = transform(`
+    let a = $ref(1), b = $ref(2), c = $ref({
+      count: 0
+    })
+    `)
+  expect(code).toMatch(`
+    let a = _ref(1), b = _ref(2), c = _ref({
+      count: 0
+    })
+    `)
+  expect(rootVars).toStrictEqual(['a', 'b', 'c'])
+  expect(importedHelpers).toStrictEqual(['ref'])
+  assertCode(code)
+})
+
+test('$computed declaration', () => {
+  const { code, rootVars, importedHelpers } = transform(`
+    let a = $computed(() => 1)
+    `)
+  expect(code).toMatch(`
+    let a = _computed(() => 1)
+    `)
+  expect(rootVars).toStrictEqual(['a'])
+  expect(importedHelpers).toStrictEqual(['computed'])
+  assertCode(code)
+})
+
+test('mixing $ref & $computed declarations', () => {
+  const { code, rootVars, importedHelpers } = transform(`
+    let a = $ref(1), b = $computed(() => a + 1)
+    `)
+  expect(code).toMatch(`
+    let a = _ref(1), b = _computed(() => a.value + 1)
+    `)
+  expect(rootVars).toStrictEqual(['a', 'b'])
+  expect(importedHelpers).toStrictEqual(['ref', 'computed'])
+  assertCode(code)
+})
+
+test('accessing ref binding', () => {
+  const { code } = transform(`
+    let a = $ref(1)
+    console.log(a)
+    function get() {
+      return a + 1
+    }
+    `)
+  expect(code).toMatch(`console.log(a.value)`)
+  expect(code).toMatch(`return a.value + 1`)
+  assertCode(code)
+})
+
+test('cases that should not append .value', () => {
+  const { code } = transform(`
+    let a = $ref(1)
+    console.log(b.a)
+    function get(a) {
+      return a + 1
+    }
+    `)
+  expect(code).not.toMatch(`a.value`)
+})
+
+test('mutating ref binding', () => {
+  const { code } = transform(`
+    let a = $ref(1)
+    let b = $ref({ count: 0 })
+    function inc() {
+      a++
+      a = a + 1
+      b.count++
+      b.count = b.count + 1
+      ;({ a } = { a: 2 })
+      ;[a] = [1]
+    }
+    `)
+  expect(code).toMatch(`a.value++`)
+  expect(code).toMatch(`a.value = a.value + 1`)
+  expect(code).toMatch(`b.value.count++`)
+  expect(code).toMatch(`b.value.count = b.value.count + 1`)
+  expect(code).toMatch(`;({ a: a.value } = { a: 2 })`)
+  expect(code).toMatch(`;[a.value] = [1]`)
+  assertCode(code)
+})
+
+test('using ref binding in property shorthand', () => {
+  const { code } = transform(`
+    let a = $ref(1)
+    const b = { a }
+    function test() {
+      const { a } = b
+    }
+    `)
+  expect(code).toMatch(`const b = { a: a.value }`)
+  // should not convert destructure
+  expect(code).toMatch(`const { a } = b`)
+  assertCode(code)
+})
+
+test('should not rewrite scope variable', () => {
+  const { code } = transform(`
+
+      let a = $ref(1)
+      let b = $ref(1)
+      let d = $ref(1)
+      const e = 1
+      function test() {
+        const a = 2
+        console.log(a)
+        console.log(b)
+        let c = { c: 3 }
+        console.log(c)
+        console.log(d)
+        console.log(e)
+      }
+    `)
+  expect(code).toMatch('console.log(a)')
+  expect(code).toMatch('console.log(b.value)')
+  expect(code).toMatch('console.log(c)')
+  expect(code).toMatch('console.log(d.value)')
+  expect(code).toMatch('console.log(e)')
+  assertCode(code)
+})
+
+test('object destructure', () => {
+  const { code, rootVars } = transform(`
+    let n = $ref(1), { a, b: c, d = 1, e: f = 2, ...g } = $(useFoo())
+    let { foo } = $(useSomthing(() => 1));
+    console.log(n, a, c, d, f, g, foo)
+    `)
+  expect(code).toMatch(
+    `let n = _ref(1), { a: __a, b: __c, d: __d = 1, e: __f = 2, ...__g } = (useFoo())`
+  )
+  expect(code).toMatch(`let { foo: __foo } = (useSomthing(() => 1))`)
+  expect(code).toMatch(`\nconst a = _ref(__a);`)
+  expect(code).not.toMatch(`\nconst b = _ref(__b);`)
+  expect(code).toMatch(`\nconst c = _ref(__c);`)
+  expect(code).toMatch(`\nconst d = _ref(__d);`)
+  expect(code).not.toMatch(`\nconst e = _ref(__e);`)
+  expect(code).toMatch(`\nconst f = _ref(__f);`)
+  expect(code).toMatch(`\nconst g = _ref(__g);`)
+  expect(code).toMatch(`\nconst foo = _ref(__foo);`)
+  expect(code).toMatch(
+    `console.log(n.value, a.value, c.value, d.value, f.value, g.value, foo.value)`
+  )
+  expect(rootVars).toStrictEqual(['n', 'a', 'c', 'd', 'f', 'g', 'foo'])
+  assertCode(code)
+})
+
+test('array destructure', () => {
+  const { code, rootVars } = transform(`
+    let n = $ref(1), [a, b = 1, ...c] = $(useFoo())
+    console.log(n, a, b, c)
+    `)
+  expect(code).toMatch(`let n = _ref(1), [__a, __b = 1, ...__c] = (useFoo())`)
+  expect(code).toMatch(`\nconst a = _ref(__a);`)
+  expect(code).toMatch(`\nconst b = _ref(__b);`)
+  expect(code).toMatch(`\nconst c = _ref(__c);`)
+  expect(code).toMatch(`console.log(n.value, a.value, b.value, c.value)`)
+  expect(rootVars).toStrictEqual(['n', 'a', 'b', 'c'])
+  assertCode(code)
+})
+
+test('nested destructure', () => {
+  const { code, rootVars } = transform(`
+    let [{ a: { b }}] = $(useFoo())
+    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 = _ref(__a);`)
+  expect(code).not.toMatch(`\nconst c = _ref(__c);`)
+  expect(code).toMatch(`\nconst b = _ref(__b);`)
+  expect(code).toMatch(`\nconst d = _ref(__d);`)
+  expect(code).toMatch(`\nconst e = _ref(__e);`)
+  expect(rootVars).toStrictEqual(['b', 'd', 'e'])
+  assertCode(code)
+})
+
+test('$$', () => {
+  const { code } = transform(`
+    let a = $ref(1)
+    const b = $$(a)
+    const c = $$({ a })
+    callExternal($$(a))
+    `)
+  expect(code).toMatch(`const b = (a)`)
+  expect(code).toMatch(`const c = ({ a })`)
+  expect(code).toMatch(`callExternal((a))`)
+  assertCode(code)
+})
+
+test('nested scopes', () => {
+  const { code, rootVars } = transform(`
+    let a = $ref(0)
+    let b = $ref(0)
+    let c = 0
+
+    a++ // outer a
+    b++ // outer b
+    c++ // outer c
+
+    function foo({ a }) {
+      a++ // inner a
+      b++ // inner b
+      let c = $ref(0)
+      c++ // inner c
+      let d = $ref(0)
+
+      const bar = (c) => {
+        c++ // nested c
+        d++ // nested d
+      }
+
+      if (true) {
+        let a = $ref(0)
+        a++ // if block a
+      }
+
+      return $$({ a, b, c, d })
+    }
+    `)
+  expect(rootVars).toStrictEqual(['a', 'b'])
+
+  expect(code).toMatch('a.value++ // outer a')
+  expect(code).toMatch('b.value++ // outer b')
+  expect(code).toMatch('c++ // outer c')
+
+  expect(code).toMatch('a++ // inner a') // shadowed by function arg
+  expect(code).toMatch('b.value++ // inner b')
+  expect(code).toMatch('c.value++ // inner c') // shadowed by local ref binding
+
+  expect(code).toMatch('c++ // nested c') // shadowed by inline fn arg
+  expect(code).toMatch(`d.value++ // nested d`)
+
+  expect(code).toMatch(`a.value++ // if block a`) // if block
+
+  expect(code).toMatch(`return ({ a, b, c, d })`)
+  assertCode(code)
+})
+
+//#4062
+test('should not rewrite type identifiers', () => {
+  const { code } = transform(
+    `const props = defineProps<{msg: string; ids?: string[]}>()
+        let ids = $ref([])`,
+    {
+      parserPlugins: ['typescript']
+    }
+  )
+  expect(code).not.toMatch('.value')
+  assertCode(code)
+})
+
+// #4254
+test('handle TS casting syntax', () => {
+  const { code } = transform(
+    `
+      let a = $ref(1)
+      console.log(a!)
+      console.log(a! + 1)
+      console.log(a as number)
+      console.log((a as number) + 1)
+      console.log(<number>a)
+      console.log(<number>a + 1)
+      console.log(a! + (a as number))
+      console.log(a! + <number>a)
+      console.log((a as number) + <number>a)
+      `,
+    {
+      parserPlugins: ['typescript']
+    }
+  )
+  expect(code).toMatch('console.log(a.value!)')
+  expect(code).toMatch('console.log(a.value as number)')
+  expect(code).toMatch('console.log(<number>a.value)')
+  assertCode(code)
+})
+
+describe('errors', () => {
+  test('non-let $ref declaration', () => {
+    expect(() => transform(`const a = $ref(1)`)).toThrow(
+      `$ref() bindings can only be declared with let`
+    )
+  })
+
+  test('$ref w/ destructure', () => {
+    expect(() => transform(`let { a } = $ref(1)`)).toThrow(
+      `cannot be used with destructure`
+    )
+  })
+
+  test('$computed w/ destructure', () => {
+    expect(() => transform(`let { a } = $computed(() => 1)`)).toThrow(
+      `cannot be used with destructure`
+    )
+  })
+
+  test('warn usage in non-init positions', () => {
+    expect(() =>
+      transform(
+        `let bar = $ref(1)
+          bar = $ref(2)`
+      )
+    ).toThrow(`$ref can only be used as the initializer`)
+
+    expect(() => transform(`let bar = { foo: $computed(1) }`)).toThrow(
+      `$computed can only be used as the initializer`
+    )
+  })
+})
diff --git a/packages/ref-transform/api-extractor.json b/packages/ref-transform/api-extractor.json
new file mode 100644 (file)
index 0000000..a8982eb
--- /dev/null
@@ -0,0 +1,7 @@
+{
+  "extends": "../../api-extractor.json",
+  "mainEntryPointFilePath": "./dist/packages/<unscopedPackageName>/src/index.d.ts",
+  "dtsRollup": {
+    "publicTrimmedFilePath": "./dist/<unscopedPackageName>.d.ts"
+  }
+}
\ No newline at end of file
diff --git a/packages/ref-transform/package.json b/packages/ref-transform/package.json
new file mode 100644 (file)
index 0000000..c6c4e1e
--- /dev/null
@@ -0,0 +1,39 @@
+{
+  "name": "@vue/ref-transform",
+  "version": "3.2.4",
+  "description": "@vue/ref-transform",
+  "main": "dist/ref-transform.cjs.js",
+  "files": [
+    "dist"
+  ],
+  "buildOptions": {
+    "formats": [
+      "cjs"
+    ],
+    "prod": false
+  },
+  "types": "dist/ref-transform.d.ts",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/vuejs/vue-next.git",
+    "directory": "packages/ref-transform"
+  },
+  "keywords": [
+    "vue"
+  ],
+  "author": "Evan You",
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/vuejs/vue-next/issues"
+  },
+  "homepage": "https://github.com/vuejs/vue-next/tree/dev/packages/ref-transform#readme",
+  "dependencies": {
+    "@vue/compiler-core": "3.2.4",
+    "@vue/shared": "3.2.4",
+    "@babel/parser": "^7.15.0",
+    "estree-walker": "^2.0.2"
+  },
+  "devDependencies": {
+    "@babel/core": "^7.15.0"
+  }
+}
diff --git a/packages/ref-transform/src/babelPlugin.ts b/packages/ref-transform/src/babelPlugin.ts
new file mode 100644 (file)
index 0000000..757ad9c
--- /dev/null
@@ -0,0 +1,3 @@
+export function plugin() {
+  // TODO
+}
diff --git a/packages/ref-transform/src/index.ts b/packages/ref-transform/src/index.ts
new file mode 100644 (file)
index 0000000..1792fe7
--- /dev/null
@@ -0,0 +1,368 @@
+import {
+  Node,
+  Identifier,
+  VariableDeclarator,
+  BlockStatement,
+  CallExpression,
+  ObjectPattern,
+  VariableDeclaration,
+  ArrayPattern
+} from '@babel/types'
+import MagicString, { SourceMap } from 'magic-string'
+import { walk } from 'estree-walker'
+import {
+  isFunctionType,
+  isInDestructureAssignment,
+  isStaticProperty,
+  walkBlockDeclarations,
+  walkFunctionParams,
+  walkIdentifiers
+} from '@vue/compiler-core'
+import { parse, ParserPlugin } from '@babel/parser'
+import { babelParserDefaultPlugins } from '@vue/shared'
+
+const TO_VAR_SYMBOL = '$'
+const TO_REF_SYMBOL = '$$'
+const shorthands = ['ref', 'computed', 'shallowRef']
+
+export interface ReactiveDeclarator {
+  node: VariableDeclarator
+  statement: VariableDeclaration
+  ids: Identifier[]
+  isPattern: boolean
+  isRoot: boolean
+}
+
+type Scope = Record<string, boolean>
+
+export interface RefTransformOptions {
+  filename?: string
+  sourceMap?: boolean
+  parserPlugins?: ParserPlugin[]
+  importHelpersFrom?: string
+}
+
+export interface RefTransformResults {
+  code: string
+  map: SourceMap | null
+  rootVars: string[]
+  importedHelpers: string[]
+}
+
+export function transform(
+  src: string,
+  {
+    filename,
+    sourceMap,
+    parserPlugins,
+    importHelpersFrom = 'vue'
+  }: RefTransformOptions = {}
+): RefTransformResults {
+  const ast = parse(src, {
+    sourceType: 'module',
+    plugins: [...babelParserDefaultPlugins, ...(parserPlugins || [])]
+  })
+  const s = new MagicString(src)
+  const res = transformAST(ast, s)
+
+  // inject helper imports
+  if (res.importedHelpers.length) {
+    s.prepend(
+      `import { ${res.importedHelpers
+        .map(h => `${h} as _${h}`)
+        .join(', ')} } from '${importHelpersFrom}'\n`
+    )
+  }
+
+  return {
+    ...res,
+    code: s.toString(),
+    map: sourceMap
+      ? s.generateMap({
+          source: filename,
+          hires: true,
+          includeContent: true
+        })
+      : null
+  }
+}
+
+export function transformAST(
+  ast: Node,
+  s: MagicString,
+  offset = 0
+): {
+  rootVars: string[]
+  importedHelpers: string[]
+} {
+  const importedHelpers = new Set<string>()
+  const blockStack: BlockStatement[] = []
+  const rootScope: Scope = {}
+  const blockToScopeMap = new WeakMap<BlockStatement, Scope>()
+  const excludedIds = new Set<Identifier>()
+  const parentStack: Node[] = []
+
+  const error = (msg: string, node: Node) => {
+    const e = new Error(msg)
+    ;(e as any).node = node
+    throw e
+  }
+
+  const helper = (msg: string) => {
+    importedHelpers.add(msg)
+    return `_${msg}`
+  }
+
+  const registerBinding = (id: Identifier, isRef = false) => {
+    excludedIds.add(id)
+    const currentBlock = blockStack[blockStack.length - 1]
+    if (currentBlock) {
+      const currentScope = blockToScopeMap.get(currentBlock)
+      if (!currentScope) {
+        blockToScopeMap.set(currentBlock, { [id.name]: isRef })
+      } else {
+        currentScope[id.name] = isRef
+      }
+    } else {
+      rootScope[id.name] = isRef
+    }
+  }
+
+  const registerRefBinding = (id: Identifier) => registerBinding(id, true)
+
+  // 1st pass: detect macro callsites and register ref bindings
+  ;(walk as any)(ast, {
+    enter(node: Node, parent?: Node) {
+      parent && parentStack.push(parent)
+
+      if (node.type === 'BlockStatement') {
+        blockStack.push(node)
+        walkBlockDeclarations(node, registerBinding)
+        if (parent && isFunctionType(parent)) {
+          walkFunctionParams(parent, registerBinding)
+        }
+        return
+      }
+
+      const toVarCall = isToVarCall(node)
+      if (toVarCall) {
+        if (!parent || parent.type !== 'VariableDeclarator') {
+          return error(
+            `${toVarCall} can only be used as the initializer of ` +
+              `a variable declaration.`,
+            node
+          )
+        }
+        excludedIds.add((node as CallExpression).callee as Identifier)
+
+        const decl = parentStack[parentStack.length - 2] as VariableDeclaration
+        if (decl.kind !== 'let') {
+          error(`${toVarCall}() bindings can only be declared with let`, node)
+        }
+
+        if (toVarCall === TO_VAR_SYMBOL) {
+          // $
+          // remove macro
+          s.remove(
+            (node as CallExpression).callee.start! + offset,
+            (node as CallExpression).callee.end! + offset
+          )
+          if (parent.id.type === 'Identifier') {
+            // single variable
+            registerRefBinding(parent.id)
+          } else if (parent.id.type === 'ObjectPattern') {
+            processRefObjectPattern(parent.id, decl)
+          } else if (parent.id.type === 'ArrayPattern') {
+            processRefArrayPattern(parent.id, decl)
+          }
+        } else {
+          // shorthands
+          if (parent.id.type === 'Identifier') {
+            registerRefBinding(parent.id)
+            // replace call
+            s.overwrite(
+              node.start! + offset,
+              node.start! + toVarCall.length + offset,
+              helper(toVarCall.slice(1))
+            )
+          } else {
+            error(
+              `${toVarCall}() cannot be used with destructure patterns.`,
+              node
+            )
+          }
+        }
+      }
+    },
+    leave(node: Node, parent?: Node) {
+      parent && parentStack.pop()
+      if (node.type === 'BlockStatement') {
+        blockStack.pop()
+      }
+    }
+  })
+
+  function processRefObjectPattern(
+    pattern: ObjectPattern,
+    statement: VariableDeclaration
+  ) {
+    for (const p of pattern.properties) {
+      let nameId: Identifier | undefined
+      if (p.type === 'ObjectProperty') {
+        if (p.key.start! === p.value.start!) {
+          // shorthand { foo } --> { foo: __foo }
+          nameId = p.key as Identifier
+          s.appendLeft(nameId.end! + offset, `: __${nameId.name}`)
+          if (p.value.type === 'AssignmentPattern') {
+            // { foo = 1 }
+            registerRefBinding(p.value.left as Identifier)
+          }
+        } else {
+          if (p.value.type === 'Identifier') {
+            // { foo: bar } --> { foo: __bar }
+            nameId = p.value
+            s.prependRight(nameId.start! + offset, `__`)
+          } else if (p.value.type === 'ObjectPattern') {
+            processRefObjectPattern(p.value, statement)
+          } else if (p.value.type === 'ArrayPattern') {
+            processRefArrayPattern(p.value, statement)
+          } else if (p.value.type === 'AssignmentPattern') {
+            // { foo: bar = 1 } --> { foo: __bar = 1 }
+            nameId = p.value.left as Identifier
+            s.prependRight(nameId.start! + offset, `__`)
+          }
+        }
+      } else {
+        // rest element { ...foo } --> { ...__foo }
+        nameId = p.argument as Identifier
+        s.prependRight(nameId.start! + offset, `__`)
+      }
+      if (nameId) {
+        registerRefBinding(nameId)
+        // append binding declarations after the parent statement
+        s.appendLeft(
+          statement.end! + offset,
+          `\nconst ${nameId.name} = ${helper('ref')}(__${nameId.name});`
+        )
+      }
+    }
+  }
+
+  function processRefArrayPattern(
+    pattern: ArrayPattern,
+    statement: VariableDeclaration
+  ) {
+    for (const e of pattern.elements) {
+      if (!e) continue
+      let nameId: Identifier | undefined
+      if (e.type === 'Identifier') {
+        // [a] --> [__a]
+        nameId = e
+      } else if (e.type === 'AssignmentPattern') {
+        // [a = 1] --> [__a = 1]
+        nameId = e.left as Identifier
+      } else if (e.type === 'RestElement') {
+        // [...a] --> [...__a]
+        nameId = e.argument as Identifier
+      } else if (e.type === 'ObjectPattern') {
+        processRefObjectPattern(e, statement)
+      } else if (e.type === 'ArrayPattern') {
+        processRefArrayPattern(e, statement)
+      }
+      if (nameId) {
+        registerRefBinding(nameId)
+        // prefix original
+        s.prependRight(nameId.start! + offset, `__`)
+        // append binding declarations after the parent statement
+        s.appendLeft(
+          statement.end! + offset,
+          `\nconst ${nameId.name} = ${helper('ref')}(__${nameId.name});`
+        )
+      }
+    }
+  }
+
+  // 2nd pass: detect references to ref bindings and append .value
+  // also remove $$ calls
+  walkIdentifiers(
+    ast,
+    (id, parent, parentStack, isReferenced) => {
+      if (!isReferenced || excludedIds.has(id)) {
+        return false
+      }
+      // locate current scope
+      let i = parentStack.length
+      while (i--) {
+        const node = parentStack[i]
+        if (node.type === 'BlockStatement') {
+          const scope = blockToScopeMap.get(node)
+          if (scope && checkRefId(scope, id, parent, parentStack)) {
+            return
+          }
+        }
+      }
+      checkRefId(rootScope, id, parent, parentStack)
+    },
+    node => {
+      if (isToRefCall(node)) {
+        s.remove(node.callee.start! + offset, node.callee.end! + offset)
+        return false // skip walk
+      }
+    },
+    true, // invoke on ALL
+    false // skip scope analysis since we did it already
+  )
+
+  function checkRefId(
+    scope: Scope,
+    id: Identifier,
+    parent: Node,
+    parentStack: Node[]
+  ): boolean {
+    if (id.name in scope) {
+      if (scope[id.name]) {
+        if (isStaticProperty(parent) && parent.shorthand) {
+          // let binding used in a property shorthand
+          // { foo } -> { foo: foo.value }
+          // skip for destructure patterns
+          if (
+            !(parent as any).inPattern ||
+            isInDestructureAssignment(parent, parentStack)
+          ) {
+            s.appendLeft(id.end! + offset, `: ${id.name}.value`)
+          }
+        } else {
+          s.appendLeft(id.end! + offset, '.value')
+        }
+      }
+      return true
+    }
+    return false
+  }
+
+  return {
+    rootVars: Object.keys(rootScope),
+    importedHelpers: [...importedHelpers]
+  }
+}
+
+function isToVarCall(node: Node): string | false {
+  if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier') {
+    return false
+  }
+  const callee = node.callee.name
+  if (callee === TO_VAR_SYMBOL) {
+    return TO_VAR_SYMBOL
+  }
+  if (callee[0] === TO_VAR_SYMBOL && shorthands.includes(callee.slice(1))) {
+    return callee
+  }
+  return false
+}
+
+function isToRefCall(node: Node): node is CallExpression {
+  return (
+    node.type === 'CallExpression' &&
+    (node.callee as Identifier).name === TO_REF_SYMBOL
+  )
+}
index 10eb7ededd8b8816461ba2e54518331433611350..12754bf3c8b102ead19a22ff97de87699de349fe 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -21,7 +21,7 @@
   resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.15.0.tgz#2dbaf8b85334796cafbb0f5793a90a2fc010b176"
   integrity sha512-0NqAC1IJE0S0+lL1SWFMxMkz1pKCNCjI4tr2Zx4LJSXxCLAdr6KyArnY+sno5m3yH9g737ygOyPABDsnXkpxiA==
 
-"@babel/core@^7.1.0", "@babel/core@^7.7.5":
+"@babel/core@^7.1.0", "@babel/core@^7.15.0", "@babel/core@^7.7.5":
   version "7.15.0"
   resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.15.0.tgz#749e57c68778b73ad8082775561f67f5196aafa8"
   integrity sha512-tXtmTminrze5HEUPn/a0JtOzzfp0nk+UEXQ/tqIJo3WDGypl/2OFQEMll/zSFU8f/lfmfLXvTaORHF3cfXIQMw==