]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler-sfc): support intersection and union types in macros
authorEvan You <yyx990803@gmail.com>
Wed, 12 Apr 2023 13:17:57 +0000 (21:17 +0800)
committerEvan You <yyx990803@gmail.com>
Wed, 12 Apr 2023 13:34:13 +0000 (21:34 +0800)
close #7553

packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineEmits.spec.ts.snap
packages/compiler-sfc/__tests__/compileScript/defineEmits.spec.ts
packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts [new file with mode: 0644]
packages/compiler-sfc/src/script/defineProps.ts
packages/compiler-sfc/src/script/resolveType.ts

index 5add78a28b35aea972a4ae10441315027afad6bc..729c019a555b9d404538453cc6505b4af6aef6c2 100644 (file)
@@ -191,6 +191,22 @@ export default /*#__PURE__*/_defineComponent({
 
     
     
+return { emit }
+}
+
+})"
+`;
+
+exports[`defineEmits > w/ type (union) 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+
+export default /*#__PURE__*/_defineComponent({
+  emits: [\\"foo\\", \\"bar\\", \\"baz\\"],
+  setup(__props, { expose: __expose, emit }) {
+  __expose();
+
+    
+    
 return { emit }
 }
 
index 3920f08efb8c0ae5a154ea04ca963a3267a7d40e..67d9674b54ce46ceac96166159967ac3340cf7bf 100644 (file)
@@ -47,13 +47,13 @@ const emit = defineEmits(['a', 'b'])
 
   test('w/ type (union)', () => {
     const type = `((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)`
-    expect(() =>
-      compile(`
+    const { content } = compile(`
     <script setup lang="ts">
     const emit = defineEmits<${type}>()
     </script>
     `)
-    ).toThrow()
+    assertCode(content)
+    expect(content).toMatch(`emits: ["foo", "bar", "baz"]`)
   })
 
   test('w/ type (type literal w/ call signatures)', () => {
diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts
new file mode 100644 (file)
index 0000000..12d18e4
--- /dev/null
@@ -0,0 +1,179 @@
+import { TSTypeAliasDeclaration } from '@babel/types'
+import { parse } from '../../src'
+import { ScriptCompileContext } from '../../src/script/context'
+import {
+  inferRuntimeType,
+  resolveTypeElements
+} from '../../src/script/resolveType'
+
+describe('resolveType', () => {
+  test('type literal', () => {
+    const { elements, callSignatures } = resolve(`type Target = {
+      foo: number // property
+      bar(): void // method
+      'baz': string // string literal key
+      (e: 'foo'): void // call signature
+      (e: 'bar'): void
+    }`)
+    expect(elements).toStrictEqual({
+      foo: ['Number'],
+      bar: ['Function'],
+      baz: ['String']
+    })
+    expect(callSignatures?.length).toBe(2)
+  })
+
+  test('reference type', () => {
+    expect(
+      resolve(`
+    type Aliased = { foo: number }
+    type Target = Aliased
+    `).elements
+    ).toStrictEqual({
+      foo: ['Number']
+    })
+  })
+
+  test('reference exported type', () => {
+    expect(
+      resolve(`
+    export type Aliased = { foo: number }
+    type Target = Aliased
+    `).elements
+    ).toStrictEqual({
+      foo: ['Number']
+    })
+  })
+
+  test('reference interface', () => {
+    expect(
+      resolve(`
+    interface Aliased { foo: number }
+    type Target = Aliased
+    `).elements
+    ).toStrictEqual({
+      foo: ['Number']
+    })
+  })
+
+  test('reference exported interface', () => {
+    expect(
+      resolve(`
+    export interface Aliased { foo: number }
+    type Target = Aliased
+    `).elements
+    ).toStrictEqual({
+      foo: ['Number']
+    })
+  })
+
+  test('reference interface extends', () => {
+    expect(
+      resolve(`
+    export interface A { a(): void }
+    export interface B extends A { b: boolean }
+    interface C { c: string }
+    interface Aliased extends B, C { foo: number }
+    type Target = Aliased
+    `).elements
+    ).toStrictEqual({
+      a: ['Function'],
+      b: ['Boolean'],
+      c: ['String'],
+      foo: ['Number']
+    })
+  })
+
+  test('function type', () => {
+    expect(
+      resolve(`
+    type Target = (e: 'foo') => void
+    `).callSignatures?.length
+    ).toBe(1)
+  })
+
+  test('reference function type', () => {
+    expect(
+      resolve(`
+    type Fn = (e: 'foo') => void
+    type Target = Fn
+    `).callSignatures?.length
+    ).toBe(1)
+  })
+
+  test('intersection type', () => {
+    expect(
+      resolve(`
+    type Foo = { foo: number }
+    type Bar = { bar: string }
+    type Baz = { bar: string | boolean }
+    type Target = { self: any } & Foo & Bar & Baz
+    `).elements
+    ).toStrictEqual({
+      self: ['Unknown'],
+      foo: ['Number'],
+      // both Bar & Baz has 'bar', but Baz['bar] is wider so it should be
+      // preferred
+      bar: ['String', 'Boolean']
+    })
+  })
+
+  // #7553
+  test('union type', () => {
+    expect(
+      resolve(`
+    interface CommonProps {
+      size?: 'xl' | 'l' | 'm' | 's' | 'xs'
+    }
+
+    type ConditionalProps =
+      | {
+          color: 'normal' | 'primary' | 'secondary'
+          appearance: 'normal' | 'outline' | 'text'
+        }
+      | {
+          color: number
+          appearance: 'outline'
+          note: string
+        }
+
+    type Target = CommonProps & ConditionalProps
+    `).elements
+    ).toStrictEqual({
+      size: ['String'],
+      color: ['String', 'Number'],
+      appearance: ['String'],
+      note: ['String']
+    })
+  })
+
+  // describe('built-in utility types', () => {
+
+  // })
+
+  describe('errors', () => {
+    test('error on computed keys', () => {
+      expect(() => resolve(`type Target = { [Foo]: string }`)).toThrow(
+        `computed keys are not supported in types referenced by SFC macros`
+      )
+    })
+  })
+})
+
+function resolve(code: string) {
+  const { descriptor } = parse(`<script setup lang="ts">${code}</script>`)
+  const ctx = new ScriptCompileContext(descriptor, { id: 'test' })
+  const targetDecl = ctx.scriptSetupAst!.body.find(
+    s => s.type === 'TSTypeAliasDeclaration' && s.id.name === 'Target'
+  ) as TSTypeAliasDeclaration
+  const raw = resolveTypeElements(ctx, targetDecl.typeAnnotation)
+  const elements: Record<string, string[]> = {}
+  for (const key in raw) {
+    elements[key] = inferRuntimeType(ctx, raw[key])
+  }
+  return {
+    elements,
+    callSignatures: raw.__callSignatures,
+    raw
+  }
+}
index bd462a2a8ea86105fb1ea27b1176f051a166181c..ee8b5e55734e04f4cac0ec0c9da21c2328fb0532 100644 (file)
@@ -193,20 +193,15 @@ function resolveRuntimePropsFromType(
   const elements = resolveTypeElements(ctx, node)
   for (const key in elements) {
     const e = elements[key]
-    let type: string[] | undefined
+    let type = inferRuntimeType(ctx, e)
     let skipCheck = false
-    if (e.type === 'TSMethodSignature') {
-      type = ['Function']
-    } else if (e.typeAnnotation) {
-      type = inferRuntimeType(ctx, e.typeAnnotation.typeAnnotation)
-      // skip check for result containing unknown types
-      if (type.includes(UNKNOWN_TYPE)) {
-        if (type.includes('Boolean') || type.includes('Function')) {
-          type = type.filter(t => t !== UNKNOWN_TYPE)
-          skipCheck = true
-        } else {
-          type = ['null']
-        }
+    // skip check for result containing unknown types
+    if (type.includes(UNKNOWN_TYPE)) {
+      if (type.includes('Boolean') || type.includes('Function')) {
+        type = type.filter(t => t !== UNKNOWN_TYPE)
+        skipCheck = true
+      } else {
+        type = ['null']
       }
     }
     props.push({
index ba41757069e82eff82168d020b0874358a877e89..6711784a7afc2fdc9a3d2ca766fa0eb0cb00e3de 100644 (file)
@@ -16,7 +16,8 @@ import { UNKNOWN_TYPE } from './utils'
 import { ScriptCompileContext } from './context'
 import { ImportBinding } from '../compileScript'
 import { TSInterfaceDeclaration } from '@babel/types'
-import { hasOwn } from '@vue/shared'
+import { hasOwn, isArray } from '@vue/shared'
+import { Expression } from '@babel/types'
 
 export interface TypeScope {
   filename: string
@@ -63,24 +64,37 @@ function innerResolveTypeElements(
       addCallSignature(ret, node)
       return ret
     }
-    case 'TSExpressionWithTypeArguments':
+    case 'TSExpressionWithTypeArguments': // referenced by interface extends
     case 'TSTypeReference':
       return resolveTypeElements(ctx, resolveTypeReference(ctx, node))
+    case 'TSUnionType':
+    case 'TSIntersectionType':
+      return mergeElements(
+        node.types.map(t => resolveTypeElements(ctx, t)),
+        node.type
+      )
   }
   ctx.error(`Unsupported type in SFC macro: ${node.type}`, node)
 }
 
 function addCallSignature(
   elements: ResolvedElements,
-  node: TSCallSignatureDeclaration | TSFunctionType
+  node:
+    | TSCallSignatureDeclaration
+    | TSFunctionType
+    | (TSCallSignatureDeclaration | TSFunctionType)[]
 ) {
   if (!elements.__callSignatures) {
     Object.defineProperty(elements, '__callSignatures', {
       enumerable: false,
-      value: [node]
+      value: isArray(node) ? node : [node]
     })
   } else {
-    elements.__callSignatures.push(node)
+    if (isArray(node)) {
+      elements.__callSignatures.push(...node)
+    } else {
+      elements.__callSignatures.push(node)
+    }
   }
 }
 
@@ -112,6 +126,45 @@ function typeElementsToMap(
   return ret
 }
 
+function mergeElements(
+  maps: ResolvedElements[],
+  type: 'TSUnionType' | 'TSIntersectionType'
+): ResolvedElements {
+  const res: ResolvedElements = Object.create(null)
+  for (const m of maps) {
+    for (const key in m) {
+      if (!(key in res)) {
+        res[key] = m[key]
+      } else {
+        res[key] = createProperty(res[key].key, type, [res[key], m[key]])
+      }
+    }
+    if (m.__callSignatures) {
+      addCallSignature(res, m.__callSignatures)
+    }
+  }
+  return res
+}
+
+function createProperty(
+  key: Expression,
+  type: 'TSUnionType' | 'TSIntersectionType',
+  types: Node[]
+): TSPropertySignature {
+  return {
+    type: 'TSPropertySignature',
+    key,
+    kind: 'get',
+    typeAnnotation: {
+      type: 'TSTypeAnnotation',
+      typeAnnotation: {
+        type,
+        types: types as TSType[]
+      }
+    }
+  }
+}
+
 function resolveInterfaceMembers(
   ctx: ScriptCompileContext,
   node: TSInterfaceDeclaration
@@ -252,6 +305,11 @@ export function inferRuntimeType(
       }
       return types.size ? Array.from(types) : ['Object']
     }
+    case 'TSPropertySignature':
+      if (node.typeAnnotation) {
+        return inferRuntimeType(ctx, node.typeAnnotation.typeAnnotation)
+      }
+    case 'TSMethodSignature':
     case 'TSFunctionType':
       return ['Function']
     case 'TSArrayType':