]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler-sfc): `<script setup>` defineProps destructure transform (#4690)
authorEvan You <yyx990803@gmail.com>
Mon, 27 Sep 2021 18:24:21 +0000 (14:24 -0400)
committerGitHub <noreply@github.com>
Mon, 27 Sep 2021 18:24:21 +0000 (14:24 -0400)
14 files changed:
packages/compiler-core/src/options.ts
packages/compiler-core/src/transforms/transformExpression.ts
packages/compiler-sfc/__tests__/__snapshots__/compileScriptPropsTransform.spec.ts.snap [new file with mode: 0644]
packages/compiler-sfc/__tests__/compileScriptPropsTransform.spec.ts [new file with mode: 0644]
packages/compiler-sfc/src/compileScript.ts
packages/ref-transform/README.md
packages/ref-transform/__tests__/refTransform.spec.ts
packages/ref-transform/src/refTransform.ts
packages/runtime-core/__tests__/apiSetupHelpers.spec.ts
packages/runtime-core/src/apiSetupHelpers.ts
packages/runtime-core/src/index.ts
packages/sfc-playground/package.json
packages/sfc-playground/src/App.vue
yarn.lock

index 78ffd4e16250a687a2879374198494d676390b08..a0658b4d75ddc781bbcc4463b20cef8e792fbac3 100644 (file)
@@ -82,6 +82,11 @@ export const enum BindingTypes {
    * declared as a prop
    */
   PROPS = 'props',
+  /**
+   * a local alias of a `<script setup>` destructured prop.
+   * the original is stored in __propsAliases of the bindingMetadata object.
+   */
+  PROPS_ALIASED = 'props-aliased',
   /**
    * a let binding (may or may not be a ref)
    */
@@ -110,6 +115,7 @@ export type BindingMetadata = {
   [key: string]: BindingTypes | undefined
 } & {
   __isScriptSetup?: boolean
+  __propsAliases?: Record<string, string>
 }
 
 interface SharedTransformCodegenOptions {
index 96284d179bf8f68fc3216360b7a9df13bb80cd59..bf4d568e51037a00d3541769c4c1bf14e1bc1a20 100644 (file)
@@ -188,11 +188,16 @@ export function processExpression(
         // use __props which is generated by compileScript so in ts mode
         // it gets correct type
         return `__props.${raw}`
+      } else if (type === BindingTypes.PROPS_ALIASED) {
+        // prop with a different local alias (from defineProps() destructure)
+        return `__props.${bindingMetadata.__propsAliases![raw]}`
       }
     } else {
       if (type && type.startsWith('setup')) {
         // setup bindings in non-inline mode
         return `$setup.${raw}`
+      } else if (type === BindingTypes.PROPS_ALIASED) {
+        return `$props.${bindingMetadata.__propsAliases![raw]}`
       } else if (type) {
         return `$${type}.${raw}`
       }
diff --git a/packages/compiler-sfc/__tests__/__snapshots__/compileScriptPropsTransform.spec.ts.snap b/packages/compiler-sfc/__tests__/__snapshots__/compileScriptPropsTransform.spec.ts.snap
new file mode 100644 (file)
index 0000000..28c5798
--- /dev/null
@@ -0,0 +1,128 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`sfc props transform aliasing 1`] = `
+"import { toDisplayString as _toDisplayString } from \\"vue\\"
+
+
+export default {
+  props: ['foo'],
+  setup(__props) {
+
+      
+      let x = foo
+      let y = __props.foo
+      
+return (_ctx, _cache) => {
+  return _toDisplayString(__props.foo + __props.foo)
+}
+}
+
+}"
+`;
+
+exports[`sfc props transform basic usage 1`] = `
+"import { toDisplayString as _toDisplayString } from \\"vue\\"
+
+
+export default {
+  props: ['foo'],
+  setup(__props) {
+
+      
+      console.log(__props.foo)
+      
+return (_ctx, _cache) => {
+  return _toDisplayString(__props.foo)
+}
+}
+
+}"
+`;
+
+exports[`sfc props transform default values w/ runtime declaration 1`] = `
+"import { mergeDefaults as _mergeDefaults } from 'vue'
+
+export default {
+  props: _mergeDefaults(['foo', 'bar'], {
+  foo: 1,
+  bar: () => {}
+}),
+  setup(__props) {
+
+      
+      
+return () => {}
+}
+
+}"
+`;
+
+exports[`sfc props transform default values w/ type declaration 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+
+export default /*#__PURE__*/_defineComponent({
+  props: {
+    foo: { type: Number, required: false, default: 1 },
+    bar: { type: Object, required: false, default: () => {} }
+  },
+  setup(__props: any) {
+
+      
+      
+return () => {}
+}
+
+})"
+`;
+
+exports[`sfc props transform default values w/ type declaration, prod mode 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+
+export default /*#__PURE__*/_defineComponent({
+  props: {
+    foo: { default: 1 },
+    bar: { default: () => {} },
+    baz: null
+  },
+  setup(__props: any) {
+
+      
+      
+return () => {}
+}
+
+})"
+`;
+
+exports[`sfc props transform nested scope 1`] = `
+"export default {
+  props: ['foo', 'bar'],
+  setup(__props) {
+
+      
+      function test(foo) {
+        console.log(foo)
+        console.log(__props.bar)
+      }
+      
+return () => {}
+}
+
+}"
+`;
+
+exports[`sfc props transform rest spread 1`] = `
+"import { createPropsRestProxy as _createPropsRestProxy } from 'vue'
+
+export default {
+  props: ['foo', 'bar', 'baz'],
+  setup(__props) {
+
+const rest = _createPropsRestProxy(__props, [\\"foo\\",\\"bar\\"])
+      
+      
+return () => {}
+}
+
+}"
+`;
diff --git a/packages/compiler-sfc/__tests__/compileScriptPropsTransform.spec.ts b/packages/compiler-sfc/__tests__/compileScriptPropsTransform.spec.ts
new file mode 100644 (file)
index 0000000..f388e50
--- /dev/null
@@ -0,0 +1,191 @@
+import { BindingTypes } from '@vue/compiler-core'
+import { SFCScriptCompileOptions } from '../src'
+import { compileSFCScript, assertCode } from './utils'
+
+describe('sfc props transform', () => {
+  function compile(src: string, options?: Partial<SFCScriptCompileOptions>) {
+    return compileSFCScript(src, {
+      inlineTemplate: true,
+      propsDestructureTransform: true,
+      ...options
+    })
+  }
+
+  test('basic usage', () => {
+    const { content, bindings } = compile(`
+      <script setup>
+      const { foo } = defineProps(['foo'])
+      console.log(foo)
+      </script>
+      <template>{{ foo }}</template>
+    `)
+    expect(content).not.toMatch(`const { foo } =`)
+    expect(content).toMatch(`console.log(__props.foo)`)
+    expect(content).toMatch(`_toDisplayString(__props.foo)`)
+    assertCode(content)
+    expect(bindings).toStrictEqual({
+      foo: BindingTypes.PROPS
+    })
+  })
+
+  test('nested scope', () => {
+    const { content, bindings } = compile(`
+      <script setup>
+      const { foo, bar } = defineProps(['foo', 'bar'])
+      function test(foo) {
+        console.log(foo)
+        console.log(bar)
+      }
+      </script>
+    `)
+    expect(content).not.toMatch(`const { foo, bar } =`)
+    expect(content).toMatch(`console.log(foo)`)
+    expect(content).toMatch(`console.log(__props.bar)`)
+    assertCode(content)
+    expect(bindings).toStrictEqual({
+      foo: BindingTypes.PROPS,
+      bar: BindingTypes.PROPS,
+      test: BindingTypes.SETUP_CONST
+    })
+  })
+
+  test('default values w/ runtime declaration', () => {
+    const { content } = compile(`
+      <script setup>
+      const { foo = 1, bar = {} } = defineProps(['foo', 'bar'])
+      </script>
+    `)
+    // literals can be used as-is, non-literals are always returned from a
+    // function
+    expect(content).toMatch(`props: _mergeDefaults(['foo', 'bar'], {
+  foo: 1,
+  bar: () => {}
+})`)
+    assertCode(content)
+  })
+
+  test('default values w/ type declaration', () => {
+    const { content } = compile(`
+      <script setup lang="ts">
+      const { foo = 1, bar = {} } = defineProps<{ foo?: number, bar?: object }>()
+      </script>
+    `)
+    // literals can be used as-is, non-literals are always returned from a
+    // function
+    expect(content).toMatch(`props: {
+    foo: { type: Number, required: false, default: 1 },
+    bar: { type: Object, required: false, default: () => {} }
+  }`)
+    assertCode(content)
+  })
+
+  test('default values w/ type declaration, prod mode', () => {
+    const { content } = compile(
+      `
+      <script setup lang="ts">
+      const { foo = 1, bar = {} } = defineProps<{ foo?: number, bar?: object, baz?: any }>()
+      </script>
+    `,
+      { isProd: true }
+    )
+    // literals can be used as-is, non-literals are always returned from a
+    // function
+    expect(content).toMatch(`props: {
+    foo: { default: 1 },
+    bar: { default: () => {} },
+    baz: null
+  }`)
+    assertCode(content)
+  })
+
+  test('aliasing', () => {
+    const { content, bindings } = compile(`
+      <script setup>
+      const { foo: bar } = defineProps(['foo'])
+      let x = foo
+      let y = bar
+      </script>
+      <template>{{ foo + bar }}</template>
+    `)
+    expect(content).not.toMatch(`const { foo: bar } =`)
+    expect(content).toMatch(`let x = foo`) // should not process
+    expect(content).toMatch(`let y = __props.foo`)
+    // should convert bar to __props.foo in template expressions
+    expect(content).toMatch(`_toDisplayString(__props.foo + __props.foo)`)
+    assertCode(content)
+    expect(bindings).toStrictEqual({
+      x: BindingTypes.SETUP_LET,
+      y: BindingTypes.SETUP_LET,
+      foo: BindingTypes.PROPS,
+      bar: BindingTypes.PROPS_ALIASED,
+      __propsAliases: {
+        bar: 'foo'
+      }
+    })
+  })
+
+  test('rest spread', () => {
+    const { content, bindings } = compile(`
+      <script setup>
+      const { foo, bar, ...rest } = defineProps(['foo', 'bar', 'baz'])
+      </script>
+    `)
+    expect(content).toMatch(
+      `const rest = _createPropsRestProxy(__props, ["foo","bar"])`
+    )
+    assertCode(content)
+    expect(bindings).toStrictEqual({
+      foo: BindingTypes.PROPS,
+      bar: BindingTypes.PROPS,
+      baz: BindingTypes.PROPS,
+      rest: BindingTypes.SETUP_CONST
+    })
+  })
+
+  describe('errors', () => {
+    test('should error on deep destructure', () => {
+      expect(() =>
+        compile(
+          `<script setup>const { foo: [bar] } = defineProps(['foo'])</script>`
+        )
+      ).toThrow(`destructure does not support nested patterns`)
+
+      expect(() =>
+        compile(
+          `<script setup>const { foo: { bar } } = defineProps(['foo'])</script>`
+        )
+      ).toThrow(`destructure does not support nested patterns`)
+    })
+
+    test('should error on computed key', () => {
+      expect(() =>
+        compile(
+          `<script setup>const { [foo]: bar } = defineProps(['foo'])</script>`
+        )
+      ).toThrow(`destructure cannot use computed key`)
+    })
+
+    test('should error when used with withDefaults', () => {
+      expect(() =>
+        compile(
+          `<script setup lang="ts">
+          const { foo } = withDefaults(defineProps<{ foo: string }>(), { foo: 'foo' })
+          </script>`
+        )
+      ).toThrow(`withDefaults() is unnecessary when using destructure`)
+    })
+
+    test('should error if destructure reference local vars', () => {
+      expect(() =>
+        compile(
+          `<script setup>
+          const x = 1
+          const {
+            foo = () => x
+          } = defineProps(['foo'])
+          </script>`
+        )
+      ).toThrow(`cannot reference locally declared variables`)
+    })
+  })
+})
index 0dac40b2340fdaff27bf72a0bd671344c291d8b0..94d13f6471a14ae6e1aa1a02c3fdacd8b2f0af43 100644 (file)
@@ -39,7 +39,9 @@ import {
   TSInterfaceBody,
   AwaitExpression,
   Program,
-  ObjectMethod
+  ObjectMethod,
+  LVal,
+  Expression
 } from '@babel/types'
 import { walk } from 'estree-walker'
 import { RawSourceMap } from 'source-map'
@@ -88,9 +90,15 @@ export interface SFCScriptCompileOptions {
   /**
    * (Experimental) Enable syntax transform for using refs without `.value`
    * https://github.com/vuejs/rfcs/discussions/369
-   * @default true
+   * @default false
    */
   refTransform?: boolean
+  /**
+   * (Experimental) Enable syntax transform for destructuring from defineProps()
+   * https://github.com/vuejs/rfcs/discussions/394
+   * @default false
+   */
+  propsDestructureTransform?: boolean
   /**
    * @deprecated use `refTransform` instead.
    */
@@ -131,6 +139,8 @@ export function compileScript(
   let { script, scriptSetup, source, filename } = sfc
   // feature flags
   const enableRefTransform = !!options.refSugar || !!options.refTransform
+  const enablePropsTransform = !!options.propsDestructureTransform
+  const isProd = !!options.isProd
   const genSourceMap = options.sourceMap !== false
   let refBindings: string[] | undefined
 
@@ -203,7 +213,7 @@ export function compileScript(
           cssVars,
           bindings,
           scopeId,
-          !!options.isProd
+          isProd
         )
         content += `\nexport default __default__`
       }
@@ -248,6 +258,8 @@ export function compileScript(
   let hasDefineExposeCall = false
   let propsRuntimeDecl: Node | undefined
   let propsRuntimeDefaults: ObjectExpression | undefined
+  let propsDestructureDecl: Node | undefined
+  let propsDestructureRestId: string | undefined
   let propsTypeDecl: TSTypeLiteral | TSInterfaceBody | undefined
   let propsTypeDeclRaw: Node | undefined
   let propsIdentifier: string | undefined
@@ -266,6 +278,14 @@ export function compileScript(
   const typeDeclaredEmits: Set<string> = new Set()
   // record declared types for runtime props type generation
   const declaredTypes: Record<string, string[]> = {}
+  // props destructure data
+  const propsDestructuredBindings: Record<
+    string, // public prop key
+    {
+      local: string // local identifier, may be different
+      default?: Expression
+    }
+  > = Object.create(null)
 
   // magic-string state
   const s = new MagicString(source)
@@ -337,7 +357,7 @@ export function compileScript(
     }
   }
 
-  function processDefineProps(node: Node): boolean {
+  function processDefineProps(node: Node, declId?: LVal): boolean {
     if (!isCallOf(node, DEFINE_PROPS)) {
       return false
     }
@@ -374,14 +394,62 @@ export function compileScript(
       }
     }
 
+    if (declId) {
+      if (enablePropsTransform && declId.type === 'ObjectPattern') {
+        propsDestructureDecl = declId
+        // props destructure - handle compilation sugar
+        for (const prop of declId.properties) {
+          if (prop.type === 'ObjectProperty') {
+            if (prop.computed) {
+              error(
+                `${DEFINE_PROPS}() destructure cannot use computed key.`,
+                prop.key
+              )
+            }
+            const propKey = (prop.key as Identifier).name
+            if (prop.value.type === 'AssignmentPattern') {
+              // default value { foo = 123 }
+              const { left, right } = prop.value
+              if (left.type !== 'Identifier') {
+                error(
+                  `${DEFINE_PROPS}() destructure does not support nested patterns.`,
+                  left
+                )
+              }
+              // store default value
+              propsDestructuredBindings[propKey] = {
+                local: left.name,
+                default: right
+              }
+            } else if (prop.value.type === 'Identifier') {
+              // simple destucture
+              propsDestructuredBindings[propKey] = {
+                local: prop.value.name
+              }
+            } else {
+              error(
+                `${DEFINE_PROPS}() destructure does not support nested patterns.`,
+                prop.value
+              )
+            }
+          } else {
+            // rest spread
+            propsDestructureRestId = (prop.argument as Identifier).name
+          }
+        }
+      } else {
+        propsIdentifier = scriptSetup!.content.slice(declId.start!, declId.end!)
+      }
+    }
+
     return true
   }
 
-  function processWithDefaults(node: Node): boolean {
+  function processWithDefaults(node: Node, declId?: LVal): boolean {
     if (!isCallOf(node, WITH_DEFAULTS)) {
       return false
     }
-    if (processDefineProps(node.arguments[0])) {
+    if (processDefineProps(node.arguments[0], declId)) {
       if (propsRuntimeDecl) {
         error(
           `${WITH_DEFAULTS} can only be used with type-based ` +
@@ -389,6 +457,13 @@ export function compileScript(
           node
         )
       }
+      if (propsDestructureDecl) {
+        error(
+          `${WITH_DEFAULTS}() is unnecessary when using destructure with ${DEFINE_PROPS}().\n` +
+            `Prefer using destructure default values, e.g. const { foo = 1 } = defineProps(...).`,
+          node.callee
+        )
+      }
       propsRuntimeDefaults = node.arguments[1] as ObjectExpression
       if (
         !propsRuntimeDefaults ||
@@ -408,7 +483,7 @@ export function compileScript(
     return true
   }
 
-  function processDefineEmits(node: Node): boolean {
+  function processDefineEmits(node: Node, declId?: LVal): boolean {
     if (!isCallOf(node, DEFINE_EMITS)) {
       return false
     }
@@ -440,6 +515,11 @@ export function compileScript(
         )
       }
     }
+
+    if (declId) {
+      emitIdentifier = scriptSetup!.content.slice(declId.start!, declId.end!)
+    }
+
     return true
   }
 
@@ -565,7 +645,7 @@ export function compileScript(
    * static properties, we can directly generate more optimzied default
    * declarations. Otherwise we will have to fallback to runtime merging.
    */
-  function checkStaticDefaults() {
+  function hasStaticWithDefaults() {
     return (
       propsRuntimeDefaults &&
       propsRuntimeDefaults.type === 'ObjectExpression' &&
@@ -582,13 +662,16 @@ export function compileScript(
     if (!keys.length) {
       return ``
     }
-    const hasStaticDefaults = checkStaticDefaults()
+    const hasStaticDefaults = hasStaticWithDefaults()
     const scriptSetupSource = scriptSetup!.content
     let propsDecls = `{
     ${keys
       .map(key => {
         let defaultString: string | undefined
-        if (hasStaticDefaults) {
+        const destructured = genDestructuredDefaultValue(key)
+        if (destructured) {
+          defaultString = `default: ${destructured}`
+        } else if (hasStaticDefaults) {
           const prop = propsRuntimeDefaults!.properties.find(
             (node: any) => node.key.name === key
           ) as ObjectProperty | ObjectMethod
@@ -608,7 +691,7 @@ export function compileScript(
           }
         }
 
-        if (__DEV__) {
+        if (!isProd) {
           const { type, required } = props[key]
           return `${key}: { type: ${toRuntimeTypeString(
             type
@@ -632,9 +715,21 @@ export function compileScript(
     return `\n  props: ${propsDecls},`
   }
 
+  function genDestructuredDefaultValue(key: string): string | undefined {
+    const destructured = propsDestructuredBindings[key]
+    if (destructured && destructured.default) {
+      const value = scriptSetup!.content.slice(
+        destructured.default.start!,
+        destructured.default.end!
+      )
+      const isLiteral = destructured.default.type.endsWith('Literal')
+      return isLiteral ? value : `() => ${value}`
+    }
+  }
+
   function genSetupPropsType(node: TSTypeLiteral | TSInterfaceBody) {
     const scriptSetupSource = scriptSetup!.content
-    if (checkStaticDefaults()) {
+    if (hasStaticWithDefaults()) {
       // if withDefaults() is used, we need to remove the optional flags
       // on props that have default values
       let res = `{ `
@@ -754,7 +849,7 @@ export function compileScript(
 
     // apply ref transform
     if (enableRefTransform && shouldTransformRef(script.content)) {
-      const { rootVars, importedHelpers } = transformRefAST(
+      const { rootRefs: rootVars, importedHelpers } = transformRefAST(
         scriptAst,
         s,
         scriptStartOffset!
@@ -900,20 +995,9 @@ export function compileScript(
         if (decl.init) {
           // defineProps / defineEmits
           const isDefineProps =
-            processDefineProps(decl.init) || processWithDefaults(decl.init)
-          if (isDefineProps) {
-            propsIdentifier = scriptSetup.content.slice(
-              decl.id.start!,
-              decl.id.end!
-            )
-          }
-          const isDefineEmits = processDefineEmits(decl.init)
-          if (isDefineEmits) {
-            emitIdentifier = scriptSetup.content.slice(
-              decl.id.start!,
-              decl.id.end!
-            )
-          }
+            processDefineProps(decl.init, decl.id) ||
+            processWithDefaults(decl.init, decl.id)
+          const isDefineEmits = processDefineEmits(decl.init, decl.id)
           if (isDefineProps || isDefineEmits) {
             if (left === 1) {
               s.remove(node.start! + startOffset, node.end! + startOffset)
@@ -1004,14 +1088,19 @@ export function compileScript(
   }
 
   // 3. Apply ref sugar transform
-  if (enableRefTransform && shouldTransformRef(scriptSetup.content)) {
-    const { rootVars, importedHelpers } = transformRefAST(
+  if (
+    (enableRefTransform && shouldTransformRef(scriptSetup.content)) ||
+    propsDestructureDecl
+  ) {
+    const { rootRefs, importedHelpers } = transformRefAST(
       scriptSetupAst,
       s,
       startOffset,
-      refBindings
+      refBindings,
+      propsDestructuredBindings,
+      !enableRefTransform
     )
-    refBindings = refBindings ? [...refBindings, ...rootVars] : rootVars
+    refBindings = refBindings ? [...refBindings, ...rootRefs] : rootRefs
     for (const h of importedHelpers) {
       helperImports.add(h)
     }
@@ -1019,7 +1108,7 @@ export function compileScript(
 
   // 4. extract runtime props/emits code from setup context type
   if (propsTypeDecl) {
-    extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes)
+    extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes, isProd)
   }
   if (emitsTypeDecl) {
     extractRuntimeEmits(emitsTypeDecl, typeDeclaredEmits)
@@ -1029,6 +1118,7 @@ export function compileScript(
   // variables
   checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS)
   checkInvalidScopeReference(propsRuntimeDefaults, DEFINE_PROPS)
+  checkInvalidScopeReference(propsDestructureDecl, DEFINE_PROPS)
   checkInvalidScopeReference(emitsRuntimeDecl, DEFINE_PROPS)
 
   // 6. remove non-script content
@@ -1062,6 +1152,20 @@ export function compileScript(
   for (const key in typeDeclaredProps) {
     bindingMetadata[key] = BindingTypes.PROPS
   }
+  // props aliases
+  if (propsDestructureDecl) {
+    if (propsDestructureRestId) {
+      bindingMetadata[propsDestructureRestId] = BindingTypes.SETUP_CONST
+    }
+    for (const key in propsDestructuredBindings) {
+      const { local } = propsDestructuredBindings[key]
+      if (local !== key) {
+        bindingMetadata[local] = BindingTypes.PROPS_ALIASED
+        ;(bindingMetadata.__propsAliases ||
+          (bindingMetadata.__propsAliases = {}))[local] = key
+      }
+    }
+  }
   for (const [key, { isType, imported, source }] of Object.entries(
     userImports
   )) {
@@ -1090,12 +1194,7 @@ export function compileScript(
     helperImports.add('unref')
     s.prependRight(
       startOffset,
-      `\n${genCssVarsCode(
-        cssVars,
-        bindingMetadata,
-        scopeId,
-        !!options.isProd
-      )}\n`
+      `\n${genCssVarsCode(cssVars, bindingMetadata, scopeId, isProd)}\n`
     )
   }
 
@@ -1118,6 +1217,14 @@ export function compileScript(
       }`
     )
   }
+  if (propsDestructureRestId) {
+    s.prependRight(
+      startOffset,
+      `\nconst ${propsDestructureRestId} = ${helper(
+        `createPropsRestProxy`
+      )}(__props, ${JSON.stringify(Object.keys(propsDestructuredBindings))})`
+    )
+  }
   // inject temp variables for async context preservation
   if (hasAwait) {
     const any = isTS ? `: any` : ``
@@ -1235,9 +1342,22 @@ export function compileScript(
     runtimeOptions += `\n  __ssrInlineRender: true,`
   }
   if (propsRuntimeDecl) {
-    runtimeOptions += `\n  props: ${scriptSetup.content
+    let declCode = scriptSetup.content
       .slice(propsRuntimeDecl.start!, propsRuntimeDecl.end!)
-      .trim()},`
+      .trim()
+    if (propsDestructureDecl) {
+      const defaults: string[] = []
+      for (const key in propsDestructuredBindings) {
+        const d = genDestructuredDefaultValue(key)
+        if (d) defaults.push(`${key}: ${d}`)
+      }
+      if (defaults.length) {
+        declCode = `${helper(
+          `mergeDefaults`
+        )}(${declCode}, {\n  ${defaults.join(',\n  ')}\n})`
+      }
+    }
+    runtimeOptions += `\n  props: ${declCode},`
   } else if (propsTypeDecl) {
     runtimeOptions += genRuntimeProps(typeDeclaredProps)
   }
@@ -1313,6 +1433,7 @@ export function compileScript(
   }
 
   s.trim()
+
   return {
     ...scriptSetup,
     bindings: bindingMetadata,
@@ -1376,10 +1497,16 @@ function walkDeclaration(
           bindingType = BindingTypes.SETUP_LET
         }
         registerBinding(bindings, id, bindingType)
-      } else if (id.type === 'ObjectPattern') {
-        walkObjectPattern(id, bindings, isConst, isDefineCall)
-      } else if (id.type === 'ArrayPattern') {
-        walkArrayPattern(id, bindings, isConst, isDefineCall)
+      } else {
+        if (isCallOf(init, DEFINE_PROPS)) {
+          // skip walking props destructure
+          return
+        }
+        if (id.type === 'ObjectPattern') {
+          walkObjectPattern(id, bindings, isConst, isDefineCall)
+        } else if (id.type === 'ArrayPattern') {
+          walkArrayPattern(id, bindings, isConst, isDefineCall)
+        }
       }
     }
   } else if (
@@ -1488,7 +1615,8 @@ function recordType(node: Node, declaredTypes: Record<string, string[]>) {
 function extractRuntimeProps(
   node: TSTypeLiteral | TSInterfaceBody,
   props: Record<string, PropTypeData>,
-  declaredTypes: Record<string, string[]>
+  declaredTypes: Record<string, string[]>,
+  isProd: boolean
 ) {
   const members = node.type === 'TSTypeLiteral' ? node.members : node.body
   for (const m of members) {
@@ -1497,7 +1625,7 @@ function extractRuntimeProps(
       m.key.type === 'Identifier'
     ) {
       let type
-      if (__DEV__) {
+      if (!isProd) {
         if (m.type === 'TSMethodSignature') {
           type = ['Function']
         } else if (m.typeAnnotation) {
index e5ba22fb2bf75464657b36b404f1db1ae70654e3..7de8d6d21463da4537cb3642b72bd0c0e3a86114 100644 (file)
@@ -64,7 +64,9 @@ const {
   // @babel/parser plugins to enable.
   // 'typescript' and 'jsx' will be auto-inferred from filename if provided,
   // so in most cases explicit parserPlugins are not necessary
-  parserPlugins: [/* ... */]
+  parserPlugins: [
+    /* ... */
+  ]
 })
 ```
 
@@ -93,7 +95,7 @@ const ast = parse(src, { sourceType: 'module' })
 const s = new MagicString(src)
 
 const {
-  rootVars, // ['a']
+  rootRefs, // ['a']
   importedHelpers // ['ref']
 } = transformAST(ast, s)
 
index 1b9f3003bfbe58997f1a7b3eb6164851c49566a4..37f55bb6ee99aae91ec925de55a3ba87e2060028 100644 (file)
@@ -17,7 +17,7 @@ function assertCode(code: string) {
 }
 
 test('$ unwrapping', () => {
-  const { code, rootVars } = transform(`
+  const { code, rootRefs } = transform(`
     import { ref, shallowRef } from 'vue'
     let foo = $(ref())
     let a = $(ref(1))
@@ -40,12 +40,12 @@ test('$ unwrapping', () => {
   // normal declarations left untouched
   expect(code).toMatch(`let c = () => {}`)
   expect(code).toMatch(`let d`)
-  expect(rootVars).toStrictEqual(['foo', 'a', 'b'])
+  expect(rootRefs).toStrictEqual(['foo', 'a', 'b'])
   assertCode(code)
 })
 
 test('$ref & $shallowRef declarations', () => {
-  const { code, rootVars, importedHelpers } = transform(`
+  const { code, rootRefs, importedHelpers } = transform(`
     let foo = $ref()
     let a = $ref(1)
     let b = $shallowRef({
@@ -70,13 +70,13 @@ test('$ref & $shallowRef declarations', () => {
   // normal declarations left untouched
   expect(code).toMatch(`let c = () => {}`)
   expect(code).toMatch(`let d`)
-  expect(rootVars).toStrictEqual(['foo', 'a', 'b'])
+  expect(rootRefs).toStrictEqual(['foo', 'a', 'b'])
   expect(importedHelpers).toStrictEqual(['ref', 'shallowRef'])
   assertCode(code)
 })
 
 test('multi $ref declarations', () => {
-  const { code, rootVars, importedHelpers } = transform(`
+  const { code, rootRefs, importedHelpers } = transform(`
     let a = $ref(1), b = $ref(2), c = $ref({
       count: 0
     })
@@ -86,31 +86,31 @@ test('multi $ref declarations', () => {
       count: 0
     })
     `)
-  expect(rootVars).toStrictEqual(['a', 'b', 'c'])
+  expect(rootRefs).toStrictEqual(['a', 'b', 'c'])
   expect(importedHelpers).toStrictEqual(['ref'])
   assertCode(code)
 })
 
 test('$computed declaration', () => {
-  const { code, rootVars, importedHelpers } = transform(`
+  const { code, rootRefs, importedHelpers } = transform(`
     let a = $computed(() => 1)
     `)
   expect(code).toMatch(`
     let a = _computed(() => 1)
     `)
-  expect(rootVars).toStrictEqual(['a'])
+  expect(rootRefs).toStrictEqual(['a'])
   expect(importedHelpers).toStrictEqual(['computed'])
   assertCode(code)
 })
 
 test('mixing $ref & $computed declarations', () => {
-  const { code, rootVars, importedHelpers } = transform(`
+  const { code, rootRefs, 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(rootRefs).toStrictEqual(['a', 'b'])
   expect(importedHelpers).toStrictEqual(['ref', 'computed'])
   assertCode(code)
 })
@@ -201,7 +201,7 @@ test('should not rewrite scope variable', () => {
 })
 
 test('object destructure', () => {
-  const { code, rootVars } = transform(`
+  const { code, rootRefs } = 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)
@@ -221,12 +221,12 @@ test('object destructure', () => {
   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'])
+  expect(rootRefs).toStrictEqual(['n', 'a', 'c', 'd', 'f', 'g', 'foo'])
   assertCode(code)
 })
 
 test('array destructure', () => {
-  const { code, rootVars } = transform(`
+  const { code, rootRefs } = transform(`
     let n = $ref(1), [a, b = 1, ...c] = $(useFoo())
     console.log(n, a, b, c)
     `)
@@ -235,12 +235,12 @@ test('array destructure', () => {
   expect(code).toMatch(`\nconst b = _shallowRef(__b);`)
   expect(code).toMatch(`\nconst c = _shallowRef(__c);`)
   expect(code).toMatch(`console.log(n.value, a.value, b.value, c.value)`)
-  expect(rootVars).toStrictEqual(['n', 'a', 'b', 'c'])
+  expect(rootRefs).toStrictEqual(['n', 'a', 'b', 'c'])
   assertCode(code)
 })
 
 test('nested destructure', () => {
-  const { code, rootVars } = transform(`
+  const { code, rootRefs } = transform(`
     let [{ a: { b }}] = $(useFoo())
     let { c: [d, e] } = $(useBar())
     console.log(b, d, e)
@@ -252,7 +252,7 @@ test('nested destructure', () => {
   expect(code).toMatch(`\nconst b = _shallowRef(__b);`)
   expect(code).toMatch(`\nconst d = _shallowRef(__d);`)
   expect(code).toMatch(`\nconst e = _shallowRef(__e);`)
-  expect(rootVars).toStrictEqual(['b', 'd', 'e'])
+  expect(rootRefs).toStrictEqual(['b', 'd', 'e'])
   assertCode(code)
 })
 
@@ -270,7 +270,7 @@ test('$$', () => {
 })
 
 test('nested scopes', () => {
-  const { code, rootVars } = transform(`
+  const { code, rootRefs } = transform(`
     let a = $ref(0)
     let b = $ref(0)
     let c = 0
@@ -303,7 +303,7 @@ test('nested scopes', () => {
       return $$({ a, b, c, d })
     }
     `)
-  expect(rootVars).toStrictEqual(['a', 'b', 'bar'])
+  expect(rootRefs).toStrictEqual(['a', 'b', 'bar'])
 
   expect(code).toMatch('a.value++ // outer a')
   expect(code).toMatch('b.value++ // outer b')
index d577347661ce5afae00520cc6cea594cc081088f..4822a4e064c8d393c31bd81efd067bceb9f2c21e 100644 (file)
@@ -31,7 +31,7 @@ export function shouldTransform(src: string): boolean {
   return transformCheckRE.test(src)
 }
 
-type Scope = Record<string, boolean>
+type Scope = Record<string, boolean | 'prop'>
 
 export interface RefTransformOptions {
   filename?: string
@@ -43,7 +43,7 @@ export interface RefTransformOptions {
 export interface RefTransformResults {
   code: string
   map: SourceMap | null
-  rootVars: string[]
+  rootRefs: string[]
   importedHelpers: string[]
 }
 
@@ -99,13 +99,23 @@ export function transformAST(
   ast: Program,
   s: MagicString,
   offset = 0,
-  knownRootVars?: string[]
+  knownRefs?: string[],
+  knownProps?: Record<
+    string, // public prop key
+    {
+      local: string // local identifier, may be different
+      default?: any
+    }
+  >,
+  rewritePropsOnly = false
 ): {
-  rootVars: string[]
+  rootRefs: string[]
   importedHelpers: string[]
 } {
   // TODO remove when out of experimental
-  warnExperimental()
+  if (!rewritePropsOnly) {
+    warnExperimental()
+  }
 
   const importedHelpers = new Set<string>()
   const rootScope: Scope = {}
@@ -113,14 +123,23 @@ export function transformAST(
   let currentScope: Scope = rootScope
   const excludedIds = new WeakSet<Identifier>()
   const parentStack: Node[] = []
+  const propsLocalToPublicMap = Object.create(null)
 
-  if (knownRootVars) {
-    for (const key of knownRootVars) {
+  if (knownRefs) {
+    for (const key of knownRefs) {
       rootScope[key] = true
     }
   }
+  if (knownProps) {
+    for (const key in knownProps) {
+      const { local } = knownProps[key]
+      rootScope[local] = 'prop'
+      propsLocalToPublicMap[local] = key
+    }
+  }
 
   function error(msg: string, node: Node) {
+    if (rewritePropsOnly) return
     const e = new Error(msg)
     ;(e as any).node = node
     throw e
@@ -145,17 +164,19 @@ export function transformAST(
 
   const registerRefBinding = (id: Identifier) => registerBinding(id, true)
 
-  function walkScope(node: Program | BlockStatement) {
+  function walkScope(node: Program | BlockStatement, isRoot = false) {
     for (const stmt of node.body) {
       if (stmt.type === 'VariableDeclaration') {
         if (stmt.declare) continue
         for (const decl of stmt.declarations) {
           let toVarCall
-          if (
+          const isCall =
             decl.init &&
             decl.init.type === 'CallExpression' &&
-            decl.init.callee.type === 'Identifier' &&
-            (toVarCall = isToVarCall(decl.init.callee.name))
+            decl.init.callee.type === 'Identifier'
+          if (
+            isCall &&
+            (toVarCall = isToVarCall((decl as any).init.callee.name))
           ) {
             processRefDeclaration(
               toVarCall,
@@ -164,8 +185,18 @@ export function transformAST(
               stmt
             )
           } else {
+            const isProps =
+              isRoot &&
+              isCall &&
+              (decl as any).init.callee.name === 'defineProps'
             for (const id of extractIdentifiers(decl.id)) {
-              registerBinding(id)
+              if (isProps) {
+                // for defineProps destructure, only exclude them since they
+                // are already passed in as knownProps
+                excludedIds.add(id)
+              } else {
+                registerBinding(id)
+              }
             }
           }
         }
@@ -303,26 +334,48 @@ export function transformAST(
     }
   }
 
-  function checkRefId(
+  function rewriteId(
     scope: Scope,
     id: Identifier,
     parent: Node,
     parentStack: Node[]
   ): boolean {
     if (hasOwn(scope, id.name)) {
-      if (scope[id.name]) {
+      const bindingType = scope[id.name]
+      if (bindingType) {
+        const isProp = bindingType === 'prop'
+        if (rewritePropsOnly && !isProp) {
+          return true
+        }
+        // ref
         if (isStaticProperty(parent) && parent.shorthand) {
           // let binding used in a property shorthand
           // { foo } -> { foo: foo.value }
+          // { prop } -> { prop: __prop.prop }
           // skip for destructure patterns
           if (
             !(parent as any).inPattern ||
             isInDestructureAssignment(parent, parentStack)
           ) {
-            s.appendLeft(id.end! + offset, `: ${id.name}.value`)
+            if (isProp) {
+              s.appendLeft(
+                id.end! + offset,
+                `: __props.${propsLocalToPublicMap[id.name]}`
+              )
+            } else {
+              s.appendLeft(id.end! + offset, `: ${id.name}.value`)
+            }
           }
         } else {
-          s.appendLeft(id.end! + offset, '.value')
+          if (isProp) {
+            s.overwrite(
+              id.start! + offset,
+              id.end! + offset,
+              `__props.${propsLocalToPublicMap[id.name]}`
+            )
+          } else {
+            s.appendLeft(id.end! + offset, '.value')
+          }
         }
       }
       return true
@@ -331,7 +384,7 @@ export function transformAST(
   }
 
   // check root scope first
-  walkScope(ast)
+  walkScope(ast, true)
   ;(walk as any)(ast, {
     enter(node: Node, parent?: Node) {
       parent && parentStack.push(parent)
@@ -371,7 +424,7 @@ export function transformAST(
         // walk up the scope chain to check if id should be appended .value
         let i = scopeStack.length
         while (i--) {
-          if (checkRefId(scopeStack[i], node, parent!, parentStack)) {
+          if (rewriteId(scopeStack[i], node, parent!, parentStack)) {
             return
           }
         }
@@ -424,7 +477,7 @@ export function transformAST(
   })
 
   return {
-    rootVars: Object.keys(rootScope).filter(key => rootScope[key]),
+    rootRefs: Object.keys(rootScope).filter(key => rootScope[key] === true),
     importedHelpers: [...importedHelpers]
   }
 }
index 728cd8e732a81e1c7ea2d5481c5e54a9c67bdaea..e9dd1717bb25d641a830478d465ec63f3f51967e 100644 (file)
@@ -11,7 +11,8 @@ import {
   SetupContext,
   Suspense,
   computed,
-  ComputedRef
+  ComputedRef,
+  shallowReactive
 } from '@vue/runtime-test'
 import {
   defineEmits,
@@ -21,7 +22,8 @@ import {
   useAttrs,
   useSlots,
   mergeDefaults,
-  withAsyncContext
+  withAsyncContext,
+  createPropsRestProxy
 } from '../src/apiSetupHelpers'
 
 describe('SFC <script setup> helpers', () => {
@@ -77,26 +79,62 @@ describe('SFC <script setup> helpers', () => {
     expect(attrs).toBe(ctx!.attrs)
   })
 
-  test('mergeDefaults', () => {
-    const merged = mergeDefaults(
-      {
-        foo: null,
-        bar: { type: String, required: false }
-      },
-      {
+  describe('mergeDefaults', () => {
+    test('object syntax', () => {
+      const merged = mergeDefaults(
+        {
+          foo: null,
+          bar: { type: String, required: false },
+          baz: String
+        },
+        {
+          foo: 1,
+          bar: 'baz',
+          baz: 'qux'
+        }
+      )
+      expect(merged).toMatchObject({
+        foo: { default: 1 },
+        bar: { type: String, required: false, default: 'baz' },
+        baz: { type: String, default: 'qux' }
+      })
+    })
+
+    test('array syntax', () => {
+      const merged = mergeDefaults(['foo', 'bar', 'baz'], {
         foo: 1,
-        bar: 'baz'
-      }
-    )
-    expect(merged).toMatchObject({
-      foo: { default: 1 },
-      bar: { type: String, required: false, default: 'baz' }
+        bar: 'baz',
+        baz: 'qux'
+      })
+      expect(merged).toMatchObject({
+        foo: { default: 1 },
+        bar: { default: 'baz' },
+        baz: { default: 'qux' }
+      })
+    })
+
+    test('should warn missing', () => {
+      mergeDefaults({}, { foo: 1 })
+      expect(
+        `props default key "foo" has no corresponding declaration`
+      ).toHaveBeenWarned()
     })
+  })
 
-    mergeDefaults({}, { foo: 1 })
-    expect(
-      `props default key "foo" has no corresponding declaration`
-    ).toHaveBeenWarned()
+  describe('createPropsRestProxy', () => {
+    const original = shallowReactive({
+      foo: 1,
+      bar: 2,
+      baz: 3
+    })
+    const rest = createPropsRestProxy(original, ['foo', 'bar'])
+    expect('foo' in rest).toBe(false)
+    expect('bar' in rest).toBe(false)
+    expect(rest.baz).toBe(3)
+    expect(Object.keys(rest)).toEqual(['baz'])
+
+    original.baz = 4
+    expect(rest.baz).toBe(4)
   })
 
   describe('withAsyncContext', () => {
index 54391fc42294d63daff97d92f9e032a24e096351..292b2f42f7afad6d3cf8e8c885baff7141232cee 100644 (file)
@@ -1,4 +1,5 @@
-import { isPromise } from '../../shared/src'
+import { ComponentPropsOptions } from '@vue/runtime-core'
+import { isArray, isPromise, isFunction } from '@vue/shared'
 import {
   getCurrentInstance,
   setCurrentInstance,
@@ -7,11 +8,7 @@ import {
   unsetCurrentInstance
 } from './component'
 import { EmitFn, EmitsOptions } from './componentEmits'
-import {
-  ComponentObjectPropsOptions,
-  PropOptions,
-  ExtractPropTypes
-} from './componentProps'
+import { ComponentObjectPropsOptions, ExtractPropTypes } from './componentProps'
 import { warn } from './warning'
 
 // dev only
@@ -195,15 +192,24 @@ function getContext(): SetupContext {
  * @internal
  */
 export function mergeDefaults(
-  // the base props is compiler-generated and guaranteed to be in this shape.
-  props: Record<string, PropOptions | null>,
+  raw: ComponentPropsOptions,
   defaults: Record<string, any>
-) {
+): ComponentObjectPropsOptions {
+  const props = isArray(raw)
+    ? raw.reduce(
+        (normalized, p) => ((normalized[p] = {}), normalized),
+        {} as ComponentObjectPropsOptions
+      )
+    : raw
   for (const key in defaults) {
-    const val = props[key]
-    if (val) {
-      val.default = defaults[key]
-    } else if (val === null) {
+    const opt = props[key]
+    if (opt) {
+      if (isArray(opt) || isFunction(opt)) {
+        props[key] = { type: opt, default: defaults[key] }
+      } else {
+        opt.default = defaults[key]
+      }
+    } else if (opt === null) {
       props[key] = { default: defaults[key] }
     } else if (__DEV__) {
       warn(`props default key "${key}" has no corresponding declaration.`)
@@ -212,6 +218,27 @@ export function mergeDefaults(
   return props
 }
 
+/**
+ * Used to create a proxy for the rest element when destructuring props with
+ * defineProps().
+ * @internal
+ */
+export function createPropsRestProxy(
+  props: any,
+  excludedKeys: string[]
+): Record<string, any> {
+  const ret: Record<string, any> = {}
+  for (const key in props) {
+    if (!excludedKeys.includes(key)) {
+      Object.defineProperty(ret, key, {
+        enumerable: true,
+        get: () => props[key]
+      })
+    }
+  }
+  return ret
+}
+
 /**
  * `<script setup>` helper for persisting the current instance context over
  * async/await flows.
index d5f1ce1b039b6b45892e3097c53ea7ee2de09cf5..b1a594263c1fef9188dec825da87ce14ca3ef26f 100644 (file)
@@ -70,6 +70,7 @@ export {
   withDefaults,
   // internal
   mergeDefaults,
+  createPropsRestProxy,
   withAsyncContext
 } from './apiSetupHelpers'
 
index 248c237f772e630661d5db5274896342bb19fdae..a6f00d6a0fb687b9382a7199b610b5942f76e7cb 100644 (file)
@@ -12,7 +12,7 @@
     "vite": "^2.5.10"
   },
   "dependencies": {
-    "@vue/repl": "^0.4.1",
+    "@vue/repl": "^0.4.2",
     "file-saver": "^2.0.5",
     "jszip": "^3.6.0"
   }
index 71f441e8424e38d4642be2c8926012e89f3d8da9..06913b3912ddf81c6ae5f27a195de7972c698fa2 100644 (file)
@@ -16,13 +16,27 @@ const store = new ReplStore({
     : `${location.origin}/src/vue-dev-proxy`
 })
 
+// enable experimental features
+const sfcOptions = {
+  script: {
+    refTransform: true,
+    propsDestructureTransform: true
+  }
+}
+
 // persist state
 watchEffect(() => history.replaceState({}, '', store.serialize()))
 </script>
 
 <template>
   <Header :store="store" />
-  <Repl :store="store" :showCompileOutput="true" :autoResize="true" />
+  <Repl
+    :store="store"
+    :showCompileOutput="true"
+    :autoResize="true"
+    :sfcOptions="sfcOptions"
+    :clearConsole="false"
+  />
 </template>
 
 <style>
index ac8344bd458ad93fa6c95d7e5e6c38f3c9a6f4cd..e790d1deb123613b1969df1abfd4ce402d6b6758 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
   resolved "https://registry.yarnpkg.com/@vue/consolidate/-/consolidate-0.17.3.tgz#9614d25a2eb263fa5df18ce98b0a576142e0ec83"
   integrity sha512-nl0SWcTMzaaTnJ5G6V8VlMDA1CVVrNnaQKF1aBZU3kXtjgU9jtHMsEAsgjoRUx+T0EVJk9TgbmxGhK3pOk22zw==
 
-"@vue/repl@^0.4.1":
-  version "0.4.1"
-  resolved "https://registry.yarnpkg.com/@vue/repl/-/repl-0.4.1.tgz#b2062bea2baa077520eb01b87df51fff357961be"
-  integrity sha512-Rq9q0MHRA0YRGBE2VSFL5ZkllqLK5HnFK9/+6Iu75M39BZAacwf808fHPTN0bhYGkJ63ur2i0sEYnnNzWzluPg==
+"@vue/repl@^0.4.2":
+  version "0.4.2"
+  resolved "https://registry.yarnpkg.com/@vue/repl/-/repl-0.4.2.tgz#594d36061201195222bc91d187cd766d692f2046"
+  integrity sha512-Spg+M7dENa0jfjEhb2odLP5uqy28fBAMmURL+vFQUIkxcvtFaMkKY3viklhFqacfQqD6YTZfnLWSXyB+F6r/eQ==
 
 "@zeit/schemas@2.6.0":
   version "2.6.0"