]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler-sfc): new script setup implementation
authorEvan You <yyx990803@gmail.com>
Thu, 29 Oct 2020 19:03:39 +0000 (15:03 -0400)
committerEvan You <yyx990803@gmail.com>
Thu, 29 Oct 2020 19:03:39 +0000 (15:03 -0400)
- now exposes all top level bindings to template
- support `ref:` syntax sugar

packages/compiler-core/src/transforms/transformExpression.ts
packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
packages/compiler-sfc/__tests__/compileScript.spec.ts
packages/compiler-sfc/src/compileScript.ts
packages/compiler-sfc/src/genCssVars.ts

index a671c563f7077d95d2268740414d07c161f693ba..c2f00a4ce274c711cb432c221e498965881c22f7 100644 (file)
@@ -161,9 +161,9 @@ export function processExpression(
         if (!isDuplicate(node)) {
           const needPrefix = shouldPrefix(node, parent)
           if (!knownIds[node.name] && needPrefix) {
-            if (isPropertyShorthand(node, parent)) {
-              // property shorthand like { foo }, we need to add the key since we
-              // rewrite the value
+            if (isStaticProperty(parent) && parent.shorthand) {
+              // property shorthand like { foo }, we need to add the key since
+              // we rewrite the value
               node.prefix = `${node.name}: `
             }
             node.name = prefix(node.name)
@@ -278,46 +278,65 @@ const isStaticProperty = (node: Node): node is ObjectProperty =>
   (node.type === 'ObjectProperty' || node.type === 'ObjectMethod') &&
   !node.computed
 
-const isPropertyShorthand = (node: Node, parent: Node) => {
-  return (
-    isStaticProperty(parent) &&
-    parent.value === node &&
-    parent.key.type === 'Identifier' &&
-    parent.key.name === (node as Identifier).name &&
-    parent.key.start === node.start
-  )
-}
-
 const isStaticPropertyKey = (node: Node, parent: Node) =>
   isStaticProperty(parent) && parent.key === node
 
-function shouldPrefix(identifier: Identifier, parent: Node) {
+function shouldPrefix(id: Identifier, parent: Node) {
+  // declaration id
   if (
-    !(
-      isFunction(parent) &&
-      // not id of a FunctionDeclaration
-      ((parent as any).id === identifier ||
-        // not a params of a function
-        parent.params.includes(identifier))
-    ) &&
-    // not a key of Property
-    !isStaticPropertyKey(identifier, parent) &&
-    // not a property of a MemberExpression
-    !(
-      (parent.type === 'MemberExpression' ||
-        parent.type === 'OptionalMemberExpression') &&
-      parent.property === identifier &&
-      !parent.computed
-    ) &&
-    // not in an Array destructure pattern
-    !(parent.type === 'ArrayPattern') &&
-    // skip whitelisted globals
-    !isGloballyWhitelisted(identifier.name) &&
-    // special case for webpack compilation
-    identifier.name !== `require` &&
-    // is a special keyword but parsed as identifier
-    identifier.name !== `arguments`
+    (parent.type === 'VariableDeclarator' ||
+      parent.type === 'ClassDeclaration') &&
+    parent.id === id
   ) {
-    return true
+    return false
+  }
+
+  if (isFunction(parent)) {
+    // function decalration/expression id
+    if ((parent as any).id === id) {
+      return false
+    }
+    // params list
+    if (parent.params.includes(id)) {
+      return false
+    }
   }
+
+  // property key
+  // this also covers object destructure pattern
+  if (isStaticPropertyKey(id, parent)) {
+    return false
+  }
+
+  // array destructure pattern
+  if (parent.type === 'ArrayPattern') {
+    return false
+  }
+
+  // member expression property
+  if (
+    (parent.type === 'MemberExpression' ||
+      parent.type === 'OptionalMemberExpression') &&
+    parent.property === id &&
+    !parent.computed
+  ) {
+    return false
+  }
+
+  // is a special keyword but parsed as identifier
+  if (id.name === 'arguments') {
+    return false
+  }
+
+  // skip whitelisted globals
+  if (isGloballyWhitelisted(id.name)) {
+    return false
+  }
+
+  // special case for webpack compilation
+  if (id.name === 'require') {
+    return false
+  }
+
+  return true
 }
index a6bb93e7405d362f80e0dd22b7e4bf0191741b86..69937bc9e7998822799c5af3fa4589f49cee47e6 100644 (file)
@@ -1,8 +1,7 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`SFC compile <script setup> <script setup lang="ts"> extract emits 1`] = `
-"import { defineComponent as __define__ } from 'vue'
-import { Slots as __Slots__ } from 'vue'
+"import { Slots, defineComponent } from 'vue'
 declare function __emit__(e: 'foo' | 'bar'): void
       declare function __emit__(e: 'baz', id: number): void
       
@@ -16,15 +15,14 @@ export function setup(_: {}, { emit: myEmit }: {
 return {  }
 }
 
-export default __define__({
+export default __defineComponent__({
   emits: [\\"foo\\", \\"bar\\", \\"baz\\"] as unknown as undefined,
   setup
 })"
 `;
 
 exports[`SFC compile <script setup> <script setup lang="ts"> extract props 1`] = `
-"import { defineComponent as __define__ } from 'vue'
-import { Slots as __Slots__ } from 'vue'
+"import { Slots, defineComponent } from 'vue'
 interface Test {}
 
       type Alias = number[]
@@ -59,7 +57,7 @@ export function setup(myProps: {
 return {  }
 }
 
-export default __define__({
+export default __defineComponent__({
   props: {
     string: { type: String, required: true },
     number: { type: Number, required: true },
@@ -88,19 +86,17 @@ export default __define__({
 `;
 
 exports[`SFC compile <script setup> <script setup lang="ts"> hoist type declarations 1`] = `
-"import { defineComponent as __define__ } from 'vue'
-import { Slots as __Slots__ } from 'vue'
+"import { Slots, defineComponent } from 'vue'
 export interface Foo {}
         type Bar = {}
-        
+      
 export function setup() {
 
-        const a = 1
-      
-return { a }
+        
+return {  }
 }
 
-export default __define__({
+export default __defineComponent__({
   setup
 })"
 `;
@@ -149,7 +145,7 @@ export default __default__"
 `;
 
 exports[`SFC compile <script setup> CSS vars injection w/ <script setup> 1`] = `
-"import { useCssVars as __useCssVars__ } from 'vue'
+"import { useCssVars } from 'vue'
 
 export function setup() {
 const color = 'red'
@@ -166,26 +162,6 @@ exports[`SFC compile <script setup> errors should allow export default referenci
 export function setup() {
 
           
-return { bar }
-}
-
-const __default__ = {
-            props: {
-              foo: {
-                default: () => bar
-              }
-            }
-          }
-        __default__.setup = setup
-export default __default__"
-`;
-
-exports[`SFC compile <script setup> errors should allow export default referencing re-exported binding 1`] = `
-"import { bar } from './bar'
-          
-export function setup() {
-
-          
 return { bar }
 }
 
@@ -205,7 +181,7 @@ exports[`SFC compile <script setup> errors should allow export default referenci
 
           const bar = 1
           
-return {  }
+return { bar }
 }
 
 const __default__ = {
@@ -228,150 +204,185 @@ return {  }
 export default { setup }"
 `;
 
-exports[`SFC compile <script setup> exports export * from './x' 1`] = `
-"import { toRefs as __toRefs__ } from 'vue'
-import * as __export_all_0__ from './x'
-          
+exports[`SFC compile <script setup> imports dedupe between user & helper 1`] = `
+"import { ref } from 'vue'
+  
 export function setup() {
 
-          const y = 1
-          
-return Object.assign(
-  { y },
-  __toRefs__(__export_all_0__)
-)
+  const foo = ref(1)
+  
+return { ref, foo }
 }
 
 export default { setup }"
 `;
 
-exports[`SFC compile <script setup> exports export { x } 1`] = `
-"export function setup() {
+exports[`SFC compile <script setup> imports import dedupe between <script> and <script setup> 1`] = `
+"import { x } from './x'
+        
+export function setup() {
 
-           const x = 1
-           const y = 2
-           
-return { x, y }
+        x()
+        
+return { x }
 }
 
 export default { setup }"
 `;
 
-exports[`SFC compile <script setup> exports export { x } from './x' 1`] = `
-"import { x, y } from './x'
-          
+exports[`SFC compile <script setup> imports should extract comment for import or type declarations 1`] = `
+"import a from 'a' // comment
+  import b from 'b'
+  
 export function setup() {
 
-           
-return { x, y }
+  
+return { a, b }
 }
 
 export default { setup }"
 `;
 
-exports[`SFC compile <script setup> exports export { x as default } 1`] = `
-"import x from './x'
-          
+exports[`SFC compile <script setup> imports should hoist and expose imports 1`] = `
+"import { ref } from 'vue'
 export function setup() {
 
-          const y = 1
-          
-return { y }
+return { ref }
 }
 
-
-const __default__ = x
-__default__.setup = setup
-export default __default__"
+export default { setup }"
 `;
 
-exports[`SFC compile <script setup> exports export { x as default } from './x' 1`] = `
-"import { x as __default__ } from './x'
-import { y } from './x'
-          
+exports[`SFC compile <script setup> ref: syntax sugar accessing ref binding 1`] = `
+"import { ref } from 'vue'
+
 export function setup() {
 
-          
-return { y }
+      const a = ref(1)
+      console.log(a.value)
+      function get() {
+        return a.value + 1
+      }
+      
+return { a, get }
 }
 
-__default__.setup = setup
-export default __default__"
+export default { setup }"
 `;
 
-exports[`SFC compile <script setup> exports export class X() {} 1`] = `
-"export function setup() {
-class X {}
-return { X }
+exports[`SFC compile <script setup> ref: syntax sugar array destructure 1`] = `
+"import { ref } from 'vue'
+
+export function setup() {
+
+      const 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)
+      
+return { n, a, b, c }
 }
 
 export default { setup }"
 `;
 
-exports[`SFC compile <script setup> exports export const { x } = ... (destructuring) 1`] = `
-"export function setup() {
+exports[`SFC compile <script setup> ref: syntax sugar convert ref declarations 1`] = `
+"import { ref } from 'vue'
 
-          const [a = 1, { b } = { b: 123 }, ...c] = useFoo()
-          const { d = 2, _: [e], ...f } = useBar()
-        
-return { a, b, c, d, e, f }
+export function setup() {
+
+      const a = ref(1)
+      const b = ref({
+        count: 0
+      })
+      let c = () => {}
+      let d
+      
+return { a, b, c, d }
 }
 
 export default { setup }"
 `;
 
-exports[`SFC compile <script setup> exports export const x = ... 1`] = `
-"export function setup() {
-const x = 1
-return { x }
+exports[`SFC compile <script setup> ref: syntax sugar multi ref declarations 1`] = `
+"import { ref } from 'vue'
+
+export function setup() {
+
+      const a = ref(1), b = ref(2), c = ref({
+        count: 0
+      })
+      
+return { a, b, c }
 }
 
 export default { setup }"
 `;
 
-exports[`SFC compile <script setup> exports export default from './x' 1`] = `
-"import __default__ from './x'
-          
+exports[`SFC compile <script setup> ref: syntax sugar mutating ref binding 1`] = `
+"import { ref } from 'vue'
+
 export function setup() {
 
-          
-return {  }
+      const a = ref(1)
+      const b = ref({ count: 0 })
+      function inc() {
+        a.value++
+        a.value = a.value + 1
+        b.value.count++
+        b.value.count = b.value.count + 1
+      }
+      
+return { a, b, inc }
 }
 
-__default__.setup = setup
-export default __default__"
+export default { setup }"
 `;
 
-exports[`SFC compile <script setup> exports export default in <script setup> 1`] = `
-"export function setup() {
+exports[`SFC compile <script setup> ref: syntax sugar nested destructure 1`] = `
+"import { ref } from 'vue'
 
-          const y = 1
-          
-return { y }
+export function setup() {
+
+      const [{ a: { b: __b }}] = useFoo()
+const b = ref(__b);
+      const { c: [__d, __e] } = useBar()
+const d = ref(__d);
+const e = ref(__e);
+      console.log(b.value, d.value, e.value)
+      
+return { b, d, e }
 }
 
-const __default__ = {
-            props: ['foo']
-          }
-          __default__.setup = setup
-export default __default__"
+export default { setup }"
 `;
 
-exports[`SFC compile <script setup> exports export function x() {} 1`] = `
-"export function setup() {
-function x(){}
-return { x }
+exports[`SFC compile <script setup> ref: syntax sugar object destructure 1`] = `
+"import { ref } from 'vue'
+
+export function setup() {
+
+      const 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);
+      console.log(n.value, a.value, c.value, d.value, f.value, g.value)
+      
+return { n, a, c, d, f, g }
 }
 
 export default { setup }"
 `;
 
-exports[`SFC compile <script setup> import dedupe between <script> and <script setup> 1`] = `
-"import { x } from './x'
-      
-export function setup() {
+exports[`SFC compile <script setup> ref: syntax sugar should not convert non ref labels 1`] = `
+"export function setup() {
 
-      x()
+      foo: a = 1, b = 2, c = {
+        count: 0
+      }
       
 return {  }
 }
@@ -379,24 +390,34 @@ return {  }
 export default { setup }"
 `;
 
-exports[`SFC compile <script setup> should extract comment for import or type declarations 1`] = `
-"import a from 'a' // comment
-import b from 'b'
+exports[`SFC compile <script setup> ref: syntax sugar using ref binding in property shorthand 1`] = `
+"import { ref } from 'vue'
 
 export function setup() {
 
-
-return {  }
+      const a = ref(1)
+      const b = { a: a.value }
+      function test() {
+        const { a } = b
+      }
+      
+return { a, b, test }
 }
 
 export default { setup }"
 `;
 
-exports[`SFC compile <script setup> should hoist imports 1`] = `
-"import { ref } from 'vue'
+exports[`SFC compile <script setup> should expose top level declarations 1`] = `
+"import { x } from './x'
+      
 export function setup() {
 
-return {  }
+      let a = 1
+      const b = 2
+      function c() {}
+      class d {}
+      
+return { x, a, b, c, d }
 }
 
 export default { setup }"
index 38a385acfcd0c3507538ad6278a37835505184b6..7c17d42d1d2d17a6aa92560406124d09115c4e7d 100644 (file)
@@ -22,199 +22,76 @@ function assertCode(code: string) {
 }
 
 describe('SFC compile <script setup>', () => {
-  test('should hoist imports', () => {
-    assertCode(
-      compile(`<script setup>import { ref } from 'vue'</script>`).content
-    )
-  })
-
-  test('should extract comment for import or type declarations', () => {
-    assertCode(
-      compile(`<script setup>
-import a from 'a' // comment
-import b from 'b'
-</script>`).content
-    )
-  })
-
   test('explicit setup signature', () => {
     assertCode(
       compile(`<script setup="props, { emit }">emit('foo')</script>`).content
     )
   })
 
-  test('import dedupe between <script> and <script setup>', () => {
+  test('should expose top level declarations', () => {
     const { content } = compile(`
-      <script>
-      import { x } from './x'
-      </script>
       <script setup>
       import { x } from './x'
-      x()
+      let a = 1
+      const b = 2
+      function c() {}
+      class d {}
       </script>
       `)
     assertCode(content)
-    expect(content.indexOf(`import { x }`)).toEqual(
-      content.lastIndexOf(`import { x }`)
-    )
+    expect(content).toMatch('return { x, a, b, c, d }')
   })
 
-  describe('exports', () => {
-    test('export const x = ...', () => {
-      const { content, bindings } = compile(
-        `<script setup>export const x = 1</script>`
-      )
-      assertCode(content)
-      expect(bindings).toStrictEqual({
-        x: 'setup'
-      })
-    })
-
-    test('export const { x } = ... (destructuring)', () => {
-      const { content, bindings } = compile(`<script setup>
-          export const [a = 1, { b } = { b: 123 }, ...c] = useFoo()
-          export const { d = 2, _: [e], ...f } = useBar()
-        </script>`)
-      assertCode(content)
-      expect(bindings).toStrictEqual({
-        a: 'setup',
-        b: 'setup',
-        c: 'setup',
-        d: 'setup',
-        e: 'setup',
-        f: 'setup'
-      })
-    })
-
-    test('export function x() {}', () => {
-      const { content, bindings } = compile(
-        `<script setup>export function x(){}</script>`
-      )
-      assertCode(content)
-      expect(bindings).toStrictEqual({
-        x: 'setup'
-      })
-    })
-
-    test('export class X() {}', () => {
-      const { content, bindings } = compile(
-        `<script setup>export class X {}</script>`
-      )
-      assertCode(content)
-      expect(bindings).toStrictEqual({
-        X: 'setup'
-      })
-    })
-
-    test('export { x }', () => {
-      const { content, bindings } = compile(
-        `<script setup>
-           const x = 1
-           const y = 2
-           export { x, y }
-          </script>`
-      )
-      assertCode(content)
-      expect(bindings).toStrictEqual({
-        x: 'setup',
-        y: 'setup'
-      })
-    })
-
-    test(`export { x } from './x'`, () => {
-      const { content, bindings } = compile(
-        `<script setup>
-           export { x, y } from './x'
-          </script>`
-      )
-      assertCode(content)
-      expect(bindings).toStrictEqual({
-        x: 'setup',
-        y: 'setup'
-      })
-    })
-
-    test(`export default from './x'`, () => {
-      const { content, bindings } = compile(
-        `<script setup>
-          export default from './x'
-          </script>`,
-        {
-          babelParserPlugins: ['exportDefaultFrom']
-        }
+  describe('imports', () => {
+    test('should hoist and expose imports', () => {
+      assertCode(
+        compile(`<script setup>import { ref } from 'vue'</script>`).content
       )
-      assertCode(content)
-      expect(bindings).toStrictEqual({})
     })
 
-    test(`export { x as default }`, () => {
-      const { content, bindings } = compile(
-        `<script setup>
-          import x from './x'
-          const y = 1
-          export { x as default, y }
-          </script>`
+    test('should extract comment for import or type declarations', () => {
+      assertCode(
+        compile(`<script setup>
+  import a from 'a' // comment
+  import b from 'b'
+  </script>`).content
       )
-      assertCode(content)
-      expect(bindings).toStrictEqual({
-        y: 'setup'
-      })
     })
 
-    test(`export { x as default } from './x'`, () => {
-      const { content, bindings } = compile(
-        `<script setup>
-          export { x as default, y } from './x'
-          </script>`
-      )
+    test('dedupe between user & helper', () => {
+      const { content } = compile(`<script setup>
+  import { ref } from 'vue'
+  ref: foo = 1
+  </script>`)
       assertCode(content)
-      expect(bindings).toStrictEqual({
-        y: 'setup'
-      })
+      expect(content).toMatch(`import { ref } from 'vue'`)
     })
 
-    test(`export * from './x'`, () => {
-      const { content, bindings } = compile(
-        `<script setup>
-          export * from './x'
-          export const y = 1
-          </script>`
-      )
+    test('import dedupe between <script> and <script setup>', () => {
+      const { content } = compile(`
+        <script>
+        import { x } from './x'
+        </script>
+        <script setup>
+        import { x } from './x'
+        x()
+        </script>
+        `)
       assertCode(content)
-      expect(bindings).toStrictEqual({
-        y: 'setup'
-        // in this case we cannot extract bindings from ./x so it falls back
-        // to runtime proxy dispatching
-      })
-    })
-
-    test('export default in <script setup>', () => {
-      const { content, bindings } = compile(
-        `<script setup>
-          export default {
-            props: ['foo']
-          }
-          export const y = 1
-          </script>`
+      expect(content.indexOf(`import { x }`)).toEqual(
+        content.lastIndexOf(`import { x }`)
       )
-      assertCode(content)
-      expect(bindings).toStrictEqual({
-        foo: 'props',
-        y: 'setup'
-      })
     })
   })
 
   describe('<script setup lang="ts">', () => {
     test('hoist type declarations', () => {
-      const { content, bindings } = compile(`
+      const { content } = compile(`
       <script setup lang="ts">
         export interface Foo {}
         type Bar = {}
-        export const a = 1
       </script>`)
       assertCode(content)
-      expect(bindings).toStrictEqual({ a: 'setup' })
     })
 
     test('extract props', () => {
@@ -333,7 +210,7 @@ import b from 'b'
     test('w/ <script setup>', () => {
       assertCode(
         compile(
-          `<script setup>export const color = 'red'</script>\n` +
+          `<script setup>const color = 'red'</script>\n` +
             `<style vars="{ color }">div{ color: var(--color); }</style>`
         ).content
       )
@@ -356,8 +233,8 @@ import b from 'b'
       assertAwaitDetection(`const a = 1 + (await foo)`)
     })
 
-    test('export', () => {
-      assertAwaitDetection(`export const a = 1 + (await foo)`)
+    test('ref', () => {
+      assertAwaitDetection(`ref: a = 1 + (await foo)`)
     })
 
     test('nested statements', () => {
@@ -366,7 +243,7 @@ import b from 'b'
 
     test('should ignore await inside functions', () => {
       // function declaration
-      assertAwaitDetection(`export async function foo() { await bar }`, false)
+      assertAwaitDetection(`async function foo() { await bar }`, false)
       // function expression
       assertAwaitDetection(`const foo = async () => { await bar }`, false)
       // object method
@@ -379,6 +256,197 @@ import b from 'b'
     })
   })
 
+  describe('ref: syntax sugar', () => {
+    test('convert ref declarations', () => {
+      const { content, bindings } = compile(`<script setup>
+      ref: a = 1
+      ref: b = {
+        count: 0
+      }
+      let c = () => {}
+      let d
+      </script>`)
+      expect(content).toMatch(`import { ref } from 'vue'`)
+      expect(content).not.toMatch(`ref: a`)
+      expect(content).toMatch(`const a = ref(1)`)
+      expect(content).toMatch(`
+      const b = ref({
+        count: 0
+      })
+      `)
+      // normal declarations left untouched
+      expect(content).toMatch(`let c = () => {}`)
+      expect(content).toMatch(`let d`)
+      assertCode(content)
+      expect(bindings).toStrictEqual({
+        a: 'setup',
+        b: 'setup',
+        c: 'setup',
+        d: 'setup'
+      })
+    })
+
+    test('multi ref declarations', () => {
+      const { content, bindings } = compile(`<script setup>
+      ref: a = 1, b = 2, c = {
+        count: 0
+      }
+      </script>`)
+      expect(content).toMatch(`
+      const a = ref(1), b = ref(2), c = ref({
+        count: 0
+      })
+      `)
+      expect(content).toMatch(`return { a, b, c }`)
+      assertCode(content)
+      expect(bindings).toStrictEqual({
+        a: 'setup',
+        b: 'setup',
+        c: 'setup'
+      })
+    })
+
+    test('should not convert non ref labels', () => {
+      const { content } = compile(`<script setup>
+      foo: a = 1, b = 2, c = {
+        count: 0
+      }
+      </script>`)
+      expect(content).toMatch(`foo: a = 1, b = 2`)
+      assertCode(content)
+    })
+
+    test('accessing ref binding', () => {
+      const { content } = compile(`<script setup>
+      ref: a = 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 } = compile(`<script setup>
+      ref: a = 1
+      console.log(b.a)
+      function get(a) {
+        return a + 1
+      }
+      </script>`)
+      expect(content).not.toMatch(`a.value`)
+    })
+
+    test('mutating ref binding', () => {
+      const { content } = compile(`<script setup>
+      ref: a = 1
+      ref: b = { count: 0 }
+      function inc() {
+        a++
+        a = a + 1
+        b.count++
+        b.count = b.count + 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`)
+      assertCode(content)
+    })
+
+    test('using ref binding in property shorthand', () => {
+      const { content } = compile(`<script setup>
+      ref: a = 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('object destructure', () => {
+      const { content, bindings } = compile(`<script setup>
+      ref: n = 1, ({ a, b: c, d = 1, e: f = 2, ...g } = useFoo())
+      console.log(n, a, c, d, f, g)
+      </script>`)
+      expect(content).toMatch(
+        `const n = ref(1), { a: __a, b: __c, d: __d = 1, e: __f = 2, ...__g } = useFoo()`
+      )
+      expect(content).toMatch(`\nconst a = ref(__a);`)
+      expect(content).not.toMatch(`\nconst b = ref(__b);`)
+      expect(content).toMatch(`\nconst c = ref(__c);`)
+      expect(content).toMatch(`\nconst d = ref(__d);`)
+      expect(content).not.toMatch(`\nconst e = ref(__e);`)
+      expect(content).toMatch(`\nconst f = ref(__f);`)
+      expect(content).toMatch(`\nconst g = ref(__g);`)
+      expect(content).toMatch(
+        `console.log(n.value, a.value, c.value, d.value, f.value, g.value)`
+      )
+      expect(content).toMatch(`return { n, a, c, d, f, g }`)
+      expect(bindings).toStrictEqual({
+        n: 'setup',
+        a: 'setup',
+        c: 'setup',
+        d: 'setup',
+        f: 'setup',
+        g: 'setup'
+      })
+      assertCode(content)
+    })
+
+    test('array destructure', () => {
+      const { content, bindings } = compile(`<script setup>
+      ref: n = 1, [a, b = 1, ...c] = useFoo()
+      console.log(n, a, b, c)
+      </script>`)
+      expect(content).toMatch(
+        `const n = ref(1), [__a, __b = 1, ...__c] = useFoo()`
+      )
+      expect(content).toMatch(`\nconst a = ref(__a);`)
+      expect(content).toMatch(`\nconst b = ref(__b);`)
+      expect(content).toMatch(`\nconst c = ref(__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: 'setup',
+        a: 'setup',
+        b: 'setup',
+        c: 'setup'
+      })
+      assertCode(content)
+    })
+
+    test('nested destructure', () => {
+      const { content, bindings } = compile(`<script setup>
+      ref: [{ a: { b }}] = useFoo()
+      ref: ({ c: [d, e] } = useBar())
+      console.log(b, d, e)
+      </script>`)
+      expect(content).toMatch(`const [{ a: { b: __b }}] = useFoo()`)
+      expect(content).toMatch(`const { c: [__d, __e] } = useBar()`)
+      expect(content).not.toMatch(`\nconst a = ref(__a);`)
+      expect(content).not.toMatch(`\nconst c = ref(__c);`)
+      expect(content).toMatch(`\nconst b = ref(__b);`)
+      expect(content).toMatch(`\nconst d = ref(__d);`)
+      expect(content).toMatch(`\nconst e = ref(__e);`)
+      expect(content).toMatch(`return { b, d, e }`)
+      expect(bindings).toStrictEqual({
+        b: 'setup',
+        d: 'setup',
+        e: 'setup'
+      })
+      assertCode(content)
+    })
+  })
+
   describe('errors', () => {
     test('<script> and <script setup> must have same lang', () => {
       expect(() =>
@@ -386,13 +454,27 @@ import b from 'b'
       ).toThrow(`<script> and <script setup> must have the same language type`)
     })
 
-    test('export local as default', () => {
+    test('non-type named exports', () => {
+      expect(() =>
+        compile(`<script setup>
+        export const a = 1
+        </script>`)
+      ).toThrow(`cannot contain non-type named exports`)
+
       expect(() =>
         compile(`<script setup>
           const bar = 1
           export { bar as default }
         </script>`)
-      ).toThrow(`Cannot export locally defined variable as default`)
+      ).toThrow(`cannot contain non-type named exports`)
+    })
+
+    test('ref: non-assignment expressions', () => {
+      expect(() =>
+        compile(`<script setup>
+        ref: a = 1, foo()
+        </script>`)
+      ).toThrow(`ref: statements can only contain assignment expressions`)
     })
 
     test('export default referencing local var', () => {
@@ -410,10 +492,10 @@ import b from 'b'
       ).toThrow(`cannot reference locally declared variables`)
     })
 
-    test('export default referencing exports', () => {
+    test('export default referencing ref declarations', () => {
       expect(() =>
         compile(`<script setup>
-        export const bar = 1
+        ref: bar = 1
         export default {
           props: bar
         }
@@ -440,22 +522,6 @@ import b from 'b'
       assertCode(
         compile(`<script setup>
           import { bar } from './bar'
-          export { bar }
-          export default {
-            props: {
-              foo: {
-                default: () => bar
-              }
-            }
-          }
-        </script>`).content
-      )
-    })
-
-    test('should allow export default referencing re-exported binding', () => {
-      assertCode(
-        compile(`<script setup>
-          export { bar } from './bar'
           export default {
             props: {
               foo: {
@@ -479,29 +545,6 @@ import b from 'b'
       `)
       ).toThrow(`Default export is already declared`)
 
-      expect(() =>
-        compile(`
-      <script>
-      export default {}
-      </script>
-      <script setup>
-      const x = {}
-      export { x as default }
-      </script>
-      `)
-      ).toThrow(`Default export is already declared`)
-
-      expect(() =>
-        compile(`
-      <script>
-      export default {}
-      </script>
-      <script setup>
-      export { x as default } from './y'
-      </script>
-      `)
-      ).toThrow(`Default export is already declared`)
-
       expect(() =>
         compile(`
       <script>
index 93cc6cc4383788b832fc3cf78856bf476051bf0a..cf40389b2e8f64fdb714e711219b561888e814e8 100644 (file)
@@ -20,7 +20,9 @@ import {
   TSDeclareFunction,
   ObjectProperty,
   ArrayExpression,
-  Statement
+  Statement,
+  Expression,
+  LabeledStatement
 } from '@babel/types'
 import { walk } from 'estree-walker'
 import { RawSourceMap } from 'source-map'
@@ -31,6 +33,7 @@ export interface SFCScriptCompileOptions {
    * https://babeljs.io/docs/en/babel-parser#plugins
    */
   babelParserPlugins?: ParserPlugin[]
+  refSugar?: boolean
 }
 
 let hasWarned = false
@@ -102,38 +105,159 @@ export function compileScript(
   }
 
   const defaultTempVar = `__default__`
-  const bindings: BindingMetadata = {}
-  const imports: Record<string, string> = {}
-  const setupScopeVars: Record<string, boolean> = {}
-  const setupExports: Record<string, boolean> = {}
-  let exportAllIndex = 0
+  const bindingMetadata: BindingMetadata = {}
+  const helperImports: Set<string> = new Set()
+  const userImports: Record<string, string> = Object.create(null)
+  const setupBindings: Record<string, boolean> = Object.create(null)
+  const refBindings: Record<string, boolean> = Object.create(null)
+  const refIdentifiers: Set<Identifier> = new Set()
+  const enableRefSugar = options.refSugar !== false
   let defaultExport: Node | undefined
   let needDefaultExportRefCheck = false
   let hasAwait = false
 
-  const checkDuplicateDefaultExport = (node: Node) => {
-    if (defaultExport) {
-      // <script> already has export default
-      throw new Error(
-        `Default export is already declared in normal <script>.\n\n` +
-          generateCodeFrame(
-            source,
-            node.start! + startOffset,
-            node.start! + startOffset + `export default`.length
-          )
-      )
-    }
-  }
-
   const s = new MagicString(source)
   const startOffset = scriptSetup.loc.start.offset
   const endOffset = scriptSetup.loc.end.offset
   const scriptStartOffset = script && script.loc.start.offset
   const scriptEndOffset = script && script.loc.end.offset
 
-  let scriptAst
+  function error(
+    msg: string,
+    node: Node,
+    end: number = node.end! + startOffset
+  ) {
+    throw new Error(
+      msg + `\n\n` + generateCodeFrame(source, node.start! + startOffset, end)
+    )
+  }
+
+  function processRefExpression(exp: Expression, statement: LabeledStatement) {
+    if (exp.type === 'AssignmentExpression') {
+      helperImports.add('ref')
+      const { left, right } = exp
+      if (left.type === 'Identifier') {
+        if (left.name[0] === '$') {
+          error(`ref variable identifiers cannot start with $.`, left)
+        }
+        refBindings[left.name] = setupBindings[left.name] = true
+        refIdentifiers.add(left)
+        s.prependRight(right.start! + startOffset, `ref(`)
+        s.appendLeft(right.end! + startOffset, ')')
+      } else if (left.type === 'ObjectPattern') {
+        // remove wrapping parens
+        for (let i = left.start!; i > 0; i--) {
+          const char = source[i + startOffset]
+          if (char === '(') {
+            s.remove(i + startOffset, i + startOffset + 1)
+            break
+          }
+        }
+        for (let i = left.end!; i > 0; i++) {
+          const char = source[i + startOffset]
+          if (char === ')') {
+            s.remove(i + startOffset, i + startOffset + 1)
+            break
+          }
+        }
+        processRefObjectPattern(left, statement)
+      } else if (left.type === 'ArrayPattern') {
+        processRefArrayPattern(left, statement)
+      }
+    } else if (exp.type === 'SequenceExpression') {
+      // possible multiple declarations
+      // ref: x = 1, y = 2
+      exp.expressions.forEach(e => processRefExpression(e, statement))
+    } else {
+      error(`ref: statements can only contain assignment expressions.`, exp)
+    }
+  }
+
+  function processRefObjectPattern(
+    pattern: ObjectPattern,
+    statement: LabeledStatement
+  ) {
+    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) {
+        // register binding
+        refBindings[nameId.name] = setupBindings[nameId.name] = true
+        refIdentifiers.add(nameId)
+        // append binding declarations after the parent statement
+        s.appendLeft(
+          statement.end! + startOffset,
+          `\nconst ${nameId.name} = ref(__${nameId.name});`
+        )
+      }
+    }
+  }
+
+  function processRefArrayPattern(
+    pattern: ArrayPattern,
+    statement: LabeledStatement
+  ) {
+    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) {
+        s.prependRight(nameId.start! + startOffset, `__`)
+        // register binding
+        refBindings[nameId.name] = setupBindings[nameId.name] = true
+        refIdentifiers.add(nameId)
+        // append binding declarations after the parent statement
+        s.appendLeft(
+          statement.end! + startOffset,
+          `\nconst ${nameId.name} = ref(__${nameId.name});`
+        )
+      }
+    }
+  }
 
   // 1. process normal <script> first if it exists
+  let scriptAst
   if (script) {
     // import dedupe between <script> and <script setup>
     scriptAst = parse(script.content, {
@@ -147,7 +271,7 @@ export function compileScript(
         for (const {
           local: { name }
         } of node.specifiers) {
-          imports[name] = node.source.value
+          userImports[name] = node.source.value
         }
       } else if (node.type === 'ExportDefaultDeclaration') {
         // export default
@@ -280,6 +404,21 @@ export function compileScript(
       end++
     }
 
+    // process `ref: x` bindings (convert to refs)
+    if (
+      enableRefSugar &&
+      node.type === 'LabeledStatement' &&
+      node.label.name === 'ref' &&
+      node.body.type === 'ExpressionStatement'
+    ) {
+      s.overwrite(
+        node.label.start! + startOffset,
+        node.body.start! + startOffset,
+        'const '
+      )
+      processRefExpression(node.body.expression, node)
+    }
+
     if (node.type === 'ImportDeclaration') {
       // import declarations are moved to top
       s.move(start, end, 0)
@@ -287,7 +426,7 @@ export function compileScript(
       let prev
       let removed = 0
       for (const specifier of node.specifiers) {
-        if (imports[specifier.local.name]) {
+        if (userImports[specifier.local.name]) {
           // already imported in <script setup>, dedupe
           removed++
           s.remove(
@@ -295,7 +434,7 @@ export function compileScript(
             specifier.end! + startOffset
           )
         } else {
-          imports[specifier.local.name] = node.source.value
+          userImports[specifier.local.name] = node.source.value
         }
         prev = specifier
       }
@@ -305,106 +444,23 @@ export function compileScript(
     }
 
     if (node.type === 'ExportNamedDeclaration' && node.exportKind !== 'type') {
-      // named exports
-      if (node.declaration) {
-        // variable/function/class declarations.
-        // remove leading `export ` keyword
-        s.remove(start, start + 7)
-        walkDeclaration(node.declaration, setupExports)
-      }
-      if (node.specifiers.length) {
-        // named export with specifiers
-        if (node.source) {
-          // export { x } from './x'
-          // change it to import and move to top
-          s.overwrite(start, start + 6, 'import')
-          s.move(start, end, 0)
-        } else {
-          // export { x }
-          s.remove(start, end)
-        }
-        for (const specifier of node.specifiers) {
-          if (specifier.type === 'ExportDefaultSpecifier') {
-            // export default from './x'
-            // rewrite to `import __default__ from './x'`
-            checkDuplicateDefaultExport(node)
-            defaultExport = node
-            s.overwrite(
-              specifier.exported.start! + startOffset,
-              specifier.exported.start! + startOffset + 7,
-              defaultTempVar
-            )
-          } else if (
-            specifier.type === 'ExportSpecifier' &&
-            specifier.exported.type === 'Identifier'
-          ) {
-            if (specifier.exported.name === 'default') {
-              checkDuplicateDefaultExport(node)
-              defaultExport = node
-              // 1. remove specifier
-              if (node.specifiers.length > 1) {
-                // removing the default specifier from a list of specifiers.
-                // look ahead until we reach the first non , or whitespace char.
-                let end = specifier.end! + startOffset
-                while (end < source.length) {
-                  if (/[^,\s]/.test(source.charAt(end))) {
-                    break
-                  }
-                  end++
-                }
-                s.remove(specifier.start! + startOffset, end)
-              } else {
-                s.remove(node.start! + startOffset!, node.end! + startOffset!)
-              }
-              if (!node.source) {
-                // export { x as default, ... }
-                const local = specifier.local.name
-                if (setupScopeVars[local] || setupExports[local]) {
-                  throw new Error(
-                    `Cannot export locally defined variable as default in <script setup>.\n` +
-                      `Default export must be an object literal with no reference to local scope.\n` +
-                      generateCodeFrame(
-                        source,
-                        specifier.start! + startOffset,
-                        specifier.end! + startOffset
-                      )
-                  )
-                }
-                // rewrite to `const __default__ = x` and move to end
-                s.append(`\nconst ${defaultTempVar} = ${local}\n`)
-              } else {
-                // export { x as default } from './x'
-                // rewrite to `import { x as __default__ } from './x'` and
-                // add to top
-                s.prepend(
-                  `import { ${
-                    specifier.local.name
-                  } as ${defaultTempVar} } from '${node.source.value}'\n`
-                )
-              }
-            } else {
-              setupExports[specifier.exported.name] = true
-              if (node.source) {
-                imports[specifier.exported.name] = node.source.value
-              }
-            }
-          }
-        }
-      }
+      // TODO warn
+      error(`<script setup> cannot contain non-type named exports.`, node)
     }
 
     if (node.type === 'ExportAllDeclaration') {
-      // export * from './x'
-      s.overwrite(
-        start,
-        node.source.start! + startOffset,
-        `import * as __export_all_${exportAllIndex++}__ from `
-      )
-      s.move(start, end, 0)
+      // TODO warn
     }
 
     if (node.type === 'ExportDefaultDeclaration') {
-      checkDuplicateDefaultExport(node)
+      if (defaultExport) {
+        // <script> already has export default
+        error(
+          `Default export is already declared in normal <script>.`,
+          node,
+          node.start! + startOffset + `export default`.length
+        )
+      }
       // export default {} inside <script setup>
       // this should be kept in module scope - move it to the end
       s.move(start, end, source.length)
@@ -421,7 +477,7 @@ export function compileScript(
         node.type === 'ClassDeclaration') &&
       !node.declare
     ) {
-      walkDeclaration(node, setupScopeVars)
+      walkDeclaration(node, setupBindings)
     }
 
     // Type declarations
@@ -483,9 +539,6 @@ export function compileScript(
     // await
     if (
       node.type === 'VariableDeclaration' ||
-      (node.type === 'ExportNamedDeclaration' &&
-        node.declaration &&
-        node.declaration.type === 'VariableDeclaration') ||
       node.type.endsWith('Statement')
     ) {
       ;(walk as any)(node, {
@@ -501,20 +554,50 @@ export function compileScript(
     }
   }
 
-  // 4. check default export to make sure it doesn't reference setup scope
+  // 4. Do a full walk to rewrite identifiers referencing let exports with ref
+  // value access
+  if (enableRefSugar && Object.keys(refBindings).length) {
+    for (const node of scriptSetupAst) {
+      if (node.type !== 'ImportDeclaration') {
+        walkIdentifiers(node, (id, parent) => {
+          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) {
+                s.appendLeft(id.end! + startOffset, `: ${id.name}.value`)
+              }
+            } else {
+              s.appendLeft(id.end! + startOffset, '.value')
+            }
+          } else if (id.name[0] === '$' && refBindings[id.name.slice(1)]) {
+            // $xxx raw ref access variables, remove the $ prefix
+            s.remove(id.start! + startOffset, id.start! + startOffset + 1)
+          }
+        })
+      }
+    }
+  }
+
+  // 5. check default export to make sure it doesn't reference setup scope
   // variables
   if (needDefaultExportRefCheck) {
-    checkDefaultExport(
-      defaultExport!,
-      setupScopeVars,
-      imports,
-      setupExports,
-      source,
-      startOffset
-    )
+    walkIdentifiers(defaultExport!, id => {
+      if (setupBindings[id.name]) {
+        error(
+          `\`export default\` in <script setup> cannot reference locally ` +
+            `declared variables because it will be hoisted outside of the ` +
+            `setup() function. If your component options requires initialization ` +
+            `in the module scope, use a separate normal <script> to export ` +
+            `the options instead.`,
+          id
+        )
+      }
+    })
   }
 
-  // 5. remove non-script content
+  // 6. remove non-script content
   if (script) {
     if (startOffset < scriptStartOffset!) {
       // <script setup> before <script>
@@ -532,11 +615,11 @@ export function compileScript(
     s.remove(endOffset, source.length)
   }
 
-  // 5. finalize setup argument signature.
+  // 7. finalize setup argument signature.
   let args = ``
   if (isTS) {
     if (slotsType === '__Slots__') {
-      s.prepend(`import { Slots as __Slots__ } from 'vue'\n`)
+      helperImports.add('Slots')
     }
     const ctxType = `{
   emit: ${emitType},
@@ -560,7 +643,7 @@ export function compileScript(
     args = hasExplicitSignature ? (setupValue as string) : ``
   }
 
-  // 6. wrap setup code with function.
+  // 8. wrap setup code with function.
   // export the content of <script setup> as a named export, `setup`.
   // this allows `import { setup } from '*.vue'` for testing purposes.
   s.prependLeft(
@@ -569,27 +652,18 @@ export function compileScript(
   )
 
   // generate return statement
-  let returned = `{ ${Object.keys(setupExports).join(', ')} }`
-
-  // handle `export * from`. We need to call `toRefs` on the imported module
-  // object before merging.
-  if (exportAllIndex > 0) {
-    s.prepend(`import { toRefs as __toRefs__ } from 'vue'\n`)
-    for (let i = 0; i < exportAllIndex; i++) {
-      returned += `,\n  __toRefs__(__export_all_${i}__)`
-    }
-    returned = `Object.assign(\n  ${returned}\n)`
-  }
+  const exposedBindings = { ...userImports, ...setupBindings }
+  let returned = `{ ${Object.keys(exposedBindings).join(', ')} }`
 
   // inject `useCssVars` calls
   if (hasCssVars) {
-    s.prepend(`import { useCssVars as __useCssVars__ } from 'vue'\n`)
+    helperImports.add(`useCssVars`)
     for (const style of styles) {
       const vars = style.attrs.vars
       if (typeof vars === 'string') {
         s.prependRight(
           endOffset,
-          `\n${genCssVarsCode(vars, !!style.scoped, setupExports)}`
+          `\n${genCssVarsCode(vars, !!style.scoped, exposedBindings)}`
         )
       }
     }
@@ -597,18 +671,18 @@ export function compileScript(
 
   s.appendRight(endOffset, `\nreturn ${returned}\n}\n\n`)
 
-  // 7. finalize default export
+  // 9. finalize default export
   if (isTS) {
     // for TS, make sure the exported type is still valid type with
     // correct props information
-    s.prepend(`import { defineComponent as __define__ } from 'vue'\n`)
+    helperImports.add(`defineComponent`)
     // we have to use object spread for types to be merged properly
     // user's TS setting should compile it down to proper targets
     const def = defaultExport ? `\n  ...${defaultTempVar},` : ``
     const runtimeProps = genRuntimeProps(typeDeclaredProps)
     const runtimeEmits = genRuntimeEmits(typeDeclaredEmits)
     s.append(
-      `export default __define__({${def}${runtimeProps}${runtimeEmits}\n  setup\n})`
+      `export default __defineComponent__({${def}${runtimeProps}${runtimeEmits}\n  setup\n})`
     )
   } else {
     if (defaultExport) {
@@ -620,22 +694,28 @@ export function compileScript(
     }
   }
 
-  // 8. expose bindings for template compiler optimization
+  // 10. finalize Vue helper imports
+  const helpers = [...helperImports].filter(i => userImports[i] !== 'vue')
+  if (helpers.length) {
+    s.prepend(`import { ${helpers.join(', ')} } from 'vue'\n`)
+  }
+
+  // 11. expose bindings for template compiler optimization
   if (scriptAst) {
-    Object.assign(bindings, analyzeScriptBindings(scriptAst))
+    Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst))
   }
-  Object.keys(setupExports).forEach(key => {
-    bindings[key] = 'setup'
+  Object.keys(exposedBindings).forEach(key => {
+    bindingMetadata[key] = 'setup'
   })
   Object.keys(typeDeclaredProps).forEach(key => {
-    bindings[key] = 'props'
+    bindingMetadata[key] = 'props'
   })
-  Object.assign(bindings, analyzeScriptBindings(scriptSetupAst))
+  Object.assign(bindingMetadata, analyzeScriptBindings(scriptSetupAst))
 
   s.trim()
   return {
     ...scriptSetup,
-    bindings,
+    bindings: bindingMetadata,
     content: s.toString(),
     map: (s.generateMap({
       source: filename,
@@ -903,38 +983,21 @@ function genRuntimeEmits(emits: Set<string>) {
 }
 
 /**
- * export default {} inside `<script setup>` cannot access variables declared
- * inside since it's hoisted. Walk and check to make sure.
+ * Walk an AST and find identifiers that are variable references.
+ * This is largely the same logic with `transformExpressions` in compiler-core
+ * but with some subtle differences as this needs to handle a wider range of
+ * possible syntax.
  */
-function checkDefaultExport(
+function walkIdentifiers(
   root: Node,
-  scopeVars: Record<string, boolean>,
-  imports: Record<string, string>,
-  exports: Record<string, boolean>,
-  source: string,
-  offset: number
+  onIdentifier: (node: Identifier, parent: Node) => void
 ) {
   const knownIds: Record<string, number> = Object.create(null)
   ;(walk as any)(root, {
     enter(node: Node & { scopeIds?: Set<string> }, parent: Node) {
       if (node.type === 'Identifier') {
-        if (
-          !knownIds[node.name] &&
-          !isStaticPropertyKey(node, parent) &&
-          (scopeVars[node.name] || (!imports[node.name] && exports[node.name]))
-        ) {
-          throw new Error(
-            `\`export default\` in <script setup> cannot reference locally ` +
-              `declared variables because it will be hoisted outside of the ` +
-              `setup() function. If your component options requires initialization ` +
-              `in the module scope, use a separate normal <script> to export ` +
-              `the options instead.\n\n` +
-              generateCodeFrame(
-                source,
-                node.start! + offset,
-                node.end! + offset
-              )
-          )
+        if (!knownIds[node.name] && isRefIdentifier(node, parent)) {
+          onIdentifier(node, parent)
         }
       } else if (isFunction(node)) {
         // walk function expressions and add its arguments to known identifiers
@@ -968,6 +1031,12 @@ function checkDefaultExport(
             }
           })
         )
+      } else if (
+        node.type === 'ObjectProperty' &&
+        parent.type === 'ObjectPattern'
+      ) {
+        // mark property in destructure pattern
+        ;(node as any).inPattern = true
       }
     },
     leave(node: Node & { scopeIds?: Set<string> }) {
@@ -983,15 +1052,64 @@ function checkDefaultExport(
   })
 }
 
-function isStaticPropertyKey(node: Node, parent: Node): boolean {
-  return (
-    parent &&
-    (parent.type === 'ObjectProperty' || parent.type === 'ObjectMethod') &&
-    !parent.computed &&
-    parent.key === node
-  )
+function isRefIdentifier(id: Identifier, parent: Node) {
+  // declaration id
+  if (
+    (parent.type === 'VariableDeclarator' ||
+      parent.type === 'ClassDeclaration') &&
+    parent.id === id
+  ) {
+    return false
+  }
+
+  if (isFunction(parent)) {
+    // function decalration/expression id
+    if ((parent as any).id === id) {
+      return false
+    }
+    // params list
+    if (parent.params.includes(id)) {
+      return false
+    }
+  }
+
+  // property key
+  // this also covers object destructure pattern
+  if (isStaticPropertyKey(id, parent)) {
+    return false
+  }
+
+  // array destructure pattern
+  if (parent.type === 'ArrayPattern') {
+    return false
+  }
+
+  // member expression property
+  if (
+    (parent.type === 'MemberExpression' ||
+      parent.type === 'OptionalMemberExpression') &&
+    parent.property === id &&
+    !parent.computed
+  ) {
+    return false
+  }
+
+  // is a special keyword but parsed as identifier
+  if (id.name === 'arguments') {
+    return false
+  }
+
+  return true
 }
 
+const isStaticProperty = (node: Node): node is ObjectProperty =>
+  node &&
+  (node.type === 'ObjectProperty' || node.type === 'ObjectMethod') &&
+  !node.computed
+
+const isStaticPropertyKey = (node: Node, parent: Node) =>
+  isStaticProperty(parent) && parent.key === node
+
 function isFunction(node: Node): node is FunctionNode {
   return /Function(?:Expression|Declaration)$|Method$/.test(node.type)
 }
index a53608890a76e06d0ea793690811f98a292413d9..d18afe606bca7581834770ccb264e7274174afb9 100644 (file)
@@ -13,7 +13,7 @@ import { ParserPlugin } from '@babel/parser'
 export function genCssVarsCode(
   varsExp: string,
   scoped: boolean,
-  knownBindings?: Record<string, boolean>
+  knownBindings?: Record<string, string | boolean>
 ) {
   const exp = createSimpleExpression(varsExp, false)
   const context = createTransformContext(createRoot([]), {