diff --git a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineEmits.spec.ts.snap b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineEmits.spec.ts.snap index 5add78a28b3..729c019a555 100644 --- a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineEmits.spec.ts.snap +++ b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineEmits.spec.ts.snap @@ -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 } } diff --git a/packages/compiler-sfc/__tests__/compileScript/defineEmits.spec.ts b/packages/compiler-sfc/__tests__/compileScript/defineEmits.spec.ts index 3920f08efb8..67d9674b54c 100644 --- a/packages/compiler-sfc/__tests__/compileScript/defineEmits.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/defineEmits.spec.ts @@ -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(` `) - ).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 index 00000000000..12d18e40687 --- /dev/null +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -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(``) + 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 = {} + for (const key in raw) { + elements[key] = inferRuntimeType(ctx, raw[key]) + } + return { + elements, + callSignatures: raw.__callSignatures, + raw + } +} diff --git a/packages/compiler-sfc/src/script/defineProps.ts b/packages/compiler-sfc/src/script/defineProps.ts index bd462a2a8ea..ee8b5e55734 100644 --- a/packages/compiler-sfc/src/script/defineProps.ts +++ b/packages/compiler-sfc/src/script/defineProps.ts @@ -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({ diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index ba41757069e..6711784a7af 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -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':